Undocumented dbt Models: Find Every Mart With No Description
Run two commands and one BigQuery query to list every undocumented dbt model in your project, rank them by real usage, and stop new ones shipping.
To find undocumented dbt models, run dbt ls --resource-type model --output json --output-keys name description and filter for empty descriptions with jq. Then cross-check BigQuery INFORMATION_SCHEMA to see which tables actually reached the warehouse with a description. Rank the gaps by 30-day query volume so you document the marts people really use first. The whole diagnosis takes under five minutes.
An undocumented mart is not a cosmetic problem. It is the table where finance and operations each assume a different definition of "net revenue", where a new analyst guesses what status = 3 means, and where nobody can say whether a column is safe to change. A pattern we see repeatedly in growth-stage companies is a dbt project that has outgrown its authors' memory: the logic is clean SQL, but the meaning lives in one person's head.
This post gives you the detection queries first, then a triage framework, then the guardrails that stop the gap reopening. The examples use BigQuery and dbt Core, but the manifest approach works on any warehouse.
Why Do Undocumented dbt Models Matter More in 2026?
Documentation used to be a courtesy to the next hire. Now it is also the context layer for every AI assistant and natural-language query tool pointed at your warehouse. Practitioners quoted in dbt Labs' 2026 report describe metadata, meaning descriptions and names, as what gives data meaning for both humans and AI. A chatbot querying a mart with no descriptions will guess, and it will guess confidently.
The broader data backs the concern. In the same 2026 report, ambiguous data ownership remained a challenge for 41% of respondents, effectively unchanged year over year. Trust in data as a priority jumped from 66% to 83% in a single year. Teams want trusted data but are not funding the boring metadata work that produces it.
In our work with Series A and B fintech and payments companies, the failure mode is consistent. The same metric shows different numbers in two dashboards, and when we trace it back, two marts compute it with different filters and neither has a description saying so. Documentation would not have fixed the logic, but it would have made the conflict visible months earlier.
📺 Watch: Building dbt Docs with an AI Agent
How Do You Find Undocumented dbt Models in Your Project?
Start with the dbt CLI. From your project root, list every mart model with an empty description (dbt 1.5 or later):
dbt ls --quiet --resource-type model --select path:models/marts \
--output json --output-keys name description \
| jq -r 'select(.description == "") | .name'
Every line returned is a mart with no model-level description. The --quiet flag matters, because without it dbt log lines pollute the JSON stream and jq will error.
Model descriptions are only half of it. A model with one sentence at the top and forty bare columns is still undocumented where it counts. For column coverage, read the compiled manifest directly:
dbt parse
jq -r '.nodes[] | select(.resource_type == "model" and (.path | startswith("marts"))) | [.name, (.description | length), ([.columns[] | select(.description != "")] | length), (.columns | length)] | @tsv' target/manifest.json | sort -k3 -n
The output columns are model name, description length, documented columns, and declared columns. One caveat: the manifest only counts columns declared in your YAML. A model selecting sixty columns with five declared will show up as "5 of 5", which flatters you. That is why the warehouse-side check in the next section matters.
If you want a packaged version, the dbt_project_evaluator package ships a model that flags undocumented models. Install it, then run dbt build --select package:dbt_project_evaluator and query fct_undocumented_models. It is solid, but it checks model descriptions, not what landed in the warehouse.

What Does BigQuery Actually Show for Your Documentation?
This is the check most teams skip. Descriptions in YAML only reach BigQuery if persist_docs is enabled, and that is where analysts, BI tools and AI assistants read them. Run this against your marts dataset to find tables with no table-level description:
SELECT t.table_name, t.table_type
FROM `my-project.marts.INFORMATION_SCHEMA.TABLES` AS t
LEFT JOIN `my-project.marts.INFORMATION_SCHEMA.TABLE_OPTIONS` AS o
ON o.table_schema = t.table_schema
AND o.table_name = t.table_name
AND o.option_name = 'description'
WHERE o.table_name IS NULL
ORDER BY t.table_name;
Then measure column-level coverage, which is the figure that reflects what an analyst sees when they open the schema tab:
SELECT
table_name,
COUNT(*) AS total_columns,
COUNTIF(description IS NULL) AS undocumented_columns,
ROUND(SAFE_DIVIDE(COUNTIF(description IS NULL), COUNT(*)), 2) AS pct_undocumented
FROM `my-project.marts.INFORMATION_SCHEMA.COLUMN_FIELD_PATHS`
GROUP BY table_name
ORDER BY undocumented_columns DESC;
If the manifest says a model is documented but this query says every column is NULL, you have found a persist_docs gap. This is the single most common discrepancy we find in audits: a team writes careful YAML for months, and none of it ever reached BigQuery because the config flag was never set.
Running those queries for one dataset is fine. Checking every model in a project is not, so fintel-scan is worth a look. It is a free, open-source (MIT) CLI that runs fifteen static checks across a dbt project locally with no warehouse connection: uvx fintel-scan
Which Undocumented dbt Models Should You Fix First?
Do not run a documentation sprint. They produce a burst of low-quality descriptions ("the customer id") and then decay. Rank by usage instead. This query uses BigQuery job history to show which marts people actually query (see our post on using referenced_tables to find dbt models scanning ghost tables for more on that view):
SELECT
ref.table_id,
COUNT(*) AS queries_30d,
COUNT(DISTINCT user_email) AS distinct_users
FROM `region-eu`.INFORMATION_SCHEMA.JOBS_BY_PROJECT,
UNNEST(referenced_tables) AS ref
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
AND state = 'DONE'
AND ref.dataset_id = 'marts'
GROUP BY ref.table_id
ORDER BY queries_30d DESC;
Join this list against the undocumented list from the previous section. Then apply a simple three-tier rule:
- Tier 1: queried by three or more distinct users, or feeding a BI dashboard. Needs a model description, a description on every column, and a named owner in
meta. Do these this week. - Tier 2: queried occasionally, or recently built. Needs a model description stating grain and purpose. One sentence on grain ("one row per settled payment") prevents most fan-out bugs.
- Tier 3: zero queries in 30 days. Do not document it. Ask whether it should exist. Dead marts are cheaper to delete than to describe.
Change the region qualifier to match your dataset location. Job history also only retains 180 days, so use a recent window.
One real-world example: a Series A payments company asked us why finance and operations reported different settlement volumes. The usage query showed the two teams were querying two different marts with near-identical names. Neither had a description. Writing one line of grain and filter logic per model made the divergence obvious in an afternoon and gave us the case for consolidating into one governed model.
How Do You Fix Undocumented dbt Models Without Slowing the Team?
First, make sure descriptions reach the warehouse. Add this to dbt_project.yml:
models:
my_project:
marts:
+persist_docs:
relation: true
columns: true
Second, write descriptions that carry information the SQL cannot. Good model descriptions state grain, source of truth, refresh cadence and known exclusions. Good column descriptions state units, currency, timezone, and what NULL means. "Amount in minor units, settlement currency, before fees" is worth ten "amount of the transaction" entries.
Third, use doc blocks for any definition that appears in more than one model. A single {% docs net_revenue %} block referenced everywhere means one edit fixes every mart. This is also how you start resolving the "same metric, different numbers" problem before you invest in a full semantic layer.
If your finance logic is still sitting in spreadsheets, this is the moment to document it properly as it moves. Our guide to migrating spreadsheet finance logic into dbt covers how to carry the business meaning across, not just the formulas. In one such migration, moving calculations into dbt models eliminated a class of recurring manual errors and cut weekly maintenance by around 30 minutes.

How Do You Stop Undocumented dbt Models Coming Back?
Detection without enforcement just creates a backlog that regrows. Two guardrails work in practice.
1. A pre-commit hook on marts. The dbt-checkpoint project provides hooks that fail a commit when a model or its columns lack descriptions. Scope it to marts so staging models do not become noise:
repos:
- repo: https://github.com/dbt-checkpoint/dbt-checkpoint
rev: <pin the latest release tag>
hooks:
- id: check-model-has-description
files: ^models/marts/
- id: check-model-columns-have-desc
files: ^models/marts/
These hooks read manifest.json, so run dbt parse in the pipeline before they execute.
2. A ratchet in CI. Legacy projects cannot go to zero overnight, so fail the build only when the count goes up. Store today's number, then:
COUNT=$(dbt ls --quiet --resource-type model --select path:models/marts --output json --output-keys name description | jq -r 'select(.description == "") | .name' | wc -l)
test "$COUNT" -le "$BASELINE"
Lower BASELINE each time someone documents a model. In practice this turns documentation into a normal part of a pull request, not a quarterly clean-up. The same logic applies to ownership: require an owner key under meta on every Tier 1 mart, so when something breaks you know who to ask.
Frequently Asked Questions
Q: How do I find undocumented dbt models?
A: Run dbt ls --quiet --resource-type model --output json --output-keys name description and filter with jq for empty descriptions. For column-level gaps, parse target/manifest.json. Then check BigQuery INFORMATION_SCHEMA.COLUMN_FIELD_PATHS to confirm descriptions reached the warehouse.
Q: How do I check dbt documentation coverage?
A: Count models and columns with non-empty descriptions in manifest.json, or use the dbt_project_evaluator package, which flags undocumented models as a queryable table. Measure coverage on marts first, since staging coverage is a less useful indicator.
Q: Why are my dbt descriptions not showing in BigQuery?
A: Almost always because persist_docs is not enabled. Set relation: true and columns: true under +persist_docs in dbt_project.yml, rebuild the models, and re-run the INFORMATION_SCHEMA query to confirm the descriptions landed.
Q: Should every dbt model have a description?
A: No. Mart and exposure-facing models should be fully documented. Staging and intermediate models need less, and unused models should be deleted rather than documented. Prioritise by query volume and downstream dependencies.
Q: How do I enforce dbt documentation in CI?
A: Use dbt-checkpoint pre-commit hooks scoped to your marts directory, or a CI ratchet that fails the build when the count of undocumented models rises above a stored baseline. Both read the compiled manifest.
Conclusion
The challenge is not writing descriptions. It is knowing which undocumented dbt models carry real business risk, then making documentation a default instead of a clean-up task. At Fintel Analytics, we have helped fintech, payments and e-commerce teams audit their dbt and BigQuery projects, fix persist_docs gaps, consolidate conflicting marts and put governance in CI, usually as a short, scoped engagement that gives the team a working baseline. If your marts are growing faster than anyone can explain them, that is a fixable problem, and every week you leave it open is another week of decisions made on definitions nobody wrote down.
