Summarizing dbt Project Metadata From YAML in One Report
Read the dbt manifest built from your properties YAML and report documentation coverage, untested models, owners, tags and source freshness in one script.

A dbt project spreads its documentation across YAML files: model descriptions, column descriptions, tests, owners in meta, tags and source freshness settings. Nothing in dbt demands that any of them exist, and so coverage drifts. Which models have no tests? What fraction of columns are documented? Who owns what? You could read the YAML files, and you would miss things, because the answers combine files and defaults. The better source is the manifest dbt writes when it parses the project, which is the YAML after all the merging. This post reads the manifest and builds a one-page report. It belongs to the cluster on analyzing configuration and metadata held in YAML, and the files it reads are written as in writing dbt model properties in YAML, defining dbt tests in YAML and YAML sources and freshness checks in dbt.
The project
Five models, each a one-line SQL file, with properties that vary on purpose:
models:
- name: customers
description: One row per customer.
meta: {owner: team-a}
config: {tags: [core]}
columns:
- name: id
description: Primary key.
data_tests: [unique, not_null]
- name: name
description: Display name.
- name: orders
description: One row per order.
meta: {owner: team-a}
config: {tags: [core]}
columns:
- name: id
description: Primary key.
data_tests: [unique, not_null]
- name: name
- name: order_items
meta: {owner: team-b}
columns:
- name: id
data_tests: [not_null]
- name: name
- name: payments
description: Payments received.
columns:
- name: id
- name: name
- name: stg_events
columns:
- name: id
- name: name
sources:
- name: raw
schema: main
loaded_at_field: id
tables:
- name: events
freshness: {warn_after: {count: 12, period: hour}}
- name: customerscustomers is fully documented and tested, stg_events has nothing, and the others sit between. That is a realistic spread. dbt parse accepted all of it (exit 0), because dbt has no minimum.
The manifest
dbt parse writes target/manifest.json. Its nodes map holds every model and test with resolved properties; sources holds sources. Reading it takes a few lines of Python:
import json
m = json.load(open("target/manifest.json"))
nodes = m["nodes"]
models = {k: v for k, v in nodes.items() if v["resource_type"] == "model"}
tests = [v for v in nodes.values() if v["resource_type"] == "test"]
print("models:", len(models), "| tests:", len(tests), "| sources:", len(m["sources"]))models: 5 | tests: 5 | sources: 2Five models, five tests (two models carry two each and one carries one) and two sources. The manifest is the effective project: the YAML merged with the SQL, config in dbt_project.yml and defaults, which is why it beats reading files. It is also a stable interface for tooling: the dbt documentation site and many catalog tools read it.
Documentation coverage
Columns and models with descriptions:
cols = [(k, c, v["description"]) for k, mm in models.items() for c, v in mm["columns"].items()]
print("columns:", len(cols), "| with description:", sum(1 for _, _, d in cols if d))
print("models with description:", sum(1 for v in models.values() if v["description"]), "of", len(models))columns: 10 | with description: 3 (30%)
models with description: 3 of 5Three of ten columns are documented, and three of five models. The first number is the one teams track: a column description is what analysts see in the catalog, and a project at 30 percent has most columns unexplained. Set a target and fail the job below it, or report the delta on each pull request. Count only the columns a team has declared; dbt lists the columns you wrote in YAML, not every column the SQL produces, unless you run dbt docs generate against the warehouse to catalog them. A fuller coverage measure joins the declared columns with the catalog's actual columns.
Tests
Which models have no tests? Each test node records the model it attaches to. Compare:
tested = {t["attached_node"] for t in tests if t.get("attached_node")}
print("models with no tests:", sorted(v["name"] for k, v in models.items() if k not in tested))models with no tests: ['payments', 'stg_events']payments and stg_events have none. For a table of payments that is the finding to act on. A more refined report counts tests per model and flags models without a test on the primary key, which needs the test name and column (unique_orders_id), both in the node. A model with a unique and not_null on its key is minimally covered; a model with only not_null is not. See defining dbt tests in YAML.
Ownership and tags
meta is free-form, and an owner convention is a common one:
owners: {None: 2, 'team-a': 2, 'team-b': 1}
tags: {'core': 2}Two models have no owner. That is the report a data platform team wants before an incident, and it is one line of Python. Tags are counted the same way. Owners can feed alerts (route a failed test to the model's owner), and the report shows where the routing would fail. Enforcing the convention means a CI check that every model has meta.owner, or a JSON Schema over the properties files (validating YAML with JSON Schema); dbt's own +meta configuration in dbt_project.yml can set an owner for a whole folder, which is easier than repeating it in every model.
Sources and freshness
sources with freshness: 1 of 2One of two source tables declares a freshness threshold. The other has none, so dbt source freshness ignores it. For each source, the manifest holds the freshness block and the loaded_at_field, so a report can list tables that have no check, or thresholds that are far looser than the loader's schedule.
Making it a habit
Put the script in the repository, run it in CI after dbt parse, and print the numbers in a pull request comment or the job summary. Store the numbers over time (a CSV or a table) and chart documentation coverage, because a coverage that rises slowly is the signal that the habit has taken hold. Add thresholds gradually: first report, then fail on regressions (a pull request must not lower coverage), then require new models to meet a minimum. dbt has packages for this kind of check (the dbt_project_evaluator and dbt-checkpoint projects are examples), and a 30-line script is enough to start and teaches you which rules you want before you adopt a package.
The manifest also supports lineage questions: which models depend on a source with no freshness check, or on a model with no tests. The parent_map and child_map keys give the graph. Combine them with the coverage numbers to rank work: an undocumented model with twenty descendants matters more than one with none.
A report you can paste
The numbers above fit in one small block that a CI job prints at the end of every run:
models: 5 | tests: 5 | sources: 2
columns: 10 | with description: 3 (30%)
models with description: 3 of 5
models with no tests: ['payments', 'stg_events']
owners: {None: 2, 'team-a': 2, 'team-b': 1}
tags: {'core': 2}
sources with freshness: 1 of 2Seven lines tell a reviewer where the project stands, and a diff of this block between a pull request and its base shows whether the change improves or erodes it. Add the new model's own numbers: when a pull request adds a model, the report should show that model is documented, tested and owned. If it is not, the job can fail with a message naming the model and the missing piece ("model refunds has no tests"). That is a cheaper and more specific control than a general rule that every model must be tested, and it applies to new work without demanding that the whole backlog be fixed at once.
For the backlog, sort by impact. A model with many children and no tests belongs at the top; a leaf staging model with no description belongs at the bottom. The manifest's child_map gives the counts. A report that lists the five models with the most descendants and no tests is a to-do list a team can actually finish, where a list of all 140 untested models is ignored.
When a metadata summary of a dbt project is not enough
The metadata is accurate and the data is wrong. A project can have 100 percent documented columns and failing logic. Tests and monitoring check data; this report checks the project's description of it.
The YAML does not declare the columns. Models whose columns are not listed in YAML look undocumented in the manifest, though the warehouse has the columns. Use the catalog for a full picture.
Quality of text is not measured. A description of "Primary key." counts the same as a paragraph. Coverage is a floor.
Other dbt versions. Manifest keys change across dbt versions (the schema is versioned), so pin dbt and check the manifest schema version in metadata.
The manifest format is documented in the dbt artifacts reference, and the properties it reflects in the dbt model properties reference. A neighboring approach for a single file is YAML in dbt, Airflow and other data tools. Check a properties file in the YAML formatter.


