← Back to list

dbt-tree: Lineage in Your Terminal

One command, a readable DAG — no docs server, no browser, no warehouse connection.

Karthik Rajashekaran · 2026-06-10 21:16 · 3 claps · 3.6 min read
#dbt-labs #dbt-core #dbt-cloud #data-engineering
Open on Medium ↗
Wiki topics: 🔧 · Data Engineering 🥊 · Combat Sports

dbt-tree: Lineage in Your Terminal

One command, a readable DAG — no docs server, no browser, no warehouse connection.

The two questions I ask all day

When you work in a large dbt project, two questions come up constantly:

  • What feeds this model?
  • What breaks if I change it?

The honest way I used to answer them was clunky: spin up dbt docs serve, wait for the catalog to build, open a browser, and drag a hairball graph around until I found the node I cared about. Or I'd run dbt ls and read a flat, alphabetical wall of names, mentally reconstructing the edges. Either way I was one context-switch away from the terminal where I was already working.

I wanted the answer in one command, in the same window, in under a second. So I built dbt-tree.

What it does

dbt-tree takes any dbt selector and prints the resulting DAG as a tree, right in your terminal:

dbt-tree "my_model+"     # downstream
dbt-tree "+my_model"     # upstream
dbt-tree "+my_model+"    # both directions
my_model (view) *
└── stg_orders (table)
    └── int_orders_joined (table)
        ├── fct_orders (table)
        │   ├── mart_revenue (table)
        │   └── ...
        └── fct_order_items (table)

The originally-selected model is marked with *. Nodes are colored by resource type, and the suffix shows materialization (table/view) or resource type (source/seed/snapshot).

The tree also re-roots itself based on the selector, so the model you care about is always at the top:

  • model+ (downstream) — root is the model, children are its descendants.
  • +model (upstream) — root is the model, children are its ancestors, walking up to sources and seeds.
  • +model+ (both) — two sections: ▲ ancestors then ▼ descendants.

The one design decision that matters

dbt-tree does not re-implement dbt's selector logic. It forwards your selector verbatim to dbt ls --output json and builds the tree from whatever dbt returns.

That means every selector you already know works exactly as it does in dbt — model+, +model, 2+model+3, tag:nightly+, path:models/marts, set unions — because dbt itself is doing the selecting. There's zero selector grammar to maintain and no drift from your installed dbt version.

And because dbt ls is parse-only, drawing the tree needs no warehouse connection at all.

Why it matters

  • No context switch. The answer appears in the terminal you’re already in — no docs server, no browser tab.
  • Fast. Parse-only means no catalog build and no warehouse round-trip.
  • Familiar. If you know dbt selectors, you already know dbt-tree. Unknown flags are even forwarded to dbt ls as an escape hatch.
  • Safe on big projects. Cycle, depth, and node-count guards keep a gnarly DAG from flooding your terminal.

Where it shines

  • Impact analysis before a changedbt-tree "+my_model+" shows ancestors and descendants at a glance.
  • Onboarding — trace a mart back to its sources without learning the docs UI.
  • Reviewing a PR — eyeball the blast radius of a model edit in one line.
  • Debugging a build — see exactly what sits between a failing source and a downstream mart.

The story of building it

It came together in a single focused day. The whole thing is small — about 580 lines of Python across five modules — but the interesting part is that most of that day wasn’t the happy path. The happy path (render a colored tree with rich) was the quick, fun 20%. The other 80% was the edge cases that make a tool feel finished:

  • Drawing it like Unix tree. I deliberately duplicate shared subtrees so you see a node everywhere it's reached. That immediately raises the spectre of diamonds and cycles — so there's a per-path "visited" guard plus depth and node-count budgets so a messy DAG can't explode your scrollback.
  • Orientation from the selector. Inferring up / down / both from the + operators, and then re-rooting upstream trees so ancestors render as children, took a few iterations to feel natural.
  • The dbt-core vs dbt Fusion gotcha. Fusion has stricter parsing and a different ls surface, so dbt-tree detects it, prefers dbt-core, and fails with a clear, actionable message instead of cryptic parse errors.
  • “Which dbt?” Resolving the right executable (explicit flag → $DBT_TREE_DBT → active virtualenv → PATH) so it "just works" once your dbt venv is active — and it even offers did-you-mean suggestions when a selector matches nothing.

The lesson I keep relearning: the demo is easy; respecting how messy real projects are is the actual work.

Try it

pip install dbt-tree          # install into the same env as your dbt-core
cd your_dbt_project
dbt-tree "your_model+"

No warehouse connection needed — dbt ls only parses the project.

The takeaway

Lineage shouldn’t require a browser. If you live in the terminal, the shape of your DAG should be one command away — and it should speak the selector language you already know.

Source: dbt-tree on GitHub. Apache-2.0, open source.

Tags: #dbt #DataEngineering #AnalyticsEngineering #Python #CLI #DataLineage #OpenSource #DeveloperTools


메타데이터
post_id
022f2c352eaa
slug
dbt-tree-lineage-in-your-terminal-022f2c352eaa
url
https://medium.com/@karthikrajashekaran/dbt-tree-lineage-in-your-terminal-022f2c352eaa
canonical_url
https://medium.com/@karthikrajashekaran/dbt-tree-lineage-in-your-terminal-022f2c352eaa
author_url
https://medium.com/@karthikrajashekaran
status
ok
fetched_at
2026-06-27 07:40:21