DBTStructure & Production

Orchestrating dbt: Airflow, the dbt platform, and Cosmos

How analytics engineers turn raw warehouse tables into trusted models — pick a topic on the left and its full breakdown loads here: the mental model, the compiled SQL dbt actually issues, live runs you can drive yourself, and the failure modes that quietly ship wrong numbers.

18 min readTopics chapter readerLevel · Hard
01 · Fifteen outcomes, one exit code

One Command, One Bit of Truth

where does the run's state live? In the dbt process (and its target/ directory), in Airflow's metadata database, or in a vendor's. Nothing else about the four options actually differs.

4 min · Topic 1 of 8

An orchestrator does not run your dbt project. It starts a dbt process, waits for it, and stores one boolean. Everything hard about orchestrating dbt comes from that boolean being a lossy summary of the graph as Chapter 12 left it — 23 nodes, 15 of them buildable — and from dbt's own record of what happened, target/run_results.json, dying with the container that produced it.

Pick a recovery question in the widget below before you read anything else. One of the four is unanswerable by every setup on the list, and finding out which one is the fastest way to stop believing the thing most teams believe about task granularity.

Monday 2026-07-27, 06:00 UTC — dbt build ran, and the orchestrator kept one bitFifteen nodes each finished in a specific state. Airflow stored an exit code. Pick a question you would actually ask at 08:00, and see which setups can answer it from their own durable state without a human opening a log file.
node outcomes dbt knew1512 OK · 1 ERROR · 2 SKIPPED
bits the orchestrator stored1exit code 1 — the task is red
setups that answer this question2of 4 · “Which model failed?”
One Airflow taskNOa single BashOperator running dbt buildThe orchestrator holds one red square for fifteen nodes. The answer exists — in the task log, and in target/run_results.json if that survived — but nothing in Airflow's own state carries it.
Four Airflow tasksPARTIALone --select per layerThe red square is named marts, which narrows fifteen candidates to six. Better than one, and still not an answer.
Task per modelYESCosmos, 15 rendered tasksThe red square is literally named fct_listens, and the two squares behind it read upstream_failed. This is the thing task-per-model actually buys.
dbt platform jobYESthe Orchestrator, one scheduled jobThe Orchestrator kept the run's artifacts, so the run page names the failed step and run_results.json names the node. You did not have to build the persistence.
2 of 4 setups answer this from durable stateEvery difference above is a difference in where the run's state is written down. That is the only variable this chapter has.
Core mental model

Every orchestration decision in this chapter reduces to one question: where does the run's state live? In the dbt process (and its target/ directory), in Airflow's metadata database, or in a vendor's. Nothing else about the four options actually differs.

Why it matters

Chapter 1 said dbt is not an orchestrator; this chapter is the invoice for that sentence. Wavelength ships a nightly production build at 06:00 UTC, and on Monday 2026-07-27 it failed halfway. Twelve nodes committed to the warehouse and stayed committed — one of them still running when fct_listens errored — one errored, two were never attempted, and every downstream human saw a system that looked fine. The cost of a bad orchestration decision is never the failed run — you get told about that. It is the forty minutes at 08:00 spent working out which model broke, whether it is safe to re-run, and who to tell, on a Monday morning when weekly_listening_dashboard is on a screen in an exec meeting.

dbt platform (formerly dbt Cloud)
The hosted product. Renamed 28 May 2025, at dbt Labs' Launch Showcase and confirmed in dbt's 2025 release notes — Chapter 1 owns the rename and this chapter just uses it. The scheduler surface is branded Orchestrator. Tiers: Developer (free), Starter ($100/user/month), Enterprise, Enterprise+.
Still spelled "dbt Cloud" in machine-facing surfaces
The Airflow provider package is apache-airflow-providers-dbt-cloud, the namespace is airflow.providers.dbt.cloud, the API path is /dbt-cloud/api-v2, and the environment variables are DBT_CLOUD_ACCOUNT_ID, DBT_CLOUD_ENVIRONMENT_ID and DBT_CLOUD_SERVICE_TOKEN. Do not "fix" any of those.
astronomer-cosmos
The open-source library that renders a dbt project as Airflow tasks. Version 1.15.0, released 1 July 2026; requires Python 3.10+, declares apache-airflow>=2.9.0, and its CI covers Airflow 3.0 to 3.3. It now runs dbt Core or dbt Fusion projects.
target/run_results.json
Chapter 2's artifact: the per-node status and timing record of one invocation. It is what dbt retry reads, and it is the single file whose survival decides whether resumption is possible. It lives in target/, which lives on a filesystem, which may not outlive the process.
A task is a failure domain
The Airflow KB's idea, not this chapter's. What a task retries, what it skips, and what it isolates are all decided by where you drew its boundary. Everything in Section 2 is that idea applied to a dbt graph.
The four setups this chapter keeps returning to
What runsWhere the run state livesWho operates it
One Airflow taskOne `dbt build`, one dbt processIn target/ on whatever filesystem the task gotYou
Four Airflow tasksFour `--select` invocationsAirflow, at layer resolution; target/ for the restYou
Task per model (Cosmos)15 rendered tasks, 1 or 15 dbt processesAirflow's metadata database, per nodeYou, plus a parse bill
dbt platform OrchestratorOne job, one or more stepsThe vendor's, including target/They do

Read the third column twice. It is the only one that changes anything, and the fourth is the one people actually decide on.

Common mistake

Saying "Airflow orchestrates my models". It does not. dbt schedules models; Airflow schedules invocations. `ref()` is the only thing that builds dbt's build order, and the moment somebody writes `stg_listens >> int_listens_enriched` in a Python file, the project has two dependency graphs that can disagree. Rename a model and dbt follows; the hand-written Airflow edge does not, and now the orchestrator is enforcing an order the project abandoned.

Choosing task-per-model because it will give you more observability. It gives you finer granularity of the boolean, which is a different thing. Fifteen green squares — three of them seeds and a snapshot, not models, and the count changes the day somebody adds a node — mean fifteen nodes built, not fifteen nodes correct. What actually tells you the data is right — tests, source freshness, run_results.json — is identical under all four setups, so you paid a parse bill and a manifest-publication step for a prettier failure map.

Better habit

Before choosing a granularity, write down the recovery question you are actually trying to answer. If you cannot name one, one task is the answer.

Treat "where does target/ live between attempts?" as a design question you answer on day one, not a thing you discover during an incident.

When somebody proposes hand-writing Airflow edges between dbt models, ask what happens on the next rename. That question ends the conversation faster than an argument does.

A task is a failure domain — the Airflow KB owns this

Everything Section 2 does with granularity is that principle applied to a dbt graph: what a task retries is what you drew inside it, and what it can page about is what it can name. Read it there once; this chapter will not re-derive it, and will lean on it heavily.

Idempotency and safe task design — Airflow KB
Provenance of every transcript in this chapter

No terminal output here was captured from a live dbt Core 1.12.0 run. Message strings reproduce documented or source text — dbt's Database Error wrapper line, the retry documentation, the Cosmos operator descriptions — and the timings are this KB's own published Wavelength figures: 39.80s for fct_subscription_revenue, 0.61s for a four-row seed, 0.21s for a staging view, 2.08s for the snapshot. Where a message belongs to the warehouse rather than to dbt, it is left as a placeholder rather than invented. Chapters 8 and 11 shipped the same note for the same reason.

The versions this chapter pins

dbt Core 1.12.0 (16 July 2026), Airflow 3.3.0 (6 July 2026), astronomer-cosmos 1.15.0 (1 July 2026), apache-airflow-providers-dbt-cloud 4.9.2 (22 June 2026, minimum Airflow 2.11.0). One sentence about the far future and then it is Chapter 22's: dbt Core v2.0 is at 2.0.0-alpha.5 as of 20 July 2026, Apache 2.0 licensed.

Remember this

An orchestrator starts a dbt process and stores one boolean; every option in this chapter is a different answer to "where does the run's state live?", and none of them answers "is the data right?".