Airflow Datasets: The Downstream DAG That Ran Too Early
Airflow dataset-aware scheduling explained: producer/consumer DAGs, asset triggers in Airflow 3, and why your downstream DAG ran before the table existed..
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Scheduling fundamentals: cron, start_date, data intervals (see airflow-scheduling).
- ✓DAG authoring and dependencies (see airflow-dags).
- ✓A producer→consumer pipeline you want to make event-driven.
- Dataset (asset) scheduling triggers a consumer DAG when a producer task updates data — not on a wall-clock time.
- Producers declare outlets; consumers declare their trigger as one or more datasets/assets.
- Multiple datasets: any-of vs all-of semantics — one update fires a run unless all are required.
- Airflow 3 renamed datasets to assets with a back-compat
scheduleparameter and stable asset names. - Production rule: event-driven triggers still need idempotency, because producers can update twice in one window.
- URIs are literal strings:
input_*.csvor regex patterns register as separate datasets that never update.
A time-based schedule is like agreeing to meet at 9 AM — even if the train is late, you're standing on the platform. A dataset trigger is like the train texting you when it actually arrives. The downstream DAG no longer guesses; it moves when the data moves. The cost: if the train sends two texts, you'd better be happy meeting twice.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
The classic Airflow dependency failure: the consumer DAG ran at 9 AM on cron, the producer finished at 9:40, and every dashboard read yesterday's table.
The fix isn't a later cron — it's event-driven scheduling. Dataset-aware DAGs (assets in Airflow 3) let the producer's completion itself trigger the consumer.
Here's producer/consumer wiring, the any-vs-all semantics that surprise teams, the Airflow 3 asset migration, and the gotchas that turn event-driven pipelines into incidents.
1. From Time-Based to Event-Driven
A cron schedule answers one question: when? An event-driven schedule answers a better one: when is the data actually ready?
Time-based chains are races. Producer at 8:00, consumer at 9:00 — that's a 60-minute buffer standing between you and a stale dashboard. The buffer shrinks when the producer gets slower, and nobody notices until the race is lost.
Event-driven scheduling removes the race. The producer's completion is the trigger. The consumer starts when the data lands, not when the clock strikes.
- Time chains: producer 8:00, consumer 9:00 — 60 min of assumed margin
- Event chains: consumer fires on producer completion — zero margin
- The buffer is guesswork that rots as pipelines slow down
- Events turn 'hopefully ready by' into 'actually ready now'
2. Producer/Consumer with Dataset Outlets
The wiring has two sides. The producer's final task declares an outlet — a dataset representing the thing it produces, usually a table. The consumer declares that dataset as its schedule.
When the producer task completes successfully, Airflow marks the dataset updated and the consumer's run is queued. This is visible in the grid view: the consumer's run shows the dataset update that caused it.
Use one dataset per logical artifact. A table that three tasks touch should be represented by one dataset, declared as the outlet of the task that finalizes it — usually the last one.
Two identity facts matter. The URI is a literal string: patterns like input_2022*.csv or input_\d+.csv don't match anything — Airflow registers each as a distinct dataset that no task ever updates. Use defined schemes (file, s3, postgres) whose semantics Airflow knows. And a dataset updates only when its task completes successfully: failed or skipped tasks emit nothing, so consumers stay quiet.
3. Any-of vs All-of Semantics
A consumer can depend on multiple datasets, and the semantics trip up most teams at least once.
By default, any dataset update triggers the consumer. If the producer updates dataset A and dataset B in the same window, the consumer runs twice.
To require every dataset — fire only when all of them have been updated since the last run — the consumer's schedule uses the all-of form, with an wrapper or a multi-item schedule where all conditions must clear.all()
The distinction matters in production: all-of consumers want "everything's ready", any-of consumers are fan-in reducers that handle one event at a time.
The syntax is explicit: a plain list like schedule=[a, b] means all-of, and a dataset updated multiple times before the others land still fires the consumer only once. Airflow also supports conditional expressions — schedule=(a | b) for any-of, & for all-of, and combinations like a | (b & c) — when a consumer needs logic beyond a plain list.
- A consumer with two dataset dependencies fires once PER update by default
- Two producers updating in the same window = two consumer runs
- Require all-of when the consumer needs everything, or make the consumer idempotent and let it fire twice
4. Airflow 3.0: Datasets Become Assets
Airflow 3 renamed datasets to assets and formalized the concept. The scheduling you already know keeps working: the Dataset class is back-compat, and a schedule on datasets is equivalent to the asset trigger.
The practical differences: assets have stable names and URIs that form an identity — the same asset in two DAGs must be spelled identically. Migration tools rewrite Dataset references in your DAG files; the schedule expression uses the asset name.
Teams on 2.x can plan the upgrade without touching pipelines: the deprecated Dataset import still resolves, and the scheduler reads both forms during the transition.
5. Combining Schedules with Datasets
A consumer doesn't have to be purely event-driven. You can combine a time schedule with a dataset dependency — the DAG runs at the cron time AND the dataset condition.
Common production shape: a batch consumer that runs daily at 7 AM, but only if the dataset was updated since the last run. The schedule is expressed as a combination; Airflow evaluates both conditions.
This is where most migration confusion lives: the schedule parameter takes either datasets or a timetable, never both mixed directly. The supported construct is DatasetOrTimeSchedule, which wraps a timetable (for example CronTriggerTimetable) together with a dataset condition and fires when either side is met — the docs' own example runs on cron OR when both datasets update. Teams that assume AND semantics misread the missed runs.
6. Event-Driven Doesn't Mean Retry-Free
The biggest misconception: dataset triggers replace the need for retries and idempotency.
A producer can complete twice in a window — a retried run, a manual re-run, a backfill. Each completion updates the dataset. The consumer fires each time.
That's the design. The consumer's tasks must be idempotent enough to run twice and produce the same result, and the producer's outlet must sit on a task whose success actually means the data is final.
And the flip side: a producer that fails never updates the dataset, so the consumer simply doesn't fire. You lose the failure signal you'd get from a cron. Watch for datasets that never update — that's a silent stall, and it needs its own alert.
- A stalled producer means a silent consumer: no run, no error, no alert
- Dataset updates need their own watchdogs
- Producer retries multiply consumer firings
- Idempotency is the contract between them
The Downstream DAG That Ran Before the Table Existed
- Cron-to-cron dependencies are races with extra steps.
- Dataset/asset triggers bind the consumer to the producer's real completion.
- Event-driven doesn't mean retry-free: keep tasks idempotent.
- Buffer times are guesses; completion events are facts.
airflow datasets listairflow dags list | grep consumer| File | Command / Code | Purpose |
|---|---|---|
| dags | from datetime import datetime | 2. Producer/Consumer with Dataset Outlets |
| dags | from datetime import datetime | 3. Any-of vs All-of Semantics |
| dags | from datetime import datetime | 4. Airflow 3.0 |
| dags | from datetime import datetime | 5. Combining Schedules with Datasets |
Key takeaways
Common mistakes to avoid
4 patternsWiring the outlet on the wrong task
Assuming a multi-dataset consumer fires once
Using glob or regex patterns in a dataset URI
No watchdog on dataset updates
Interview Questions on This Topic
How does dataset-aware scheduling work in Airflow?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Airflow. Mark it forged?
3 min read · try the examples if you haven't