BeginnerPipeline Architecture · 25 min · 11 challenges

Orchestration: Beginner

At Airbnb, a nightly cron chain turns yesterday's bookings into the host payouts report finance reads at 8am. Tonight the bookings extract runs 50 minutes long. Every downstream job still fires on its clock time and reads a bookings table the extract has not refreshed. The report lands on time and looks complete, and it is wrong. Whether anyone notices depends on one design choice: clock or dependency.

Why Cron Is Not an Orchestrator

Daily Life
Interviews

A schedule is a list of start times

Cron is a program that lives on a server and starts a command at a clock time. A cron entry has 2 parts: when, written as 5 time fields, and what, the command to run. That is the whole contract. Cron does not read the command's output, does not wait for it to finish, and does not know that the entry on the next line depends on the one above.
A nightly orders pipeline built on cron looks like 5 entries in 1 file. Each entry starts an hour after the previous one, because an hour is longer than any of these jobs has ever taken.
Clock timeCron fieldsJobUsual duration
01:000 1 * * *extract_orders40 min
02:000 2 * * *clean_orders20 min
03:000 3 * * *join_orders15 min
04:000 4 * * *aggregate_orders10 min
05:000 5 * * *publish_orders5 min
The hour-long gaps are a guess about durations, and the guess holds until the first night it does not.
Each job starts at its own clock time and trusts that the job before it has finished00:50
01:0002:0003:0004:0005:00
yesterday
yesterday
yesterday
yesterday
yesterday
cron
extract_orders
CRON01:00
cron
clean_orders
CRON02:00
cron
join_orders
CRON03:00
cron
aggregate_orders
CRON04:00
cron
publish_orders
CRON05:00
Looker
revenue_dashboard
yesterday
The night the extract ran long
1/13

Reading the night step by step

Play the cron chain variant. extract_orders starts at 01:00 as always, but a slow source database stretches it to 1h47m, so it finishes at 02:47. At 02:00, clean_orders starts on schedule and reads the extract table, which still holds yesterday's rows because tonight's have not been written yet. It cleans yesterday's data and writes the result with tonight's timestamp. At 03:00 join_orders reads that fresh-looking table, at 04:00 aggregate_orders rolls it up, and at 05:05 publish_orders hands the revenue dashboard yesterday's revenue under today's date.

Every job exited with status 0, and no log line says error. The number on the dashboard is plausible, because yesterday's revenue looks a lot like today's. This is the canonical cron failure: the chain runs to completion on stale input, and the first sign of it is a question from finance 3 days later.

Now play the orchestrator variant. clean_orders has no clock time of its own; its trigger is the success of extract_orders. It waits until 02:47, runs for 20 minutes, and join_orders starts at 03:07. The chain finishes at 03:37, 1h28m earlier than the cron chain managed, and every table it wrote holds today's rows.

Failure modes of a cron chain

The cron chain fails in 3 shapes
  • ▸Late: an upstream job overruns its slot, and every downstream job processes the previous run's output as if it were new.
  • ▸Failed: an upstream job crashes at 01:10 without alerting anyone, and the 4 jobs after it run anyway on whatever the table held.
  • ▸Invisible: when the dashboard is questioned, the evidence is 5 log files on 1 server with no record of which run fed which.

What an orchestrator adds

An orchestrator keeps the clock time and adds a graph. Its jobs are now called tasks, and it stores the edges between them under 1 rule: a task starts when the tasks it depends on have all succeeded. It retries a task that fails, and it keeps a record of every run, so the question of what fed the 2026-10-22 dashboard has an answer. Cron supplies the clock and none of the rest.
✓Cron knows
  • The current time
  • The command on this line
  • Nothing about the line above or below
✗An orchestrator knows
  • The current time and the schedule
  • Which tasks this task depends on
  • Whether those tasks succeeded, and when
  • How many times to retry a failure
  • The history of every run

Cron is still the right tool for 1 job with no dependents, such as rotating a log file. The trouble starts at the second line that reads what the first line wrote.

TIP
A quick test for whether a set of cron entries is secretly a pipeline: if deleting the first entry would make the second one produce garbage, they depend on each other, and the clock is pretending to be an edge.
Multiple Choice

On the cron chain above, extract_orders crashes at 01:10 with a database connection error and writes nothing. What does clean_orders do at 02:00?

The DAG: Tasks, Edges, No Cycles

Daily Life
Interviews
The graph an orchestrator holds has a name: a directed acyclic graph, DAG for short. Each of the 3 words rules out one thing, and the things ruled out are what make the graph runnable.
WordMeaningWhat it rules out
DirectedEvery edge is an arrow from an upstream task to a downstream taskAn edge that says 2 tasks are related without saying which runs first
AcyclicNo path of arrows returns to the node it started fromA task that waits, through any chain of edges, for itself
GraphNodes joined by edges, any number of edges per nodeA plain list, which can only say one after another

Nodes are tasks, edges are must-finish-before

A node is a task: a named unit of work with 1 command, such as clean_orders running python clean.py. An edge is a promise about order. The edge from extract_orders to clean_orders says that clean_orders may not start until extract_orders has succeeded. Succeeded means the command exited cleanly; a task that exits with an error does not release the edges leaving it, and a task that is still running has not released them either.

orchestrator
extract orders
RETRY2BACKOFF5 min fixed
orchestrator
extract customers
RETRY2BACKOFF5 min fixed
orchestrator
clean orders
orchestrator
join orders
orchestrator
aggregate orders
orchestrator
publish orders
BI tool
revenue dashboard

The daily_orders DAG: 6 tasks, 5 edges between them, and the dashboard that reads the last one.

The diagram adds a second root, extract_customers, which depends on nothing. Tasks with no upstream edges become ready at the same moment, so the orchestrator runs the 2 roots side by side when it has 2 workers free. join_orders has 2 upstream edges, so it is released only after both extracts have succeeded; a customer extract that finishes in 4 minutes does not let the join start early.

Reading a DAG declaration

Orchestrators build the graph from a declaration. The one below uses the shape they share: a name and a schedule, then a list of tasks in which each task names the ones it depends on. No arrow is written anywhere; every edge is implied by a depends_on entry.

dag: daily_orders
schedule: "0 1 * * *"
tasks:
- name: extract_orders
depends_on: []
- name: extract_customers
depends_on: []
- name: clean_orders
depends_on: [extract_orders]
- name: join_orders
depends_on: [clean_orders, extract_customers]
- name: aggregate_orders
depends_on: [join_orders]
- name: publish_orders
depends_on: [aggregate_orders]

Count nodes and edges before running anything. There are 6 tasks. Each name inside a depends_on list is 1 edge pointing into that task. The 2 extracts have 0 and join_orders has 2; clean_orders has 1, as do aggregate_orders and publish_orders. That makes 5 edges. The 2 tasks with empty lists are the roots, and from the lists alone the orchestrator works out the waves in which tasks become ready.

WaveTasks that become readyWaiting on
1extract_orders, extract_customersnothing
2clean_ordersextract_orders
3join_ordersclean_orders and extract_customers
4aggregate_ordersjoin_orders
5publish_ordersaggregate_orders

A wave is a convenience for reading, and the orchestrator does not wait for a whole wave. clean_orders starts the moment extract_orders succeeds, even while extract_customers is still running, because clean_orders has 1 upstream edge and that edge is satisfied.

Why no cycles

Suppose publish_orders writes a summary table and someone adds publish_orders to the depends_on list of aggregate_orders to read it. Now aggregate_orders waits for publish_orders, which waits for aggregate_orders. Neither can become ready, so the rule that starts a task once its upstream tasks have succeeded finds nothing to start. An orchestrator that reads a declared graph, like the file above, checks for cycles when it parses the declaration and refuses the file before the first night, since a graph with a cycle has no order to run in.

TIP
If a task needs the output of a task downstream of it, that need is about the previous run, not this one. Point it at yesterday's output, or split the work into a task of its own, rather than drawing an arrow backward.
Multiple Choice

An engineer wants tonight's extract_orders to wait until tonight's publish_orders has finished, so the dashboard is never mid-update while the next extract runs. They add publish_orders to the depends_on list of extract_orders. What happens?

What an Orchestrator Does

Daily Life
Interviews
Strip an orchestrator down to its duties and 4 remain. Each answers a question cron left open, and each concerns control of the work; other systems handle the data itself.
ResponsibilityQuestion it answersIn the orders pipeline
SchedulingWhen does a run begin?A run starts at 01:00 every day
Dependency resolutionWhich task may start now?join_orders starts when clean_orders and extract_customers have both succeeded
Failure handlingWhat happens when a task exits with an error?Retry twice, 5 minutes apart; then mark it failed and hold everything downstream
State and visibilityWhat ran, when, for how long, with what result?The 2026-10-22 run page shows extract_orders took 1h47m and the rest waited

Scheduling and dependency resolution

Scheduling creates a run: at 01:00 the orchestrator records that the daily_orders run for 2026-10-22 exists and that none of its tasks have started. Dependency resolution then runs in a loop for the life of the run. Every few seconds the orchestrator asks, for each task not yet started, whether all of its upstream tasks have succeeded, and starts the ones where the answer is yes. The 2 roots pass on the first check; clean_orders passes the moment extract_orders reports success, whether that is 01:40 or 02:47.

Failure handling

A task fails when its command exits with a non-zero code or runs past its timeout. The orchestrator then applies the task's retry policy: wait the retry delay, run the same command again, up to the retry count. During the retries every downstream task keeps waiting, which cron could never do. When the last retry fails, the task is marked failed and every task downstream of it is marked upstream-failed without running. An alert goes out only if the DAG declares a target for it, such as an email address or a chat channel.
A retry starts the command from the beginning, because the orchestrator has no idea what the first attempt wrote before it died. A task that appended 10,000 rows and then crashed will append them again on the retry. Writing tasks so that a second run produces the same result as a first run is its own subject, covered in a later lesson; for now, know that the orchestrator provides the retry and the task has to be written so that a rerun does no harm.

State and visibility

Every task attempt gets a row in the orchestrator's database. The row carries the task name with its run date and attempt number, when the attempt started and ended, its final state, and a pointer to its log. The web interface colours each node of the DAG by state, and the run history shows 30 nights of durations. With that record, a question about a bad night has an answer: open the 2026-10-15 run, see extract_orders red with 3 attempts, and read the connection error in the first attempt's log.

What it hands to other systems

The orchestrator issues commands and reads exit codes; the work inside each command happens elsewhere. extract_orders runs a query against the operational database and lands files in the lake. join_orders sends a SQL statement to the warehouse and waits while the warehouse engine runs the join and stores the result. Keep that boundary sharp. A task that pulls 200GB through a worker holds the slot for hours while ready tasks queue behind it, and the worker can run out of memory or disk.

WorkOwner
Deciding that a run starts at 01:00Orchestrator
Deciding that join_orders may startOrchestrator
Running the SELECT that joins orders to customersWarehouse engine
Holding the joined table between runsWarehouse storage
Moving bytes from the operational database into the lakeThe extract program
Running the extract a second time after a timeoutOrchestrator
Recording that attempt 2 succeeded at 01:57Orchestrator
Owns versus delegates
  • ▸Owns: when a run begins, in what order tasks start, what to do on failure, and the record of what happened.
  • ▸Delegates: all work on the data itself, from the first read to the stored result.
Multiple Choice

Which of these is the orchestrator's own job, rather than work it delegates to another system?

The Major Orchestrators by Name

Daily Life
Interviews
Most open source orchestration runs on 3 tools, and each gives a different answer to the question of what a pipeline author should write down first. All 3 run the model from the previous section underneath.

Airflow: tasks and operators

Airflow came out of Airbnb's data team, where a nightly set of cron jobs had grown past what anyone could reason about, and it has been an Apache project since. Its unit is the task, declared in Python inside a DAG object, and its main draw is a library of operators: prebuilt task types, such as one that runs a SQL statement and one that waits for a file to appear, with others that call APIs. Airflow emphasizes breadth. If a system exists, there is probably an operator for it. Most engineers have also already seen its scheduler and web interface.
2015
Airflow open sourced
2019
Airflow top-level Apache project

Dagster: assets first

Dagster starts from the tables a pipeline produces rather than the steps that produce them. You declare an asset, such as clean_orders, as a function that returns the data, and name the assets it reads in its arguments; the edges come from those arguments. The emphasis is on knowing which code and inputs produced each table, and when. Dagster also puts weight on running a pipeline on a laptop with typed inputs and outputs before it runs on a schedule, so a broken declaration fails at development time instead of at 01:00.

Prefect: plain Python functions

Prefect treats a workflow as a Python function, called a flow, that calls other functions, called tasks. The graph is whatever the function does when it runs, so a flow can loop over a list of 40 regions and create 40 tasks at run time without declaring each in advance. The emphasis is on writing the pipeline as ordinary code and letting the orchestrator wrap it with retries and state records as it runs.

The shared model

Side by side, the 3 use different words for the same nouns. Each has a container for a graph, a node that does work, a schedule that creates runs, a retry policy per node, and a run history behind a web interface. Learn the model once and the first week on any of the 3 is vocabulary.
ConceptAirflow calls itDagster calls itPrefect calls it
Graph containerDAGjob, or the asset graphflow
Unit of worktaskop, or assettask
One executionDAG runrunflow run
When runs beginschedule on the DAGschedule on the jobschedule on the deployment
Under the declarations the 3 ship the same moving parts. A scheduler process creates runs and resolves dependencies, a pool of workers executes commands, and a metadata database holds every state change; the web interface is a window onto that database. On any of the 3, if the metadata database is down, no run starts.
What differs is the thing you write first. Airflow asks for the task and its order, Dagster asks for the table and its inputs, Prefect asks for the function. A pipeline designed as a graph of tasks with a schedule and retry counts moves between them with its design intact; what changes is the file that declares it.
If your company already runs one of them, use that one; a second orchestrator doubles the number of places a run can hide. For a team starting fresh, Airflow's operator library covers the most systems out of the box, Dagster's asset view answers the question of what made a table without extra work, and Prefect keeps the declaration closest to the Python the team already writes.

Cloud providers sell managed versions of all 3. The model is unchanged; what changes is who runs the scheduler and the metadata database.

Multiple Choice

Your team moves the daily_orders pipeline from one of the 3 orchestrators to another. Which part of the design can you keep without a redesign?

First DAG: 3 Tasks, 1 Schedule

Daily Life
Interviews
The pipeline is the smallest version of the orders chain: extract, clean, publish, started at 01:00 with 1 retry policy for every task. The code below is illustrative. It has a name and a schedule, the retry policy, and 3 tasks joined by 2 edges, which is the shared shape; each orchestrator spells those same parts in its own syntax.
from orchestrator import DAG, task
with DAG(
dag_id="daily_orders",
schedule="0 1 * * *",
retries=2,
retry_delay_minutes=5,
) as dag:
extract = task("extract_orders", command="python extract.py")
clean = task("clean_orders", command="python clean.py")
publish = task("publish_orders", command="python publish.py")
extract >> clean >> publish

Reading it line by line

What each line declares
  • schedule="0 1 * * *": the 5 cron fields. Minute 0 of hour 1, on every day of every month, whatever the weekday. One run per night at 01:00.
  • retries=2: each task gets 3 attempts in total, the first run plus 2 retries, and the policy on the DAG applies to every task in it.
  • retry_delay_minutes=5: the orchestrator waits 5 minutes between a failed attempt and the next one.
  • task(...): a named node with 1 command. The name is what the run page and the alert will show, so it names the output.
  • extract >> clean >> publish: 2 edges. Read >> as 'must succeed before': the task on the left must succeed before the task on the right may start.
Cron fieldValueMeaning
minute0at minute 0
hour1of hour 1, so 01:00
day of month*every day
month*every month
day of week*every day of the week

The order of execution

The orchestrator reads the edges, and the order the tasks were written in plays no part. extract has no upstream edge, so it is the only task ready at 01:00. clean has 1 upstream edge from extract, so it becomes ready when extract succeeds. publish has 1 upstream edge from clean. With 3 tasks and 2 edges there is 1 possible order, and the orchestrator finds it by asking on each loop which task has nothing unfinished above it.

TimeEvent on a normal night
01:00Run created; extract_orders attempt 1 starts
01:40extract_orders succeeds; clean_orders starts
02:00clean_orders succeeds; publish_orders starts
02:05publish_orders succeeds; run marked success
Multiple Choice

You add a fourth task, extract_customers. publish_orders reads its output, but clean_orders does not. Which single change starts every task as soon as its inputs exist?

A night with a failure

Now the source database drops the connection 12 minutes in. The orchestrator marks attempt 1 failed, waits the 5 minute delay, and starts attempt 2 at 01:17. The whole extract runs again from the first row, succeeds at 01:57, and clean becomes ready. The dashboard is 17 minutes later than usual and correct. Nobody was paged, and the run page shows 2 attempts on extract_orders with the error sitting in the first attempt's log.

TimeEvent on the failure night
01:00extract_orders attempt 1 starts
01:12attempt 1 fails: connection reset
01:17attempt 2 starts after the 5 minute delay
01:57attempt 2 succeeds; clean_orders starts
02:17clean_orders succeeds; publish_orders starts
02:22publish_orders succeeds; run marked success

If attempts 2 and 3 fail as well, the sequence ends differently. After the third failure the orchestrator marks extract_orders failed and sets clean_orders and publish_orders to upstream-failed without starting either. It then fails the run and sends the alert. The published table keeps yesterday's rows, which is the right outcome: a stale table that everyone knows is stale beats a fresh-looking table built from the wrong night.

✓Do
  • Name tasks after what they produce, so the run page reads as the pipeline
  • Give every task a retry count, even 1, so a flaky connection does not page anyone
  • Let the edges decide order; never encode order in start times
  • Read the run page after the first night and note each task's duration
✗Don't
  • Chain tasks by giving each its own clock time inside one DAG
  • Set so many retries that a broken source gets queried again and again for hours
  • Move data through the orchestrator's own workers
  • Put 2 unrelated pipelines in 1 DAG because they share a schedule

Capstone

Multiple Choice

extract_orders succeeds at 01:40. clean_orders then fails on all 3 attempts because a column it expects is missing. At 02:30, what is the state of publish_orders, and what does the dashboard show?

PUTTING IT ALL TOGETHER

> Return to the Airbnb host payouts chain from the intro, rebuilt as a DAG with the same 01:00 schedule. The bookings extract runs 50 minutes long, and the payouts task waits on its edge instead of its clock, so it reads a complete table. A connection dropped 12 minutes in costs 1 retry and 17 minutes, because the extract reruns from the first row after the 5 minute delay. If every attempt fails, finance opens a run page that is red at the extract and reads the error, instead of a report that arrived on time and wrong.

The cron chain from Section 0 and the rebuilt DAG differ in 1 thing: whether the payouts task is triggered by a clock time or by the extract's success.
The edges from Section 1 are what let the payouts task wait 50 extra minutes; the orchestrator's dependency loop from Section 2 is what releases it the moment the extract reports success.
The retry policy from Section 4 turns a connection dropped 12 minutes in into a 17 minute delay; the failure handling from Section 2 turns 3 dropped connections into a held downstream and an alert.
Whichever orchestrator from Section 3 runs the payouts DAG, the data work happens in the extract program and the warehouse. The orchestrator records which task ran and when.
KEY TAKEAWAYS
Cron starts a command at a clock time and knows nothing about the entry above or below it; a gap between start times is a guess about duration.
A DAG is tasks joined by must-succeed-before edges with no path leading back to its start, and the orchestrator derives the running order from the edges alone.
An orchestrator owns 4 things: when a run begins, which task may start, what to do when a task fails, and the record of what happened.
Other systems do the data work: rows are read from the source, and the warehouse or the lake computes and holds the result. The orchestrator sends commands and reads exit codes.
Airflow and Dagster share a model with Prefect: a graph on a schedule, with retries and a run history. They differ in what you write first, which is a task in Airflow and an asset in Dagster, while Prefect starts from a function.
A task that fails its last retry holds every downstream task and alerts the target the DAG declares; a stale table everyone knows about beats a fresh-looking wrong one.

Cron starts jobs on a clock; an orchestrator starts them when the jobs they depend on have succeeded.

Category
Pipeline Architecture
Difficulty
beginner
Duration
25 minutes
Challenges
11 hands-on challenges

Topics covered: Why Cron Is Not an Orchestrator, The DAG: Tasks, Edges, No Cycles, What an Orchestrator Does, The Major Orchestrators by Name, First DAG: 3 Tasks, 1 Schedule

Lesson Sections

  1. Why Cron Is Not an Orchestrator (concepts: DAG Orchestration)

    A schedule is a list of start times Cron is a program that lives on a server and starts a command at a clock time. A cron entry has 2 parts: when, written as 5 time fields, and what, the command to run. That is the whole contract. Cron does not read the command's output, does not wait for it to finish, and does not know that the entry on the next line depends on the one above. A nightly orders pipeline built on cron looks like 5 entries in 1 file. Each entry starts an hour after the previous one

  2. The DAG: Tasks, Edges, No Cycles (concepts: DAG Orchestration)

    The graph an orchestrator holds has a name: a directed acyclic graph, DAG for short. Each of the 3 words rules out one thing, and the things ruled out are what make the graph runnable. Nodes are tasks, edges are must-finish-before Reading a DAG declaration Why no cycles

  3. What an Orchestrator Does (concepts: DAG Orchestration)

    Strip an orchestrator down to its duties and 4 remain. Each answers a question cron left open, and each concerns control of the work; other systems handle the data itself. Scheduling and dependency resolution Failure handling A task fails when its command exits with a non-zero code or runs past its timeout. The orchestrator then applies the task's retry policy: wait the retry delay, run the same command again, up to the retry count. During the retries every downstream task keeps waiting, which c

  4. The Major Orchestrators by Name (concepts: DAG Orchestration)

    Most open source orchestration runs on 3 tools, and each gives a different answer to the question of what a pipeline author should write down first. All 3 run the model from the previous section underneath. Airflow: tasks and operators Airflow came out of Airbnb's data team, where a nightly set of cron jobs had grown past what anyone could reason about, and it has been an Apache project since. Its unit is the task, declared in Python inside a DAG object, and its main draw is a library of operato

  5. First DAG: 3 Tasks, 1 Schedule (concepts: DAG Orchestration)

    The pipeline is the smallest version of the orders chain: extract, clean, publish, started at 01:00 with 1 retry policy for every task. The code below is illustrative. It has a name and a schedule, the retry policy, and 3 tasks joined by 2 edges, which is the shared shape; each orchestrator spells those same parts in its own syntax. Reading it line by line The order of execution A night with a failure Capstone