DBTStructure & Production

Debugging and Production Failure Modes

How analytics engineers turn raw warehouse tables into trusted models — pick a topic on the left and its full breakdown loads here: the mental model, the compiled SQL dbt actually issues, live runs you can drive yourself, and the failure modes that quietly ship wrong numbers.

18 min readTopics chapter readerLevel · Hard
01 · Four places, four different clocks

The Run That Goes Green With Wrong Numbers

Ask which clock you are reading. The console is live and disposable. The file log is durable and the richer twin of the console. target/ is the SQL with no verdict. run_results.json is the verdict with no history. A "why is this missing?" question almost always resolves to naming the wrong clock.

4 min · Topic 1 of 9

stg_listens just failed a scheduled dbt build with a SQL compilation error. Before you touch anything, four different artifacts hold four different, overlapping slices of the truth about that one run — and they disagree on purpose.

Click through all four below before reading past it. Note what each one holds that the others do not, and what each one is silent about.

One failed dbt build. Four places the evidence lives — click one
What it holds, for this run
  • The live PASS / ERROR / SKIP summary line for every node, in the order dbt dispatched it.
  • The top-level wrapper for a failure — Database Error, Compilation Error, FailFast Error — printed once, right when it happens.
  • Default format text: readable, un-timestamped past the hour:minute:second, no thread name.
What it does NOT hold
  • Nothing survives the terminal closing — this is the only one of the four with zero persistence.
  • No per-line invocation_id, no full compiled SQL, no adapter_response detail (rows_affected, timing breakdown).
  • debug-level detail: connection acquire/release, the exact rendered Jinja, the raw query text sent to Snowflake.
console — text format, default
1 of 15 START sql view model staging.stg_listens ........... [RUN]
1 of 15 ERROR creating sql view model staging.stg_listens ..... [ERROR in 0.34s]

Database Error in model stg_listens (models/staging/stg_listens.sql)
  000904 (42000): SQL compilation error: error line 4 at position 4
  invalid identifier 'LISTENER_TIMEZONE'
  compiled SQL at target/run/wavelength_analytics/models/staging/stg_listens.sql
The four disagree on purpose: the console is live and disposable, the file log is the durable and richer twin, target/ is the SQL with no verdict attached, and run_results.json is the verdict with no history — it is overwritten by whatever you run next in this directory.
Core mental model

Ask which clock you are reading. The console is live and disposable. The file log is durable and the richer twin of the console. target/ is the SQL with no verdict. run_results.json is the verdict with no history. A "why is this missing?" question almost always resolves to naming the wrong clock.

Why it matters

The four sources are not redundant copies of the same evidence — each one is the only place a specific fact lives. The console is the only one with zero persistence: close the terminal and it is gone. The file log is richer than the console by default, because its format defaults to debug while the console defaults to text, so connection lifecycle, rendered Jinja and raw query text only ever show up in the file. target/compiled and target/run hold the exact SQL with no verdict attached — enough to reproduce a failure by hand, nothing about whether it succeeded. And run_results.json is the verdict with no history: it is overwritten by the very next invocation in that directory, so if you want to keep it, copy it out before running anything else. One more fact belongs here because it is easy to assume otherwise: dbt source freshness writes sources.json, not run_results.json, and dbt build never runs freshness at all — a green build carries zero freshness information, full stop.

The invocation banner
============================== HH:MM:SS.ffffff | <invocation_id> ============================== — logs/dbt.log is append-only across every run a project has ever done, so find your run by this line, never by scrolling to the bottom.
.args.which
The field in run_results.json that names the subcommand that actually ran. jq -r '.args.which' answers "did tests even execute?" before you trust that a run "passed".
Three different status unions
model/run: success | error | skipped | partial success | no-op. test: pass | error | fail | warn | skipped. freshness: pass | warn | error | runtime error. A single status=="error" filter silently misses every test fail and every freshness runtime error.
Three distinct absences
skipped (its parent failed, and it still gets a result row), no result row at all (never dispatched — nothing to find), and no-op (dispatched, ran, changed nothing). Confusing the second for the first is the most common misreading of a red build.
DBT_ENV_SECRET_*
Scrubbed to ***** in the log — but only values resolved from profiles.yml or packages.yml. A compiled SQL literal or your own log(info=True) call is not a secret to dbt, and lands in logs/dbt.log in plain text.
Same failed run, four artifacts
Consolelogs/dbt.logtarget/compiled + target/runrun_results.json
Formattext, defaultdebug, default — richer by defaultraw SQL, no formatJSON, schema v6
Persists?No — gone when the terminal closesYes — append-only, every invocation everYes, until the next command overwrites the relevant filesYes, until the very next invocation overwrites it
Holds a verdict?Yes — the live summary lineYes — the same lines, plus everything at debug levelNo — the query, not the outcomeYes — one status per node, per invocation
Freshness info?NoNoNoNo — freshness writes sources.json only

Nothing here is a copy of anything else. Losing any one of the four loses a real, distinct kind of fact.

Common mistake

Reading a green dbt build as proof that freshness was checked. dbt source freshness writes sources.json; dbt build never calls it and never writes it. A green build tells you every model, test, seed and snapshot succeeded — it says nothing about whether any source is stale, because it never asked.

Scrolling to the bottom of logs/dbt.log to find "your" run. The file is append-only across the project's entire history. On a busy CI runner the bottom of the file belongs to whatever ran last, which may not be you. Search for the invocation banner with your invocation_id instead.

Better habit

Copy run_results.json out of target/ before you run anything else against a failure you are still investigating — the next invocation destroys it.

Grep for the invocation banner, not the tail of the file, when you open logs/dbt.log for a specific run.

Before trusting "tests passed", run jq -r '.args.which' target/run_results.json — a plain dbt run has none to pass.

What teams actually do with these artifacts, standing

The read-it-after-the-fact workflow above does not scale to fleet-wide observability, so current practice loads results continuously: an on-run-end hook — dbt_artifacts (Brooklyn Data Co., v2.10.1) is the common one — calls dbt_artifacts.upload_results(results) against the in-memory results object during the run itself, not by uploading a JSON file after the fact (the package name is legacy). Elementary is the other real open-source option. dbt platform's Discovery API is the vendor-native alternative. Be honest about what "loaded" means either way: the CONTENT usually is, the FILES usually are not — put file://target/run_results.json @stage then copy into a VARIANT column is the older, still-useful pattern for backfilling from artifacts you already archived. manifest.json (v12) rides alongside run_results.json in that same load: it supplies the node metadata — depends_on, configs, owners — that run_results.json alone never carries, and it is the join key that turns a flat table of statuses into the graph Chapter 12 built.

Provenance of every transcript in this chapter

No transcript below was captured from a live dbt Core 1.12.0 run. Message strings reproduce documented or source text — the Database Error wrapper, the FailFast Error wrapper, the log line shapes — and every timing either reuses Chapter 16's published Wavelength figures or is this KB's own illustrative round number. Chapters 8, 10 and 11 carried the same note for the same reason.

Remember this

Console, file log, target/, and run_results.json disagree on purpose — each is the only place a specific fact survives, and knowing which clock you are reading answers most "why is this missing?" questions before you ask them.