Doc blocks allow column and model descriptions to be defined once in a .md file and referenced from any number of schema YAML files. This prevents duplicate descriptions from diverging across models.
The Syntax
A doc block lives in any .md file within your dbt project’s resource paths (model-paths, seed-paths, etc.):
{% docs customer_id %}Unique identifier for a customer in our system. Generated as a surrogatekey from the source system's customer reference number.{% enddocs %}You reference it from YAML with the doc() function:
columns: - name: customer_id description: "{{ doc('customer_id') }}"Multiple doc blocks can live in the same .md file. Names must be globally unique across your entire project (including installed packages), cannot start with a digit, and only allow alphanumeric characters plus underscores. If a dbt package you have installed defines a customer_id doc block, yours will collide. The two-argument form handles cross-package references:
description: "{{ doc('my_package', 'customer_id') }}"Doc blocks work for models, columns, sources, seeds, snapshots, analyses, macros, and macro arguments. Known bugs exist with exposures and versioned models where 'doc' is undefined errors can appear.
Writing Descriptions Once
The real payoff comes from defining common column descriptions as shared doc blocks. A column like customer_id or order__created_at that appears across your project gets one authoritative definition:
{% docs customer_id %}Unique identifier for a customer in our system. Generated as a surrogatekey from the source system's customer reference number.{% enddocs %}
{% docs order__created_at %}Timestamp (UTC) when the order was first created in the source system.This reflects the initial creation time, not the time it was loadedinto the warehouse.{% enddocs %}Every model that has these columns references the same doc block. Change the wording once, and it propagates everywhere on the next dbt docs generate.
Model-Level Doc Blocks
For model-level descriptions that need more space than a YAML one-liner allows, doc blocks let you use full Markdown with headers, tables, and formatting:
{% docs mrt__marketing__customer_ltv %}Customer lifetime value calculations using a 3-year rolling window.
**Business context**: Used by the retention marketing team to identifyhigh-value customers for targeted campaigns.
**Methodology**: Discounted cash flow with a 10% annual discount rate,applied to all completed orders (excludes refunded transactions).
| Metric | Definition ||--------|-----------|| customer__ltv_usd | Total discounted revenue over the rolling window || customer__ltv_tier | Quartile bucket (1-4) based on ltv_usd |{% enddocs %}Reference it in your schema YAML: description: "{{ doc('mrt__marketing__customer_ltv') }}". For details on what Markdown features are supported, see dbt Docs Markdown Capabilities.
Naming Conventions
Community naming conventions vary. The main patterns:
- Plain column names (
customer_id) when a single definition works project-wide. Simplest, works well when column semantics are consistent. - Model-scoped column names (
orders_col_total_amount) when a column means different things in different models. More verbose but avoids ambiguity. - Model-level blocks often use
table_<model_name>to distinguish from column blocks.
Pick a convention, document it in your style guide, and stick with it. Inconsistency in naming is worse than any particular choice.
Adoption pattern
Shared doc blocks for the most repeated columns (IDs, timestamps, common metrics) address the bulk of duplicated and inconsistent descriptions — typically 10–15 column names account for most of the repetition. Model-level doc blocks for mart models are the next area, since those are what business users encounter most often. persist_docs can push descriptions into warehouse comments alongside the docs site.
Settling on a file organization pattern early avoids a refactor later. For propagating existing descriptions automatically through the DAG without writing doc blocks for every column, see dbt-osmosis.