DBTBuilding a Project

Seeds, Documentation, and Exposures

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 · Medium
01 · Open the docs site you already have

A Project That Cannot Explain Itself

manifest.json is parsed from disk — free and offline — while catalog.json is queried from your warehouse. Every staleness question reduces to which one you are looking at.

4 min · Topic 1 of 10

Ten chapters in, Wavelength has seventeen nodes, a snapshot nobody can rebuild, and a docs site that has existed the whole time. Run dbt docs generate and dbt docs serve and it comes up on port 8080.

Click around the replica below in its "After Chapter 10" state before reading anything else. Find out for yourself what it does and does not say.

6 of 7 resources with no description
localhost:8080 — dbt Docs (Legacy)wavelength_analytics
Project
Sources (2)
Models (4)
Snapshots (1)
No Seeds section and no Exposures section, because there are none. The tree renders what manifest.json contains and nothing else.
fct_listens
columntype (catalog.json)description (manifest.json)
listen_idVARCHAR
user_idVARCHAR
episode_idVARCHAR
show_idVARCHAR
listened_atTIMESTAMP_NTZ
listen_dateDATE
ms_playedNUMBER
pct_completedNUMBER
is_completedBOOLEAN
9 columns, 0 described. The types arrived from the warehouse; the empty column is the one a human owes you.
which artifact produced this paneTwo artifacts, merged in the browser. The column NAMES and TYPES come from catalog.json — the half that ran metadata queries against Snowflake. The DESCRIPTIONS come from manifest.json. A column described in YAML but absent from the warehouse has no row here at all, and dbt never says so.
Core mental model

The docs site is a static renderer over two JSON files: manifest.json is parsed from disk — free and offline — while catalog.json is queried from your warehouse. Every staleness question reduces to which one you are looking at.

Why it matters

The emptiness is not cosmetic. Open agg_daily_listens and its Referenced by pane is blank — and that blank is a true statement about the project and a lie about the business, because the exec dashboard reads that table every Monday morning. A graph that looks complete is the dangerous failure mode: nothing errors, nothing warns, and the honest answer "nothing downstream" is exactly what somebody will act on before they drop a column. Everything in this chapter exists to make the graph's silence trustworthy.

manifest.json
Schema v12, written into target/ by any dbt command that parses the project — every command except deps, clean, debug and init.
catalog.json
Schema v1, written by step 3 of dbt docs generate: information-schema-style metadata queries against the warehouse, one per object.
seed
A CSV in your repo that dbt parses, infers a schema for, and loads as a table you can ref(), test, document and select — the only node type whose VALUES are reviewed in a pull request. Section 2 builds one; Section 4 decides which files qualify.
exposure
A declaration in YAML that something outside dbt consumes a node: a dashboard, a notebook, an analysis, an ML job, an application. It creates no object, issues no SQL and cannot fail. It buys exactly two things — DAG membership so lineage reaches a named human, and selectability via +exposure:name.
analysis
SQL in analyses/ that is compiled but never materialised. It can use {{ ref() }}, so it survives a rename; it produces no relation, so it can never become a table nobody meant to own. Section 9 ships one.
The two artifacts behind every pane on that site
manifest.jsoncatalog.json
Produced byParsing files on diskMetadata queries against the warehouse
Costs a connection?No — dbt parse works offlineYes. This is the half that needs compute
HoldsEvery node, every config, every rendered descriptionPer-object type/database/schema/comment; per-column name/type/comment/position, plus adapter-dependent stats
The load-bearing keyparent_map and child_map — these ARE the lineage graph. The docs site renders them; impact analysis traverses themerrors — the key that exists precisely because a per-object metadata query can fail on its own
Goes stale whenYour checkout is stale — re-parse and it is currentAnything is rebuilt; and it is wrong-by-absence for models never built at all
Escape hatchNone needed--empty-catalog skips step 3 entirely

Chapter 2 already told you docs can go stale. This chapter answers the harder question: WHICH HALF, and which one you pay for. They go stale for completely different reasons and on completely different clocks.

Common mistake

Reading an empty Referenced by pane as "nothing depends on this, so it is safe to change". child_map is honest about what dbt knows, and dbt only knows about ref() and source() calls inside your project. A dashboard, a reverse-ETL sync, a notebook and an analyst with a SQL client are all invisible to it. The pane says nothing depends on agg_daily_listens; four people would disagree, and none of them find out until Monday.

Treating documentation as a task for the end of the project. The descriptions are cheap while the model is fresh in your head and expensive six months later when the person who wrote it has left. Worse, the site is generated from what exists — a project documented at the end is documented once, and drifts from that day forward.

Better habit

Open the docs site for a project you inherit before you open its SQL. Ten minutes of clicking tells you which parts of the graph anyone has ever cared about.

When a Referenced by pane is empty, ask a human before you believe it — the pane is a statement about your dbt project, not about your company.

Keep dbt docs generate in the same job that builds production, so the artifact and the warehouse are never more than one run apart.

Chapter 5 already promised you this site

Chapter 5 opened with a ballot on what a declared source buys you, and one of the seven capabilities on it was "It shows up in lineage graphs and the generated docs site" — one of the six Chapter 3 capabilities a hard-coded table reference silently loses. (The seventh on that ballot, the one with no Chapter 3 equivalent at all, was source freshness.) That was the claim about what you get for free; this chapter is the invoice. The by-product is real, and it is exactly as good as the descriptions and exposures somebody wrote down. Chapter 3 owns the other half of the mechanism — the DAG comes from ref() and source(), not from a diagramming tool — and nothing here changes that.

Provenance of every transcript in this chapter

None of the terminal output in this chapter was captured from a live dbt Core 1.12.0 run. The message strings reproduce documented or source text — dbt's seed-size warnings, the exposure owner validation error, the Database Error wrapper line — and the timings are this KB's own published Wavelength figures. Where a message belongs to the warehouse rather than to dbt, it is left as a placeholder rather than invented. Chapter 8 shipped the same note about the unit-test diff format, and for the same reason.

The version this chapter targets

dbt Core 1.12.0, released 16 July 2026, is the current stable Python line and is what this chapter pins every behaviour to. The Rust line — dbt Core v2.0, Apache 2.0, in the dbt-core repository — is at 2.0.0-alpha.5 as of 20 July 2026 and is named here only where behaviour differs. The hosted product is the dbt platform, and what used to be called Explorer is dbt Catalog.

Remember this

The docs site is a renderer over manifest.json and catalog.json — one free and offline, one paid for in warehouse queries — and an empty Referenced by pane is a true sentence about your project and a false one about your company.