Data · Guide

Orchestration & Transformation

Airflow and the DAG, dbt and transformation as version-controlled SQL, and what to do when a task fails at 3am.

— min read Data

Something Has To Decide What Runs

A pipeline is rarely one job. It is thirty, with dependencies, and the orchestrator is what knows that the revenue mart cannot run before the orders load finished — and what to do when it did not.

Cron gets you scheduling and nothing else: no dependencies, no retries, no history, no visibility into why last Tuesday produced nothing. An orchestrator adds all four, and it is the piece that turns a set of scripts into a system somebody can operate.

Airflow & DAGs

Airflow models a workflow as a directed acyclic graph: tasks are nodes, dependencies are edges, and the scheduler runs each task once its upstreams have succeeded for that logical date.

with DAG(
    "daily_revenue",
    schedule="0 3 * * *",
    start_date=datetime(2026, 1, 1),
    catchup=True,                 # backfill missed runs deliberately
    default_args={"retries": 2, "retry_delay": timedelta(minutes=10)},
) as dag:

    extract = PythonOperator(task_id="extract_orders", python_callable=extract_orders)
    load    = PythonOperator(task_id="load_raw",       python_callable=load_raw)
    mart    = BashOperator(task_id="build_mart", bash_command="dbt run --select revenue+")

    extract >> load >> mart      # the dependency, stated once
ConceptMeaning
Logical dateThe window a run is for — not when it executed
CatchupWhether missed intervals are run on deploy
RetriesAutomatic, with backoff, before anyone is woken
SensorA task that waits for a file, partition or upstream
SLA / timeoutAlert when a task is late, not only when it fails
The logical date is the parameter every task should use. Reading the current date instead is the same bug as calling now() in a transform: the DAG works today and produces nonsense the moment anyone backfills.

dbt & Transformation

dbt takes the transformation half and makes it ordinary software: models are SELECT statements in version control, dependencies are inferred from references, and the whole graph is testable and documented.

-- models/marts/revenue_daily.sql
{{ config(materialized='incremental', unique_key='date_day') }}

SELECT
    date_trunc('day', o.settled_at) AS date_day,
    o.store_id,
    SUM(o.net_amount)               AS revenue
FROM {{ ref('stg_orders') }} o           -- ref() builds the dependency graph
{% if is_incremental() %}
WHERE o.settled_at >= (SELECT MAX(date_day) FROM {{ this }})
{% endif %}
GROUP BY 1, 2
What it bringsInstead of
SQL in git, reviewed in pull requestsQueries pasted into a scheduler
A dependency graph from ref()A hand-maintained run order
Tests as configurationHoping the numbers are right
Generated documentation and lineageA stale wiki page
Incremental modelsRebuilding a billion rows nightly

The division of labour that has settled: the orchestrator moves and schedules, and dbt transforms. Airflow triggers dbt run; it does not reimplement what dbt already does.

When It Fails At 3am

Pipelines fail on a schedule you did not choose, so the design question is what the failure does to everything downstream.

PracticeWhy
Retry transient errors automaticallyMost failures are a timeout, not a bug
Fail fast on bad dataBetter a missing table than a wrong number in a board pack
Alert on lateness, not only errorsA job that never started raises nothing
Make every task idempotentSo "clear and rerun" is a safe first response
Publish freshnessConsumers should see how old the data is
Silent success is the worst outcome: a job that finishes green having written zero rows. Assert on row counts and freshness as part of the run, so empty output is a failure rather than a surprise found next week.

Interview Questions

Why not just use cron?

Cron schedules and nothing more. An orchestrator adds dependencies, retries with backoff, run history, backfills and visibility into why a run produced nothing.

What is the logical date in Airflow?

The data window a run is for, as opposed to when it executed. Every task should use it, otherwise backfills recompute old windows with today's date.

What does dbt actually give you?

Transformation as version-controlled SQL: models in git, a dependency graph inferred from ref(), tests as configuration, generated lineage docs, and incremental models.

How do orchestration and dbt divide the work?

The orchestrator schedules and moves data and triggers dbt; dbt owns the transformation graph inside the warehouse. Neither reimplements the other.

Why alert on lateness as well as failure?

A job that never started emits no error. An SLA or freshness alert catches the silent case, which is the one that reaches a dashboard unnoticed.

What is a silent success?

A run that completes green having written nothing or far too little. Asserting row counts and freshness inside the run turns it into an ordinary failure.

Quick Quiz

1. An Airflow DAG models…
2. Tasks should use the logical date because…
3. dbt infers its dependency graph from…
4. A run that finishes green with zero rows is…
5. Automatic retries are appropriate for…