Docs & Code Guide

Keep dbt docs in sync: regenerate on every build and test coverage in CI

dbt is the rare tool that ships its documentation system in the box, and teams still end up with stale docs, because dbt docs generate only tells the truth about the moment it ran. Keeping dbt docs in sync is three habits: regenerate and host from CI on every merge, push descriptions to where people query, and fail the build when documentation coverage drops.

8 min readFor analytics engineers whose dbt docs site was last generated from someone's laptop

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

247/20003 credits left
Try:

No account needed · Editable canvas, not a picture

What dbt docs generate actually produces

The command writes two files into target/. manifest.json is the compiled project graph: every model, source, test and exposure with its dependencies, built from your code without touching the warehouse. catalog.json is the warehouse side: dbt queries the adapter for the actual tables, columns and types that exist. The docs site, target/index.html, is a single page that reads both files, which is why the lineage graph and the column lists can disagree with production the moment either the code or the warehouse moves on.

That mechanism defines the staleness problem precisely. Docs generated from a laptop describe that laptop's branch and that day's warehouse. The fix is not discipline, it is moving the generate step somewhere that runs on every merge.

Host the docs site from CI on every merge

Run dbt docs generate against production in the deploy job and publish target/ as a static site: GitHub Pages, Netlify, an S3 bucket behind your SSO, anything that serves three files. dbt Cloud does the hosting automatically on scheduled runs if you are there; self-hosted teams get the same result with one CI job. The property that matters is that nobody generates docs by hand anymore, so the site can no longer describe a stale branch.

One caution before publishing anywhere public: manifest.json embeds your model SQL and metadata, so treat the docs site as internal by default and put it behind access control rather than on the open web.

Push descriptions into the warehouse with persist_docs

A description that lives only in the docs site misses the analysts who never open it. The persist_docs config makes dbt write your model and column descriptions into the warehouse as native comments on every run, so they surface in the query editor, in information_schema, and in any catalog that crawls the warehouse. Exposures close the other end: declaring the dashboard or application that consumes your marts puts the downstream dependency into the lineage graph, so impact analysis stops ending at the last model.

# dbt_project.yml: descriptions become warehouse comments on every run
models:
  my_project:
    +persist_docs:
      relation: true
      columns: true

# models/exposures.yml: the dashboard enters the lineage graph
exposures:
  - name: revenue_dashboard
    type: dashboard
    owner:
      name: Analytics team
      email: [email protected]
    depends_on:
      - ref('fct_orders')
      - ref('dim_customers')

Test description coverage instead of hoping for it

Coverage regresses one undocumented model at a time, so put it in CI where regressions get caught. dbt-checkpoint ships pre-commit hooks such as check-model-has-description and check-column-desc-are-same that fail the run when a model or column ships without documentation. The dbt_project_evaluator package takes the audit approach: it builds fact tables like fct_undocumented_models inside your project so you can query the debt and track it down over time.

The pragmatic policy for an existing project is a ratchet: enforce descriptions on new and changed models only, so the gate never demands a documentation sprint, it just refuses to let coverage get worse.

Minimum viable gate

One CI job: dbt docs generate on merge to main, publish target/, run check-model-has-description on changed models. Under an hour to set up, and coverage stops regressing that day.

The lineage view your stakeholders actually open

The dbt docs lineage graph is developer-grade: complete, unstyled, and overwhelming for anyone who does not know what a staging model is. For architecture reviews and stakeholder pages, teams keep a curated lineage diagram: the zones, the marts that matter, the exposures, without four hundred staging nodes. Generate the first version by pasting your model list or manifest excerpt into the dbt lineage diagram generator, and keep it current the same way you keep the code current: an agent connected to the Datadef MCP server, registry name io.datadef/mcp, reads manifest.json after a merge and updates the diagram. Datadef does not watch the repo; the update is one agent call or CI step, and the API key it uses comes from a paid plan. The full loop is in living diagram from dbt.

Shared public, the diagram serves a permanent image URL that re-renders on change, so the copy embedded in your README or wiki follows within minutes. And to keep scope honest: this loop keeps the diagram current. The YAML descriptions themselves are prose, and writing them stays with your team; dbt best practices covers the conventions that make that sustainable.

FAQ

How do I keep dbt docs up to date?

Move dbt docs generate into CI so it runs against production on every merge, and publish target/ as an internal static site. Add persist_docs so descriptions land in the warehouse as comments, declare exposures for downstream consumers, and gate CI with a description check such as dbt-checkpoint's check-model-has-description so coverage cannot regress.

Does dbt docs update automatically?

No. dbt docs generate produces a snapshot: manifest.json from compiling your project and catalog.json from querying the warehouse, read by a static site. It is exactly as fresh as its last run, which is why the standard practice is running it in the deploy job on every merge rather than from a laptop.

What is the difference between manifest.json and catalog.json?

manifest.json is the code side: the compiled graph of models, sources, tests and exposures with their dependencies, produced without a warehouse connection. catalog.json is the warehouse side: the tables, columns and types that actually exist, gathered by querying the adapter during dbt docs generate. The docs site joins the two, so either can be the stale one.

How do I enforce documentation coverage in a dbt project?

Add dbt-checkpoint hooks like check-model-has-description to pre-commit or CI so undocumented models fail the build, and use dbt_project_evaluator's fct_undocumented_models to measure the existing debt. On established projects, apply the gate to new and changed models only, which stops regression without demanding a backfill sprint.

What does persist_docs actually do?

With persist_docs set for relations and columns, dbt writes your descriptions into the warehouse as native table and column comments on every run. They then appear in query editors, in information_schema, and to any data catalog crawling the warehouse, so the documentation reaches analysts who never open the dbt docs site.

Why declare dbt exposures?

An exposure records a downstream consumer, such as a dashboard or an application, as a node in the lineage graph with an owner and the models it depends on. Without exposures, lineage ends at the last model and impact analysis misses the things that actually break; with them, dbt ls can select everything upstream of a named dashboard.