08 · Intro to Orchestration¶
Every pipeline so far in this course has been "run this script." Real pipelines are usually several scripts with dependencies between them ("clean the orders after extracting them, but only load the warehouse after both orders and customers are cleaned"), running on a schedule, with retries and alerting when something fails. That's what an orchestrator (Airflow is the most common one) manages. This lesson builds the core idea — dependency-ordered execution — from scratch, then shows what it looks like in real Airflow syntax.
What actually ran
The dependency-resolution code below is real, runnable Python (standard library only). The Airflow DAG file later in the lesson is realistic Airflow syntax shown for reference — it is not executed on this page, since running Airflow itself needs a scheduler and metadata database beyond what a lesson page can set up. Airflow hands-on execution starts in Level 2.
The problem: tasks have dependencies, not just an order¶
# task -> list of tasks that depend on it
dag = {
"extract_orders": ["clean_orders"],
"extract_customers": ["clean_customers"],
"clean_orders": ["join_orders_customers"],
"clean_customers": ["join_orders_customers"],
"join_orders_customers": ["load_warehouse"],
"load_warehouse": ["send_success_alert"],
"send_success_alert": [],
}
clean_orders can't start until extract_orders finishes; join_orders_customers
can't start until both cleaning tasks finish. This is a DAG — a
Directed Acyclic Graph — and "directed" + "acyclic" are both load-bearing
words: dependencies point one way, and there must be no cycle, or nothing
could ever legally start.
Computing a valid execution order¶
from collections import deque
def topological_order(dag):
indegree = {node: 0 for node in dag}
for node, deps in dag.items():
for d in deps:
indegree[d] += 1
queue = deque([n for n, deg in indegree.items() if deg == 0])
order = []
while queue:
node = queue.popleft()
order.append(node)
for nxt in dag[node]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
queue.append(nxt)
return order
order = topological_order(dag)
for i, task in enumerate(order, 1):
print(f"{i}. {task}")
1. extract_orders
2. extract_customers
3. clean_orders
4. clean_customers
5. join_orders_customers
6. load_warehouse
7. send_success_alert
This is Kahn's algorithm: a task is runnable once every task it depends on
has completed (indegree drops to zero). Every real orchestrator — Airflow,
Dagster, Prefect — runs some version of exactly this, just with a scheduler,
a UI, retries, and logging wrapped around it. Note extract_orders and
extract_customers both start with indegree 0 and have no dependency on each
other: a real orchestrator would run them in parallel, not in the arbitrary
sequence this list happens to print them in.
What a cycle actually does to a pipeline¶
bad_dag = dict(dag)
bad_dag["send_success_alert"] = ["extract_orders"] # accidental cycle
order2 = topological_order(bad_dag)
scheduled = set(order2)
stuck = [t for t in bad_dag if t not in scheduled]
print(f"Tasks scheduled: {len(order2)} out of {len(bad_dag)}")
print(f"Stuck: {stuck}")
Tasks scheduled: 2 out of 7
Stuck: ['extract_orders', 'clean_orders', 'join_orders_customers', 'load_warehouse', 'send_success_alert']
One accidental edge — imagine a well-intentioned "rerun everything if the
alert fails" link — and five of seven tasks become permanently unrunnable,
because extract_orders now depends (transitively, through the whole chain)
on itself. This is exactly why "acyclic" is a hard requirement, and why
Airflow refuses to load a DAG file containing a cycle rather than trying to
run it partially.
What this looks like in Airflow¶
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime
def extract_orders(): ...
def clean_orders(): ...
def extract_customers(): ...
def clean_customers(): ...
def join_and_load(): ...
with DAG(
dag_id="orders_pipeline",
schedule="@daily",
start_date=datetime(2026, 1, 1),
catchup=False,
) as dag:
t_extract_orders = PythonOperator(task_id="extract_orders", python_callable=extract_orders)
t_clean_orders = PythonOperator(task_id="clean_orders", python_callable=clean_orders)
t_extract_customers = PythonOperator(task_id="extract_customers", python_callable=extract_customers)
t_clean_customers = PythonOperator(task_id="clean_customers", python_callable=clean_customers)
t_join_and_load = PythonOperator(task_id="join_and_load", python_callable=join_and_load)
t_extract_orders >> t_clean_orders
t_extract_customers >> t_clean_customers
[t_clean_orders, t_clean_customers] >> t_join_and_load
The >> operator declares dependencies directly — a >> b means "b runs
after a." [t_clean_orders, t_clean_customers] >> t_join_and_load is exactly
the "both must finish" fan-in from the hand-rolled DAG above. Airflow reads
this file, builds the same kind of dependency graph you built by hand, and
adds the parts that are painful to build yourself: a scheduler that runs
orders_pipeline every day, retry policies per task, a UI showing which task
failed and why, and alerting hooks. Level 2's "Airflow Hands-On" lesson
installs Airflow and runs a DAG like this for real.
Traps¶
- Building implicit dependencies through shared state instead of the DAG.
If
clean_orderswrites a file thatjoin_and_loadreads, but there's no explicit>>edge between them, the orchestrator might run them out of order or in parallel, and you get a race condition invisible in the DAG visualization. - One giant task instead of small dependent ones. A single "do everything" task can't be retried granularly (a transient API failure in extraction forces re-running the load too) and gives no visibility into which step actually failed.
- Forgetting a task can fail halfway through. Orchestration is not just ordering — it's what happens on failure: does a half-written file get cleaned up? Is the failed task's downstream skipped or does it wrongly run on stale data? (This is exactly why lesson 4's idempotency matters: a retried task must be safe to run again.)
- Confusing
schedulewithcatchup. A daily DAG that's been paused for a week will, by default, try to "catch up" by running once for every missed day — sometimes desired (backfilling), often a surprise flood of jobs if you weren't expecting it.
Cheat sheet¶
| Term | Meaning |
|---|---|
| DAG | Directed Acyclic Graph — tasks with one-way dependencies, no cycles |
| Topological order | A valid run order respecting all dependencies |
| Fan-in | Multiple upstream tasks must finish before one downstream task starts |
| Fan-out | One task's completion unblocks multiple downstream tasks |
a >> b (Airflow) |
Declares "b depends on a" |
| Cycle | A dependency loop — makes part or all of the DAG unrunnable |
| Catchup | Whether missed scheduled runs are backfilled automatically |
How It Actually Works¶
A pipeline's tasks form a directed graph where an edge A→B means "B must not start until A finishes." Computing a valid execution order is the classic topological sort: repeatedly find a node with no unprocessed incoming edges, emit it, remove its outgoing edges, and repeat (Kahn's algorithm) — this is literally what a scheduler like Airflow's does when it decides which tasks in a DAG are eligible to run at a given moment. A cycle (A depends on B which depends on A) breaks the algorithm outright: no node in the cycle ever has zero remaining incoming edges, so Kahn's algorithm terminates having emitted fewer nodes than exist in the graph — which is precisely how Airflow detects a cyclic DAG at parse time and refuses to schedule it, rather than deadlocking silently at runtime.
Airflow's scheduler doesn't execute tasks itself; it continuously parses DAG files, computes which task instances have satisfied dependencies for the current run, and hands eligible tasks to an executor (LocalExecutor forks a subprocess, CeleryExecutor/KubernetesExecutor dispatch to workers). Each task instance's state (queued, running, success, failed) is persisted in the metadata database, which is what lets the scheduler resume correctly after a restart instead of re-deriving state from scratch.
Exercise¶
Add a notify_failure task to the hand-rolled dag dict that should run
whenever any of extract_orders, extract_customers, clean_orders, or
clean_customers fails — but should be skipped entirely on a normal
successful run. The plain "depends on everything finishing" model from this
lesson can't express "run only on failure." Write a short paragraph (no need
for code) describing what extra information the scheduler would need to track
per task (hint: task state — success/failed/skipped — not just "has it run
yet") to make that possible. This is exactly the gap Airflow's trigger rules
(trigger_rule="one_failed", etc.) exist to fill.