dbt-tree: Lineage in Your Terminal
One command, a readable DAG — no docs server, no browser, no warehouse connection.

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:▲ ancestorsthen▼ 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 lsas 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 change —
dbt-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
lssurface, 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