Module 7: Macros Docs And Lineage

Generating docs with `dbt docs generate`

Description

Every description: and every docs block you wrote in the previous lesson live, so far, scattered across YAML and Markdown files — useful for whoever reads the project's source code, but not yet gathered anywhere someone could explore without opening each file one by one. dbt docs generate is the command that brings all that information together — descriptions, columns, the warehouse's real data types, declared tests, dependencies between models — into two JSON artifacts: manifest.json and catalog.json. This lesson really runs the command, inspects both files' real structure, and explains a piece you already lived through without naming it in module 6: why dbt docs generate, just like dbt build, needs --vars once fact_orders is an incremental model.

Connection to the module. Lesson 4 wrote the documentation; this lesson turns it into real artifacts. And it connects directly with module 6: the same Required var 'run_date' not found error you saw there, running dbt build with no arguments, shows up again here — evidence that documenting a project isn't an isolated operation of reading YAML, it requires dbt to compile every model in the project, including fact_orders.

manifest.json versus catalog.json: two different questions

Before running anything, it's worth understanding what each file produces, because they answer different questions:

  • manifest.json is what dbt knows about your project, without touching the warehouse. It contains every model, source, macro, test, snapshot, and docs block that exists in your code — their descriptions, their declared columns, what each one depends on (ref(), source(), even which macros it invokes). It's built entirely from your .sql and .yml files, with no need for any model to have even run once.
  • catalog.json is what the warehouse really has, right now. It contains the real column types (VARCHAR, DECIMAL(10,2), TIMESTAMP), exactly as DuckDB reports them when querying its tables — not what you declared, but what physically exists. To generate it, dbt needs to connect to the warehouse and run introspection queries against every model that's already built.

This distinction explains each name: the manifest is a declaration of intent (your code); the catalog is an inventory of what really exists (the warehouse).

Worked example: running dbt docs generate

With fact_orders already incremental since module 6, run the command with --vars, the same way you've already been running dbt build since then:

dbt docs generate --vars '{"run_date": "2026-08-09"}'

What to expect.

Running with dbt=1.12.2
Registered adapter: duckdb=1.11.0
Found 7 models, 15 data tests, 1 snapshot, 4 sources, 502 macros

Concurrency: 4 threads (target='dev')

Building catalog
Catalog written to /path/to/your/kiosko_analytics/target/catalog.json

Confirm both files show up in target/:

ls -la target/manifest.json target/catalog.json

What to expect. Two files, several hundred kilobytes each — manifest.json includes the complete definition of the project's 502 macros, most inherited from dbt-core, so its size isn't proportional only to Kiosko's seven models.

Why this command also requires --vars

Try, on purpose, running the command with no --vars, over a project where fact_orders already exists as a table:

dbt docs generate

What to expect.

Found 7 models, 15 data tests, 1 snapshot, 4 sources, 502 macros

Concurrency: 4 threads (target='dev')

[ERROR]: Encountered an error:
Runtime Error
  Compilation Error in model fact_orders (models/marts/fact_orders.sql)
    Required var 'run_date' not found in config:
    Vars supplied to fact_orders = {}

Exactly the same error you already diagnosed in module 6's mini-project. The reason is the same in both cases: dbt docs generate needs to compile every model in the project to build its part of manifest.json — columns, dependencies, compiled SQL — and compiling fact_orders.sql means evaluating the complete {% if is_incremental() %} block. Since the fact_orders table already exists in the warehouse, is_incremental() returns True, Jinja enters the branch that uses var("run_date"), and without that value the compilation fails — before dbt docs generate even gets to building the catalog. This isn't behavior exclusive to dbt build or dbt run: any dbt command that needs to compile the complete project inherits the same operational contract module 6 established.

Inspecting manifest.json: fact_orders's entry

manifest.json is plain JSON — it can be inspected with any tool that reads JSON. Use Python, already familiar from foundations and data-modeling, to look at fact_orders's entry:

import json

manifest = json.load(open("target/manifest.json"))
fact_orders = manifest["nodes"]["model.kiosko_analytics.fact_orders"]

print("description:", fact_orders["description"])
print("materialized:", fact_orders["config"]["materialized"])
print("depends_on.nodes:", fact_orders["depends_on"]["nodes"])
print("depends_on.macros:", fact_orders["depends_on"]["macros"])
print()
print("revenue column, description:")
print(fact_orders["columns"]["revenue"]["description"])

What to expect.

description: Sales fact, order-line grain. Joins stg_orders with dim_store and dim_date via ref().
materialized: incremental
depends_on.nodes: ['model.kiosko_analytics.stg_orders', 'model.kiosko_analytics.dim_store', 'model.kiosko_analytics.dim_date']
depends_on.macros: ['macro.kiosko_analytics.calculate_revenue', 'macro.dbt.is_incremental']

revenue column, description:
Order line revenue, calculated with the `calculate_revenue(quantity_col, price_col)` macro
(`macros/calculate_revenue.sql`) instead of repeating `quantity * unit_price` by hand. The numeric
result didn't change compared with this model's earlier version -- the only thing that changed is
that the calculation now lives in a single, reusable place.

Three things to notice. First, the revenue column's description doesn't show {{ doc('fact_orders_revenue') }} unresolved — it shows the docs block's complete text, already substituted: dbt resolves any doc() before writing manifest.json, so the file always contains final text, never pending Jinja. Second, depends_on.nodes is exactly the dependency tree you already know from module 3 — stg_orders, dim_store, dim_date — now as a list inside an artifact, instead of a command's output. Third, and new in this module: depends_on.macros includes calculate_revenue — the manifest tracks which macros a model depends on, not just which other models or sources. That field didn't exist in module 6's manifest, because fact_orders didn't invoke any project-specific macro yet.

Which fields vary on every run, and which don't

Before moving on, an important warning for any JSON artifact you generate yourself: some fields are deterministic — they're going to be identical every time you run the command over the same code — and others change on every execution, no matter that nothing in the project changed. Look at the metadata block:

print(json.dumps(manifest["metadata"], indent=2))

What to expect (with the volatile values explicitly flagged):

{
  "dbt_schema_version": "https://schemas.getdbt.com/dbt/manifest/v12.json",
  "dbt_version": "1.12.2",
  "generated_at": "2026-08-13T01:51:18.751543Z",
  "invocation_id": "4f0582e2-4871-47da-9a5e-6f51f623581a",
  "invocation_started_at": "2026-08-13T01:51:18.531062Z",
  "project_name": "kiosko_analytics",
  "project_id": "36ebfa1d8bd7b708bf92227417bf982e",
  "user_id": "ecde221d-af0a-4052-97c7-58cbcaa39693",
  "adapter_type": "duckdb"
}

dbt_schema_version, dbt_version, project_name, and adapter_type are fixed — they're going to be identical on your machine, as long as you use this guide's same dbt-core and dbt-duckdb versions. generated_at, invocation_id, and invocation_started_at are volatile — they change on every run, a timestamp and a unique identifier generated fresh every time, never byte-for-byte reproducible between two different executions. project_id and user_id are anonymous identifiers dbt generates once per project and installation, for usage statistics — they're not going to match between your machine and any other one either. When you compare your own manifest.json against what you see in this lesson, expect nodes, sources, docs's content to match in structure; never expect metadata to match byte for byte.

Inspecting catalog.json: what the warehouse really has

Now the other file — the real column types, exactly as DuckDB reports them:

catalog = json.load(open("target/catalog.json"))
fact_orders_catalog = catalog["nodes"]["model.kiosko_analytics.fact_orders"]

for col_name, col_info in fact_orders_catalog["columns"].items():
    print(f"{col_name:<12}{col_info['type']}")

print()
print("number of sources in catalog.json:", len(catalog["sources"]))

What to expect.

order_id    VARCHAR
store_id    VARCHAR
product_id  VARCHAR
quantity    INTEGER
unit_price  DECIMAL(10,2)
revenue     DECIMAL(18,2)
order_ts    TIMESTAMP

number of sources in catalog.json: 0

Two observations. First, revenue is DECIMAL(18,2), not DECIMAL(10,2) like unit_price — DuckDB automatically expands a DECIMAL's precision when it multiplies two columns, to avoid losing digits to overflow; nobody declared that type explicitly in any .sql file, it's the engine's own inference. Second, and more important: catalog.json has zero sources, even though manifest.json recognizes Kiosko's four sources with no problem. The reason connects directly to module 2: source('kiosko_raw', 'orders') with external_location compiles directly to a file path ('raw_data/kiosko/orders_*.csv'), it never creates a real table or view inside DuckDB — there's no physical relation for the catalog to introspect. catalog.json can only list objects that really exist in the warehouse; a source read with external_location never becomes one.

dbt docs serve: the browsable site, described

With manifest.json and catalog.json already generated, dbt docs serve spins up a local web server that reads them and assembles a browsable site — the same index.html that's already in target/ since you ran dbt docs generate:

dbt docs serve

What to expect.

Serving docs at 8080
To access from your browser, navigate to: http://localhost:8080

Press Ctrl+C to exit.

The default port is 8080 — change it with dbt docs serve --port 8081 if that port is already in use by something else on your machine. The site it serves includes every description you wrote in lesson 4, a column explorer per model with catalog.json's real types, and an interactive lineage graph — the same lineage this module's lesson 6 is going to read directly from manifest.json, with no need to open any browser. Stop the server with Ctrl+C when you're done exploring it; there's no need to leave it running to complete the rest of this guide.

Common mistakes

Running dbt docs generate before running dbt run at least once. What happens: someone, over a freshly deleted kiosko.duckdb, runs dbt docs generate directly, expecting the same complete catalog.json as always. Why it happens: the command does compile the complete manifest with no problem at all — manifest.json doesn't depend on any model having run — so the run looks successful. How to spot it: catalog.json is going to report missing or empty column types for any model that doesn't exist yet as a real table or view in the warehouse — the catalog can only describe what it finds, and a freshly deleted database has nothing to describe yet. How to fix it: run dbt build (or at least dbt run) before dbt docs generate, so the warehouse has real objects for the catalog to introspect — the order matters, even though both commands accept running at any time with no complaint.

Confusing manifest.json with catalog.json when looking for a description. What happens: someone looks for a column's description: inside catalog.json, and doesn't find it. Why it happens: both files talk about the same columns, so it's easy to assume either one has all the information. How to spot it: check this lesson's "manifest.json versus catalog.json" section — catalog.json only has type, index, and comment per column (what the warehouse reports), never description (what you wrote in YAML). How to fix it: any text you wrote by hand — description:, docs blocks, declared data_tests: — lives in manifest.json; any data only the warehouse can report — real types, whether a table has statistics — lives in catalog.json. dbt docs serve combines both to show the complete site, but as files they're independent.

Expecting catalog.json to list Kiosko's four sources. What happens: someone looks for kiosko_raw.orders inside catalog.json["sources"] and finds an empty list, and assumes something failed in the generation. Why it happens: manifest.json does recognize all four sources with no problem, so the absence in catalog.json feels like an inconsistency. How to spot it: check meta.external_location on any of the four sources in _sources.yml (module 2) — all of them read directly from a file, with no persistent relation created in DuckDB. How to fix it: this isn't an error — it's the correct, expected behavior, explained in this lesson's catalog.json section. If Kiosko ever loaded its raw data with dbt seed instead of external_location, those sources would show up in the catalog, because dbt seed does create real tables.

Exercises

Exercise 1 — Count how many docs are in the manifest. Using Python, count how many entries manifest["docs"] has, and print their names. Why is the number greater than 1, if you only wrote one docs block in lesson 4?

See solution
print(len(manifest["docs"]))
for key in manifest["docs"]:
    print(key)

The result is 2: doc.kiosko_analytics.fact_orders_revenue (the one you wrote in lesson 4) and doc.dbt.__overview__ (a docs block dbt-core includes by default in every project, the welcome page that shows up when you first open dbt docs serve). You didn't write it — it ships with dbt-core, the same way the 501 macros the project already counted before you added yours in lesson 3.

Exercise 2 — Confirm dbt parse doesn't generate catalog.json. Run rm -f target/catalog.json, then dbt parse, and confirm with ls target/ whether the file came back.

See solution

catalog.json doesn't come back — dbt parse only rebuilds manifest.json (and some internal artifacts like partial_parse.msgpack), because its only job is validating and compiling the project, with no connection to the warehouse to introspect anything. This explicitly confirms this lesson's distinction: manifest.json is "what dbt knows about your code," something dbt parse can build with no touch on the database; catalog.json is "what the warehouse really has," something only a command that does connect — like dbt docs generate, dbt run, or dbt build — can produce.

Exercise 3 — Argue why manifest.json and catalog.json have to be generated together for dbt docs serve to work well. In 2-3 sentences, explain what a teammate would see on the dbt docs serve site if only manifest.json existed, with no up-to-date catalog.json.

See solution

They'd see the complete descriptions, declared tests, and lineage graph — everything that lives in manifest.json — but any column would show up with no real data type, or with an outdated type if the catalog is from a run before a schema change. The documentation site combines both artifacts because they answer complementary questions: "what does this column mean, and what does it depend on?" (manifest) and "what data type does it really have, today, in the warehouse?" (catalog) — without the second one, the documentation would have gaps exactly where real evidence, not declared evidence, matters most.

Summary and next step

In this lesson you ran dbt docs generate --vars '{"run_date": "..."}', confirming it produces manifest.json and catalog.json in target/. You saw, with the real error, why this command needs --vars once fact_orders is incremental — the same operational contract from module 6, now applied to a different command. You inspected both files with Python: manifest.json with fact_orders's description, model dependencies, and macro dependencies; catalog.json with the real column types, including the deliberate absence of the four sources. And you described dbt docs serve — port 8080 by default — with no need to keep it running.

Before moving on you should be able to: explain, in your own words, the difference between manifest.json and catalog.json; and name which metadata fields are fixed and which vary on every run.

Lesson 6 uses manifest.json for something more specific: rebuilding fact_orders's complete dependency tree — the same lineage you already inspected with dbt ls in module 3 — now read directly from the artifact, with no additional dbt command to run.

Resources