Data Lineage Guide

Data lineage for dbt projects: what the DAG gives you and what it does not

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

dbt hands you lineage almost for free. Every ref and source call is an edge, so the DAG in the docs site is generated from the same code that builds the tables and is correct by construction. That covers the middle of the flow. The two ends, where the data came from before dbt saw it and who consumes the marts afterwards, are not in the graph unless you put them there, and neither is the column detail on dbt Core.

7 min readFor analytics engineers whose lineage story starts and ends inside dbt

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

156/20003 credits left
Try:

No account needed · Editable canvas, not a picture

What ref and source already buy you

The dbt DAG is model-to-model lineage derived from the compiled project, with upstream models to the left and downstream models to the right. It is regenerated on every docs build, which means it never drifts from the code, and it is the reason dbt teams often have better internal lineage than teams with a purchased platform.

Sources declared in YAML pin the upstream boundary, so the graph knows that stg_orders comes from a specific raw table in a specific schema. That is the first thing to get complete, because a project where half the staging models select from hardcoded table names has a DAG that silently starts one layer too late.

Tests attached to models make the graph actionable rather than decorative. A not-null test on the join key is what turns a downstream edge into something CI can enforce, and it is the difference between knowing about a dependency and being warned when it breaks.

Three things the dbt DAG does not show

Column-level lineage is not in dbt Core. Model-to-model edges are, field-to-field derivations are not. dbt Labs ships column-level lineage in dbt Catalog, the product previously called Explorer, and its documentation (docs.getdbt.com/docs/explore/column-level-lineage) restricts it to the Enterprise plans. Below that tier it comes from a separate tool or from recording the derivations yourself, and for most projects the practical middle ground is to record column detail only for the fields in regulated or contested reports.

What happens before the source is invisible. The ingestion tool, the API, the CDC stream, and the landing bucket are all upstream of the first raw table, and the DAG starts after them. When the question is why the data is late or wrong, the answer is usually in that missing prefix.

What consumes the marts is invisible unless declared. Exposures exist precisely for this and are chronically underused. Without them, the graph ends at the mart and every dashboard, export, reverse ETL sync, and ML feature table downstream is a dependency nobody can enumerate during a change review.

# models/marts/exposures.yml
exposures:
  - name: revenue_dashboard
    type: dashboard
    maturity: high
    url: https://looker.internal/dashboards/42
    owner:
      name: Finance Analytics
      email: [email protected]
    depends_on:
      - ref('fct_revenue_daily')
      - ref('dim_customers')

  - name: crm_customer_sync
    type: application
    owner:
      name: RevOps
      email: [email protected]
    depends_on:
      - ref('dim_customers')

Making a large DAG readable again

Past a few hundred nodes the DAG stops being a diagram and becomes an inventory. Everything is technically there and nothing can be read at a zoom level where labels are legible. The answer is not a better layout engine, it is a second artifact: a curated map per domain, showing the ingestion end, the handful of models that matter, and the named consumers, with the full DAG remaining the reference for engineers.

That domain map is the thing you show in a review, hand to a new analytics engineer, or attach to an audit response. It is also the one that has to carry the parts the DAG cannot see, which means the ingestion prefix and the consumer suffix drawn explicitly.

Connect the repository holding the dbt project to Datadef read-only, on GitHub, GitLab, or Azure DevOps, and the sync regenerates the diagram and an architecture.md daily from the chosen branch or tag. Commits that change nothing structural do not redraw the diagram, and nodes you moved by hand keep their positions across syncs, so the map you arranged for humans stays arranged. Embed it in the dbt docs site, the repo README, or a Confluence page with one markdown line and it updates with the canvas.

Keep both artifacts

The DAG is the engineer reference and stays authoritative for model dependencies. The domain map is the one non-engineers read. See keeping dbt docs in sync for the regeneration loop.

What a repository sync actually reads in a dbt project

A diagram generated from a repository is only as honest as its file selection, so it is worth stating what gets read. Datadef walks the tree once and classifies: dbt_project.yml is its own class, every .sql file under a models directory is a dbt model, and migrations, SQL outside models, container files, CI workflows, and dependency manifests each get their own. Directories that never describe architecture are skipped, node_modules and .terraform among them, as are lockfiles and binary formats. Jupyter notebooks are on the skipped list, so a model that exists only in a notebook does not appear.

Then the caps apply: 40 files, 250KB in total, 30KB per file, longer files cut at a truncation marker. Per-class ceilings stop one class from taking the budget. Under the balanced default a project contributes at most twelve model files and two project files; set the sync to a content focus and models rise to eighteen with plain SQL at fourteen; set it to an architecture focus and models drop to three while infrastructure code rises to sixteen.

The consequence for a 400-model project is worth saying plainly. The diagram is drawn from a sample of the project plus the shape of the tree around it, not from every model. That is the right trade for an architecture picture and the wrong one for an inventory, which is what the DAG and the docs site already are.

What the selector takes from a dbt repo

  dbt_project.yml            always, its own class
  models/**/*.sql            12 files (default focus)
                             18 (content focus)
                              3 (architecture focus)
  migrations/**              sampled from BOTH ends: the
                             first files show the initial
                             schema, the last the direction
                             of change
  .github/workflows/*.yml    the jobs that run the project
  README, docs/architecture* the prose that names things

  never: node_modules, .terraform, lockfiles, .ipynb,
         anything over 400KB

FAQ

Does dbt have column-level lineage?

dbt Core does not. The DAG built from ref and source calls is model to model. Column-level lineage lives in dbt Catalog, the dbt Cloud product previously named Explorer, and the dbt documentation restricts it to the Enterprise plans. Below that tier it comes from a separate tool, or from recording the derivations yourself for the handful of fields that are actually disputed.

What are dbt exposures and why do they matter for lineage?

Exposures are YAML declarations of downstream consumers such as dashboards, applications, and ML pipelines, with an owner and a URL. They extend the lineage graph past the transformation layer, which is the only way a change review can enumerate what a model change actually affects.

How do I show what happens upstream of my dbt sources?

Not from dbt itself, since the graph begins at the source declaration. The ingestion tool, API, or stream that fills the raw table has to be drawn or declared separately, which is why most useful dbt lineage maps are the DAG plus a hand-added ingestion prefix.

My DAG has thousands of nodes and nobody reads it. What now?

Keep the full DAG as the engineering reference and build a curated map per domain for everyone else: the ingestion end, the handful of models that matter, and the named consumers. Readability is a scoping decision rather than a layout problem, and past roughly thirty nodes a diagram gets searched rather than read, which is where a second artifact starts paying for itself.

Can a dbt lineage diagram stay current without manual updates?

Where the project lives in git, yes. A read-only connection to GitHub, GitLab, or Azure DevOps regenerates the diagram and its architecture document on a daily sync, so the picture follows the chosen branch or tag instead of following someone remembering to redraw it. Commits that change nothing structural are skipped, so the diagram moves only when the project does.