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.
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.
- 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.
- 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.
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.sqlAsk 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.
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.
| Console | logs/dbt.log | target/compiled + target/run | run_results.json | |
|---|---|---|---|---|
| Format | text, default | debug, default — richer by default | raw SQL, no format | JSON, schema v6 |
| Persists? | No — gone when the terminal closes | Yes — append-only, every invocation ever | Yes, until the next command overwrites the relevant files | Yes, until the very next invocation overwrites it |
| Holds a verdict? | Yes — the live summary line | Yes — the same lines, plus everything at debug level | No — the query, not the outcome | Yes — one status per node, per invocation |
| Freshness info? | No | No | No | No — 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.
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.
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.
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.
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.
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.
