dbt's dependency graph naturally covers everything inside dbt — models depending on other models. Exposures extend that graph one hop further, into the things outside dbt that actually consume the data: a Looker dashboard, a Tableau workbook, an ML feature pipeline, an internal API. Without exposures, "what breaks if I change this column" stops at the last model; with them, it reaches all the way to the dashboard a VP checks every morning.
Declaring an exposure
An exposure is declared in YAML, usually its own exposures.yml, and references the models it depends on with ref() inside its depends_on — dbt treats it as another node in the DAG, just one that cannot be built, only depended on.
exposures:
- name: executive_revenue_dashboard
type: dashboard
maturity: high
url: https://looker.company.com/dashboards/42
description: >
Daily revenue and churn dashboard reviewed in the Monday
leadership sync. Owned by the Finance data team.
depends_on:
- ref('fct_orders')
- ref('fct_subscriptions')
owner:
name: Finance Data Team
email: finance-data@company.com
Why this changes how you make a change
Before touching fct_orders, running dbt ls --select fct_orders+ shows every downstream model — but not the dashboard built on top of it. With the exposure declared, that same command surfaces executive_revenue_dashboard in the results, and the generated lineage graph in dbt docs draws a node for it. The practical effect: before merging a schema change, you can see exactly which dashboards and owners to notify, instead of finding out when someone in the Monday meeting asks why the number is wrong.
dbt does not verify that the dashboard actually still queries those columns — an exposure is a manually maintained declaration, not a live integration. If nobody updates exposures.yml when a dashboard is retired or rebuilt on different tables, the lineage graph quietly goes stale. Treat exposure maintenance as part of the same PR that changes the dashboard, not a separate housekeeping task.
Running lineage-aware selection with exposures in the graph
The `+` graph operator works the same way toward exposures as it does toward models, which is the main practical payoff:
# everything downstream of fct_orders, including exposures
dbt ls --select fct_orders+
# only exposures affected by a given model
dbt ls --select fct_orders+ --resource-type exposure
# build only what an exposure needs, useful before a demo
dbt build --select +exposure:executive_revenue_dashboard
Where exposures fit next to a dedicated lineage tool
Exposures are cheap and require no extra infrastructure, but they only cover what someone manually declared, and only in the direction dbt already understands (models to consumers). A dedicated lineage platform consuming OpenLineage events can trace lineage automatically from ingestion tools through dbt through BI tools without manual YAML upkeep — worth it once you have enough dashboards that keeping `exposures.yml` current by hand becomes unreliable.
| Exposure type | Typical use |
|---|---|
dashboard | Looker, Tableau, Mode reports |
ml | Feature store or model training pipeline |
application | Internal API or service reading warehouse tables |
notebook | Recurring analyst notebook, not ad hoc queries |
Wrapping up
Add an exposure for anything a real person depends on regularly — a leadership dashboard, a billing pipeline, an ML feature set — so that changing an upstream model surfaces the actual blast radius before you merge, not after someone notices a broken chart. Keep the YAML current as dashboards change, and layer a dedicated lineage tool on top once manual upkeep stops scaling.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.