Module 7: Macros Docs And Lineage

Documenting models and columns

Description

Since module 2, _models.yml has already had a description: per model — a short sentence, enough to remember what each one is about while the project is small. But fact_orders's columns, apart from an occasional comment in the SQL itself, have no formal documentation at all: nothing tells a new teammate what store_id means, why quantity is never negative, or exactly what formula produces revenue. This lesson closes that gap: it adds description: to every column of dim_store, dim_date, and fact_orders, and presents a new mechanism — docs blocks — for reusing a complete documentation paragraph, instead of repeating it inside every YAML entry.

Connection to the module. Lessons 2 and 3 solved reusing logic — a macro, invoked from a model. This lesson solves reusing text: a docs block gets written once, in a .md file, and gets referenced from any description: with {{ doc('name') }} — the same "write once, use in several places" idea from lessons 2 and 3, now applied to documentation instead of SQL.

An analogy: the jar's label, and the insert cited by reference

description: on a column is the label stuck directly on the jar: short, specific, visible with just a glance at that jar. A docs block is different — it's like a medication's package insert, shared by several different presentations of the same product (tablets, syrup, injectable): instead of reprinting the complete "directions for use" text on every box, each one simply says "see attached insert," and the same long text gets reused without copying it. {{ doc('fact_orders_revenue') }} works the same way: instead of writing a long paragraph explaining how revenue gets calculated directly inside _models.yml, the column simply references the docs block by name, and dbt inserts the complete text when generating documentation.

description: on every column of the three marts

Extend models/marts/_models.yml with a description: for every column that didn't have one yet — dim_store and dim_date had no column descriptions at all until now; fact_orders already had data_tests: since module 4, and this lesson adds description: alongside each one:

# models/marts/_models.yml
version: 2

models:
  - name: dim_store
    description: "Store dimension, a dbt model over stg_stores via ref()."
    columns:
      - name: store_id
        description: "Store identifier, this dimension's primary key (S01, S02, S03)."
      - name: store_name
        description: "The store's commercial name."
      - name: city
        description: "City where the store operates."

  - name: dim_date
    description: "Date dimension, a fixed August 2026 range generated with a deterministic recursive CTE."
    columns:
      - name: date_key
        description: "Date as an integer in YYYYMMDD format, the compact form most dimensional warehouses use to join against a fact."
      - name: calendar_date
        description: "The same date as a date type, used for the real join against fact_orders."
      - name: day_of_week
        description: "Day of the week's name in English (Monday, Tuesday...), calculated with strftime."
      - name: month
        description: "Month number (1-12), extracted from calendar_date."
      - name: quarter
        description: "Quarter of the year (1-4), extracted from calendar_date."
      - name: year
        description: "Calendar year, extracted from calendar_date."
      - name: is_weekend
        description: "True if calendar_date falls on Saturday or Sunday."

  - name: fact_orders
    description: "Sales fact, order-line grain. Joins stg_orders with dim_store and dim_date via ref()."
    columns:
      - name: order_id
        description: "Order line identifier, this fact's primary key."
        data_tests:
          - unique
          - not_null
      - name: store_id
        description: "Foreign key toward dim_store."
        data_tests:
          - relationships:
              arguments:
                to: ref('dim_store')
                field: store_id
      - name: product_id
        description: "Identifier of the product sold in this order line."
        data_tests:
          - accepted_values:
              arguments:
                values: ['P001', 'P002', 'P003', 'P004']
      - name: quantity
        description: "Units sold in this order line."
        data_tests:
          - is_positive:
              arguments:
                strict: true
      - name: unit_price
        description: "Unit sale price, in the same currency as the product catalog's unit_cost."
        data_tests:
          - is_positive:
              arguments:
                strict: false
      - name: revenue
        description: "{{ doc('fact_orders_revenue') }}"
      - name: order_ts
        description: "The order's exact date and time, the column dim_date.calendar_date uses for the join."

Notice two things: first, every data_tests: from module 4 stays exactly the same, with no change at all — description: is a sibling key to data_tests: within the same column entry, never a replacement. Second, the revenue column doesn't have quoted plain text like the others — it has "{{ doc('fact_orders_revenue') }}", a Jinja invocation inside the YAML. That's this lesson's new piece.

Worked example: revenue's docs block

revenue is the only column in the whole project whose meaning isn't obvious with a short sentence — it's a macro's result, and it's worth documenting how it gets calculated, not just what it represents. Instead of writing that paragraph directly inside _models.yml, create it as a docs block, in a .md file inside models/:

{% docs fact_orders_revenue %}
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.
{% enddocs %}

Save it as models/marts/_docs.md. Three rules govern this file:

  • {% docs name %} ... {% enddocs %} is a docs block's specific syntax — similar to module 5's {% snapshot %}/{% endsnapshot %} or module 4's {% test %}/{% endtest %}: an opening tag with a name, a closing tag with the sibling label. The text between both tags can be any markdown: paragraphs, code in backticks, lists.
  • The name (fact_orders_revenue) has to be unique across the whole project. There's no automatic prefix by folder or by model — if two docs blocks anywhere in the project used the same name, dbt couldn't decide which one matches each doc(), as you're going to confirm in Common mistakes.
  • The file can live anywhere inside model-paths — it doesn't need to share a name with the model it documents, or be in the same folder. Saving it next to _models.yml, with the _ prefix you've already used for configuration files since module 2, is a convention, not a technical requirement.

Confirming dbt recognizes the documentation, without generating anything yet

description: and docs blocks don't affect any dbt run or dbt test — they're pure metadata, invisible to the warehouse — so the correct way to confirm them at this point is dbt parse, the same command you already used to validate YAML syntax without running anything.

dbt parse

What to expect. No error output at all — the same "silence means success" pattern you already saw with dbt parse in earlier modules. If doc('fact_orders_revenue') couldn't find any docs block with that exact name, dbt parse would fail with a specific compilation error, which you're going to see in Common mistakes.

Common mistakes

Referencing a docs block that doesn't exist, due to a typo in the name. What happens: someone writes {{ doc('fact_order_revenue') }} (missing the "s" in "orders") in the revenue column, a name nearly identical to the real one but not exact. Why it happens: unlike ref(), which dbt can validate against the project's real list of models with clear suggestions, a typo in a docs block's name produces a message pointing at the model that references it, not directly at the misspelled name. How to spot it: dbt parse (or any command that compiles the project) fails with:

Compilation Error
  Documentation for 'model.kiosko_analytics.fact_orders' depends on doc 'fact_order_revenue' which was not found

How to fix it: check the docs block's exact name in the .md file ({% docs fact_orders_revenue %}) against the name used in the invocation (doc('fact_orders_revenue')) — they have to match letter for letter, with no folder or model prefix added automatically.

Declaring two docs blocks with the same name in different files. What happens: someone, documenting a new model in the future, reuses the name fact_orders_revenue for a completely different docs block, in another .md file. Why it happens: with no single place to see every name already in use, it's easy not to notice a name already exists elsewhere in the project. How to spot it: dbt parse fails, with a message pointing at both conflicting files:

Compilation Error
  dbt found two docs with the name "fact_orders_revenue".

  Since these resources have the same name, dbt will be unable to find the correct resource
  when looking for doc("fact_orders_revenue").

  To fix this, change the name of one of these resources:
  - doc.kiosko_analytics.fact_orders_revenue (models/marts/_docs_dup.md)
  - doc.kiosko_analytics.fact_orders_revenue (models/marts/_docs.md)

How to fix it: docs block names share a single namespace for the whole project, regardless of which folder each .md file lives in — prefixing the name with the model it belongs to (fact_orders_revenue, not just revenue) is exactly the convention that avoids this clash as the project grows.

Forgetting the quotes around {{ doc(...) }} in the YAML. What happens: someone writes description: {{ doc('fact_orders_revenue') }}, with no quotes, right after the colon. Why it happens: in several places in this project — to: ref('dim_store') inside a relationships test, for example — a Jinja invocation inside YAML gets written with no quotes, so it's easy to generalize that habit. How to spot it: YAML interprets unquoted {{ ... }} as the start of a mapping structure ({ } is valid YAML syntax for an object), not as text — the YAML parser fails before dbt even tries to resolve the doc(), with a YAML syntax error, not a dbt error. How to fix it: any description: value that contains a Jinja invocation needs explicit quotes around the entire value — description: "{{ doc('fact_orders_revenue') }}", exactly as in this lesson's worked example — so YAML treats it as a text string, not as a structure.

Exercises

Exercise 1 — Rebuild the docs block from memory. Without looking at the worked example, write models/marts/_docs.md again, with the exact name fact_orders_revenue, and confirm with dbt parse that there's no error.

See solution
{% docs fact_orders_revenue %}
Order line revenue, calculated with the calculate_revenue macro instead of repeating
quantity * unit_price by hand.
{% enddocs %}

The exact text inside the block doesn't matter for dbt parse to pass with no error — the only thing that has to match precisely is the name between {% docs %} and the following space (fact_orders_revenue), because it's the only piece doc('fact_orders_revenue') uses to find the correct block.

Exercise 2 — Add a docs block for store_id. store_id shows up in three places in the project: dim_store.store_id (the primary key), fact_orders.store_id (the foreign key). Create a store_id_field docs block that explains, in one sentence, what a store_id represents at Kiosko (S01, S02, S03), and reference it from both columns.

See solution
{% docs store_id_field %}
Kiosko store identifier: S01 (Bogota), S02 (Lima), S03 (Santiago). Fixed, with no new stores
added in this guide's data.
{% enddocs %}
# in dim_store.columns
      - name: store_id
        description: "{{ doc('store_id_field') }}"

# in fact_orders.columns
      - name: store_id
        description: "{{ doc('store_id_field') }}"

This is exactly the payoff the shared-insert analogy promised: if Kiosko opened a fourth store tomorrow, you'd update the docs block once, and both columns referencing it would reflect the change automatically, with no need to touch each description: separately.

Exercise 3 — Argue when a docs block pays off, and when a simple description: is enough. In 2-3 sentences, using this lesson's criteria, explain why dim_store.city uses a one-sentence description:, while fact_orders.revenue uses a complete docs block.

See solution

dim_store.city is self-explanatory with a single short sentence ("City where the store operates") — there's no logic or formula to document, so a docs block would add a layer of indirection with no real benefit. fact_orders.revenue, by contrast, isn't just a column name but the result of a specific macro (calculate_revenue), with a design decision behind it (extracting it from module 6 in lesson 3) worth explaining in more than one sentence, and that could get reused the day some future model also calculates revenue. The practical rule: if a column's documentation fits comfortably in one sentence, a direct description: is enough; if it needs context, examples, or is going to be reused in more than one place, a docs block is the right tool.

Summary and next step

In this lesson you added description: to every column of dim_store, dim_date, and fact_orders, and wrote your first docs block (fact_orders_revenue), referenced from the revenue column with {{ doc('fact_orders_revenue') }}. You confirmed with dbt parse that dbt recognizes all the documentation with no error at all, and saw three concrete errors — misspelled name, duplicate name, missing quotes — with the exact message each one produces.

Before moving on you should be able to: write, from memory, the {% docs name %} ... {% enddocs %} syntax; and explain why description: never makes dbt test fail, unlike data_tests:.

Lesson 5 puts all this documentation to work: you're going to really run dbt docs generate, and inspect the two JSON files — manifest.json and catalog.json — it produces, including the descriptions and docs blocks you just wrote, already resolved inside those artifacts.

Resources