Orchestration: Intermediate
| Schedule | Written as | Starts runs at | A run starts because |
|---|---|---|---|
| Cron | `0 6 * * *` | 06:00 every day | the clock reached a named minute |
| Cron | `*/15 * * * *` | :00, :15, :30 and :45 | the clock reached a named minute |
| Interval | every 15 minutes from 10:07 | 10:07, 10:22, 10:37 | a fixed gap passed since the anchor |
| Event | on file `rates_2026-10-23.csv` | whenever the file lands | a signal arrived |
Cron: when the clock is the contract
A cron expression has 5 fields: minute, hour, day of month, month and day of week, so 0 6 * * 1-5 means 06:00, Monday to Friday. Cron fits when the data is complete by a known time and the consumer reads at a known time. A finance report opened at 09:00, built from a ledger that closes at midnight, belongs on cron at 02:00, leaving hours of slack for a slow night.
Interval: an even rhythm, anchored to a start
An interval schedule says "every 15 minutes" and counts from an anchor, usually the moment the DAG was switched on. Anchored at 10:07, it starts runs at 10:07, 10:22 and 10:37, where */15 would start them at 10:15, 10:30 and 10:45. Both give equal windows. Pick interval when the gap matters and the clock positions do not, or when cron cannot express the gap: every 90 minutes takes 2 cron lines, because the minute alternates between :00 and :30.
A dashboard must show orders no older than 20 minutes, and each load takes 6 minutes. What is the longest gap that meets the limit?
Event: when arrival time is unpredictable
| Data arrives | Consumer expects data | Choose |
|---|---|---|
| complete by a known time | ready at a fixed reading time | cron, after the usual arrival plus slack |
| continuously, all day | no older than a stated age | interval or cron, with gap plus run time within that age |
| at unpredictable times | soon after it lands | event trigger, plus an alert for silence |
| at unpredictable times | ready at a fixed reading time | cron after the latest arrival, or event plus deadline alert |
A bank sends a settlement file. Over the last 30 days it landed between 01:10 and 04:50 UTC. Treasury wants each file's totals within 30 minutes of landing. Which schedule fits?
When something breaks at 04:00, people use 5 words loosely, and the right fix depends on which one you mean. Take a DAG named daily_revenue with 3 tasks in a line: extract_bookings, then convert_currency, then publish_report.
| Term | What it is | In daily_revenue | How many |
|---|---|---|---|
| DAG | the definition: tasks, edges, schedule | `daily_revenue` as written in code | 1 |
| Task | a single node in the definition | `convert_currency` | 3 |
| DAG run | a single execution of the DAG for a single data interval | the run for 2026-10-23 | 1 per interval |
| Task instance | a task inside a specific run | `convert_currency` in the 2026-10-23 run | 3 per run |
| Attempt | a single try at a task instance | try 2 of that task instance | the retry count plus 1 |
Success and failure live on task_instance rows, so every fix below acts on runs and task instances.
Automatic retries act on a single task instance
Say convert_currency fails in the 2026-10-23 run with a retry count of 2. The orchestrator leaves the rest of the run alone: it moves that task instance to up for retry, waits the configured delay, and starts attempt 2. extract_bookings keeps its success and doesn't execute again, and publish_report keeps waiting. A retry count of 2 allows 3 attempts. Only when attempt 3 fails does the task instance become failed, and only then does the run fail.
An hourly DAG has 4 tasks, each with a retry count of 1. After 7 days, how many task instances exist, and how many attempts did a task instance that ended failed use?
Choosing the scope of a manual rerun
| Action | Re-executes | Use it when |
|---|---|---|
| Clear a single task instance | that task instance only | it failed and nothing downstream has run yet |
| Clear with downstream | that task instance and every later one in the same run | its output changed after later tasks read it |
| Clear the whole run | every task instance in the run | the run's inputs were wrong from the first task |
| Trigger a new run | a fresh run with a new run id | you want an extra execution that leaves existing runs untouched |
The common mistake is clearing too little. Suppose extract_bookings for 2026-10-23 succeeded while its source was still filling in. You wait for the source to finish and clear only extract_bookings. It reruns and succeeds, while convert_currency and publish_report keep their earlier success, so the report still shows partial numbers. Once a task's output changes, every task that read it must run again in the same run, which is what clearing with downstream does.
In the 2026-10-23 run of daily_revenue, all 3 tasks succeeded. Then you learn the rates table that convert_currency read was missing 2 currencies, and the table is now fixed. What should you do?
Once 2 teams own 2 DAGs, one DAG's output becomes another's input, and the link between them sits outside both graphs. fx_rates_daily belongs to the finance data team and loads each day's exchange rates into warehouse.fx_rates. daily_revenue belongs to analytics and reads that table. The orchestrator links them only if you build the link, with a time offset, a sensor or an asset trigger.
Exchange rates reach daily_revenue through the shared table, and neither DAG's graph contains the other.
Time offset: a dependency nobody wrote down
Schedule fx_rates_daily at 01:00 and daily_revenue at 03:00, because the rates load has finished by 02:40 on every night anyone checked. Neither DAG records the dependency. It costs nothing to build and fails silently: on a night when the load takes 2 hours 25 minutes, daily_revenue reads the previous day's rates at 03:00, succeeds, and publishes. Offsets also decay, since the upstream grows slower month by month and nobody downstream hears when it crosses 03:00.
Sensor: wait for the upstream task
A sensor is a task whose only job is to check a condition on a timer; it succeeds once the condition holds. Put one first in daily_revenue, waiting for load_rates in fx_rates_daily to succeed for the matching data interval. A slow night now delays revenue instead of corrupting it, and a missed night fails the sensor at its timeout. The cost is coupling. The sensor names another team's DAG id and task id, and it finds the matching upstream run from a fixed time difference between the 2 schedules. If finance renames load_rates, or moves the DAG from 01:00 to 01:30, the sensor waits for a task instance that will never exist.
Asset trigger: depend on the table itself
An asset trigger moves the contract from the DAG to the data. The load_rates task declares that it updates the asset warehouse.fx_rates, and when that task instance succeeds, the orchestrator records an update event. daily_revenue names the asset as its schedule, so each update event creates a run. Downstream now knows a table name and nothing about who writes it or when: finance can rename tasks, split the DAG or move it to 01:30 without telling analytics. Revenue also starts seconds after the rates land, on slow nights and fast ones.
| Approach | Downstream must know | Upstream runs slow | Upstream changes schedule |
|---|---|---|---|
| Time offset | the usual finish time | runs on stale data and succeeds | breaks with no error |
| Sensor on the upstream task | DAG id, task id, schedule difference | waits, then runs | waits until timeout, then fails |
| Asset trigger | the asset name | runs when the update lands | keeps working |
Which failure can a time offset produce that a sensor and an asset trigger both prevent?
orders_load runs hourly in your orchestrator and writes warehouse.orders. Its owners reorganize its tasks every quarter. Your DAG must start within minutes of each load. Which dependency fits?
Where a sensor belongs
A sensor belongs at the boundary: the first task of the DAG that consumes the outside data, waiting on the exact object that run needs. For the 2026-10-23 run, that is landing/rates/2026-10-23.csv, a path built from the run's data interval. A sensor waiting for any file in landing/rates/ succeeds on yesterday's file, and a sensor in the middle of a DAG lets earlier tasks run before the input exists, then holds the run open for hours.
Timeouts decide what silence means
Sensor execution modes and worker slots
| Mode | Slot held while waiting | Each check is | Fits |
|---|---|---|---|
| Blocking (poke) | yes, for the whole wait | a loop inside a single attempt | waits of a few minutes with frequent checks |
| Reschedule | only during each check | a short attempt started on a timer | waits of hours, checked every few minutes |
| Deferred | none; an async process holds the wait | a callback from that process | hundreds of long waits at the same time |
External triggers: let the source call in
An external trigger reverses the direction, and it lets finance drop the 01:00 cron from fx_rates_daily. The storage bucket emits an event when landing/rates/2026-10-23.csv is written, and a small function receives it and calls the orchestrator's API to start fx_rates_daily, passing the file path in the run's configuration. Nothing polls, and the run starts seconds after the write. Events can also be lost or duplicated. A lost event means no run and no error, so back every external trigger with a deadline check that alerts if no run has started by the consumer's cutoff. A duplicated event means 2 runs for 1 file, and using the file name as the run id makes the orchestrator reject the second as a run that already exists.
The storage event starts the run, and the DAG then reads the file from the bucket. The 06:20 deadline alert covers the night the event never arrives.
fx_rates_daily starts from an external trigger and has no deadline check. One night the storage event is lost. What does the team see?
- Wait on the exact file or partition named for this run's interval
- Derive the timeout from the consumer's deadline
- Back every external trigger with a deadline alert
- Sense another DAG where an asset trigger fits
- Hold a worker slot through a wait of hours
- Poll a partner's server every few seconds
A vendor posts a report between 23:00 and 03:00. The consumer reads at 08:00, and the downstream load takes 1 hour. You run 200 such sensors each night on a 64-slot cluster. Which sensor setup fits?
| Upstream DAG | Writes | Schedule | Loaded through, at finish |
|---|---|---|---|
| `orders_15min` | `warehouse.orders` | interval, every 15 minutes from :01 | the quarter hour that ended |
| `payments_hourly` | `warehouse.payments` | cron `5 * * * *` | the top of the current hour |
| `fx_rates_daily` | `warehouse.fx_rates` | event, when the bank's file lands | midnight ending the day that is starting, whose rates the overnight file carries |
The offset design and where it breaks
A first draft schedules revenue_by_currency at :30 past every hour, reasoning that orders finish by :06, payments by :15 and rates by 06:20. That stacks 3 time offsets, each failing silently: a payments file 40 minutes late means the 14:30 run builds 13:00 to 14:00 without payments and publishes it as final. Sensors fit badly too: the event-triggered rates DAG has no schedule from which to compute the run to wait for, and mapping each hourly run to 4 orders runs works but hard-codes both teams' cadences.
Join on freshness instead
In the design that holds up, every upstream DAG ends with a task that records how far its table is loaded, as a row in pipeline_freshness, and that task declares the table as an asset. Then revenue_by_currency runs whenever any of the 3 assets updates, and its first task asks the freshness table a single question: up to which hour are all 3 inputs complete?
Each upstream DAG updates its table and its freshness row, and any of those updates starts revenue_by_currency, which reads the freshness rows before building anything.
pipeline_freshness at 14:40 UTC on 2026-10-24
| table_name | loaded_through | updated_at |
|---|---|---|
| warehouse.orders | 2026-10-24 14:30 | 2026-10-24 14:36 |
| warehouse.payments | 2026-10-24 14:00 | 2026-10-24 14:12 |
| warehouse.fx_rates | 2026-10-25 00:00 | 2026-10-24 03:25 |
The run then builds every hour that ends by build_through and has not been built yet, and it writes build_through beside the output so the dashboard can label its numbers final through 14:00. If inputs_reporting is below 3, an upstream has never reported, and the run fails loudly. The slowest input sets the line, whichever input that happens to be.
At 09:50 UTC on 2026-10-24, pipeline_freshness shows orders loaded through 2026-10-24 09:45, payments through 2026-10-24 09:00 and fx_rates through 2026-10-24 00:00. What does the freshness query return?
| Time (UTC) | Update that starts the run | build_through | Hours built |
|---|---|---|---|
| 14:06 | orders loaded through 14:00 | 13:00 | none new |
| 14:12 | payments loaded through 14:00 | 14:00 | 13:00 to 14:00 |
| 14:21 | orders loaded through 14:15 | 14:00 | none new |
| 14:36 | orders loaded through 14:30 | 14:00 | none new |
| 15:06 | orders loaded through 15:00 | 14:00 | none new |
| 15:12 | payments loaded through 15:00 | 15:00 | 14:00 to 15:00 |
When a payments file is 2 hours late, orders updates keep starting runs that find build_through stuck at 14:00, so the dashboard keeps saying final through 14:00 rather than publishing the hour ending 15:00 with no payments. The alert pages the owner of slowest_input, with a threshold per input: orders or payments at 90 minutes behind the clock, rates only if still slowest after 06:20, since until the bank's file lands they hold build_through at midnight every night.
A teammate proposes running revenue_by_currency only when all 3 assets have updated since its last run. What happens to the dashboard?
> Picture the finance analytics team at Airbnb rebuilding the revenue job from the opening. Bookings land every 15 minutes, payment settlements land hourly, and the exchange-rate file arrives whenever the rates provider sends it overnight. The team retires the 03:00 offset and has to decide, for each piece, which schedule, which cross-DAG link, which way of waiting on the outside world and which rerun scope applies.
payments_hourly opens with a reschedule-mode sensor on that hour's settlement file, timed out before the next hour, and the rates file enters through an external trigger backed by the 06:20 deadline alert.Pick schedules by how data arrives, retry the right unit, and link DAGs on freshness instead of the clock.
- Category
- Pipeline Architecture
- Difficulty
- intermediate
- Duration
- 30 minutes
- Challenges
- 11 hands-on challenges
Topics covered: Schedules: Cron, Interval, Event, Task vs DAG vs Run for Retries, Cross-DAG Dependencies, Sensors and External Triggers, Three Cadences, One Output
Lesson Sections
- Schedules: Cron, Interval, Event
A schedule tells the orchestrator when to start a new run of a DAG. Nearly every pipeline uses one of 3 kinds: a cron schedule names moments on the wall clock, an interval schedule names a fixed gap between runs, and an event schedule names a signal, such as a file landing, and starts a run each time the signal arrives. Cron: when the clock is the contract Cron trusts the clock and never looks at the data. If the ledger closes late, the 02:00 run reads a half-written day and still finishes green
- Task vs DAG vs Run for Retries
Daily runs over 30 days produce 30 DAG runs and 90 task instances from 1 DAG and 3 tasks. Every state the orchestrator shows (queued, running, success, failed, up for retry) belongs to a task instance or a run. Each run is identified by its data interval, which the schedule cuts: at 03:00, the 2026-10-23 run covers 03:00 on the 23rd to 03:00 on the 24th and starts as it closes. A task needing calendar 23 October filters on the run's date. Automatic retries act on a single task instance Choosing
- Cross-DAG Dependencies
Time offset: a dependency nobody wrote down Sensor: wait for the upstream task Asset trigger: depend on the table itself Asset triggers have 2 limits. An update event says the asset changed and says nothing about which slice of data changed, so a downstream that cares must check the content, which the last section of this lesson builds. And the event exists only if the write happened inside the same orchestrator, or the outside writer reports it through the orchestrator's API.
- Sensors and External Triggers
Asset triggers need the write to happen where the orchestrator can see it. Many inputs are produced elsewhere: a bank drops a file on a server, a vendor's API marks a report ready, a partner writes into your storage bucket. For those, the orchestrator has 2 ways to learn that data exists: it asks, with a sensor, or it is told, with an external trigger. Where a sensor belongs Timeouts decide what silence means A sensor without a timeout waits forever, and a run that waits forever alerts nobody. W
- Three Cadences, One Output
A revenue team wants hourly revenue in USD, and each hour should appear on the dashboard only once all of its inputs are in. Those inputs arrive on 3 cadences, from 3 DAGs owned by 3 teams. The offset design and where it breaks Join on freshness instead pipeline_freshness at 14:40 UTC on 2026-10-24