Every dbt run produces a machine-readable graph of your project
dbt (data build tool) compiles your SQL models, tests, and sources into a dependency graph and, as a side effect of every dbt compile, dbt run, or dbt build, writes that entire graph to disk as `manifest.json` inside the target/ directory. This isn't a debug artifact — it's the file that powers dbt's auto-generated documentation site, third-party lineage tools, and most CI checks that reason about "what changed" rather than "did the SQL run."
What's actually inside it
manifest.json is large (a real project's can run into tens of megabytes), but its structure is consistent: a nodes object keyed by a unique ID per model/test/seed, each value describing that node in full —
{
"nodes": {
"model.my_project.orders": {
"resource_type": "model",
"database": "analytics",
"schema": "public",
"name": "orders",
"compiled_code": "SELECT ...",
"depends_on": { "nodes": ["model.my_project.stg_orders", "source.my_project.raw.orders"] },
"columns": { "id": { "data_type": "integer" } },
"tags": ["finance"]
}
},
"sources": { "...": "..." },
"child_map": { "model.my_project.stg_orders": ["model.my_project.orders"] }
}depends_on.nodes is what turns the whole file into a graph — every node lists exactly what it depends on, and child_map is the pre-computed inverse (what depends on this node), which is what lineage tools actually traverse when they render a DAG or compute blast radius for a proposed change.
Why CI pipelines read this file directly
Two dbt features are built entirely on parsing manifest.json rather than re-running your whole project:
- State comparison (
dbt run --select state:modified+) diffs the currentmanifest.jsonagainst a previous run's, finds every model whose compiled SQL or config changed, and runs only those models plus their downstream dependents — the mechanism behind "only rebuild what actually changed" CI pipelines on large projects where a full rebuild would be too slow to run on every pull request. - Impact analysis — before merging a change, tools walk
child_mapfrom the changed node outward to answer "what breaks downstream if I change this column," entirely from the JSON graph, without executing any SQL.
run_results.json: what actually happened, not what was planned
A companion file, run_results.json, is written after execution finishes and records the outcome per node — status, timing, rows affected, and the compiled SQL that actually ran, as opposed to manifest.json's static description of the graph before execution. Diffing two run_results.json files from a slow run and a fast one is a legitimate way to spot which specific model's execution time regressed, without adding any extra instrumentation.
Working with the graph yourself
Since it's plain JSON, manifest.json is queryable with any general-purpose JSON tool without needing dbt's own APIs — format it to browse a specific model's metadata, or write a small script that walks depends_on.nodes to answer a lineage question your team's dashboard doesn't already surface. This is also how most third-party dbt lineage and cataloging products actually work under the hood: they parse this same file rather than reimplementing dbt's own dependency resolution.