Airflow Conditional Execution: Yesterday's Data, Today's Run
Airflow can rerun yesterday's data on today's run.
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Airflow DAG basics: tasks, dependencies, and the scheduler loop.
- ✓Comfort with the @task and @dag decorators (TaskFlow style).
- ✓Basic understanding of data intervals and DAG runs.
- Conditional execution means a task only runs when today's data interval actually qualifies, not just because the DAG fired.
- ShortCircuitOperator stops the whole downstream branch when its condition fails; BranchPythonOperator picks one of several paths.
- LatestOnlyOperator lets the latest DAG run pass while late triggers skip, and one task protects a 30-task chain.
- Guard inside tasks with data_interval_end comparisons; it's 3 lines of code and catches what operators miss.
- Skipped tasks fail default all_success joins, so set trigger rules like none_failed on every join task.
- A ShortCircuit skip overrides downstream trigger rules by default; pass ignore_downstream_trigger_rules=False to let joins re-evaluate.
Imagine a newspaper that prints every morning. If the printing press breaks down and runs at 6 PM instead, it will reprint yesterday's edition unless someone checks the date on the front page. Conditional execution is that check: a bouncer at the door who reads the ticket, sees which day the run actually belongs to, and turns away runs that would replay old data. The DAG still runs, but the tasks inside decide whether today's work is worth doing at all.
Your DAG fired at 9:02 AM. The run row is green, every task shows success. And the dashboard is showing yesterday's numbers anyway. Sound familiar? That's the gap between "the DAG ran" and "the data is fresh" — and it's the gap that gets teams in trouble.
Schedules drift. The source feed lands late, a worker lags, someone hits the trigger button outside the window. When the run finally happens, nothing stops it from reprocessing a data interval that went stale hours ago. The scheduler doesn't care about freshness. It only cares about execution.
That's what conditional execution is for: skip semantics, ShortCircuitOperator, LatestOnlyOperator, and guards that read data_interval_end before touching any data. Each one answers a different version of "should this run happen at all?". This article is about choosing the right one — and knowing when you shouldn't be conditionally executing in the first place.
1. Why "The DAG Ran" Doesn't Mean the Data Is Fresh
Airflow is an execution engine, not a freshness engine. A DAG run fires on a schedule, executes tasks, and records states. Nothing in that loop knows whether the data inside the run's interval is what the business wants on screen right now.
The mismatch appears the moment reality breaks the schedule: a late file, a slow worker, a holiday, a manual trigger. The run executes anyway, against the interval it was created for. If that interval isn't today's, the run is quietly serving old data while every status light stays green.
This is why the most senior habit in this article is skepticism: never read run success as data success. Read the interval.
- A DAG run is an execution of a data interval, not a guarantee about that interval's data.
- Freshness is a property of the data; execution is a property of the scheduler.
- Conditional execution closes the gap by making the interval a first-class condition.
2. ShortCircuit vs Branch: Two Ways to Skip
BranchPythonOperator returns a task_id and sends the run down one path. ShortCircuitOperator returns a boolean: True keeps the whole downstream chain, False skips everything after it. Branch chooses between paths; ShortCircuit decides whether the journey happens at all.
That distinction drives production design. Use Branch when two or more real outcomes exist — say, "file is full" vs "file is empty". Use ShortCircuit when there is exactly one outcome that should do work, and every other outcome should do nothing.
The classic trap: teams use Branch for a yes/no question, then fight the skipped branch through every join. A boolean question deserves a ShortCircuit, not a fork in the road.
One behavior to know before you rely on a short-circuit: its skip overrides downstream trigger rules by default. A join with none_failed sitting after a short-circuited chain is still skipped — even when another branch of the graph succeeded. Set ignore_downstream_trigger_rules=False to let downstream trigger rules evaluate again; only the direct children stay skipped.
3. LatestOnlyOperator and Late Schedules
LatestOnlyOperator sits at the head of a DAG and lets exactly one run through: the latest. When the scheduler fires a run for an old interval — after an outage, a catchup, or a manual trigger — LatestOnly skips everything downstream for that old run.
It's the bluntest freshness tool, and it's perfect for exactly one scenario: a DAG that must only ever run for the current interval, period. Reconciliations and backfills need old intervals processed, and LatestOnly would skip them all.
Know the weapon before you wield it. LatestOnly is a one-liner that protects a whole chain, but it quietly disables history.
4. Guarding on data_interval_end
Data intervals are the real contract of a run. Every run carries data_interval_start and data_interval_end, and the scheduler picks these from the schedule, not from the wall clock. A guard that compares data_interval_end to today's date is the most precise freshness check you can write.
Where do you put it? Either in a ShortCircuitOperator callable, as in section 2, or directly inside the load task as a hard assertion. The assertion version is stronger: it fails loudly instead of skipping silently, which is what you want for compliance-grade loads.
One warning: the first run for a schedule fires after start_date plus one interval, and timezone boundaries can shift the comparison by a day. Always compare in UTC against the run's own interval, never against the local clock of the worker.
5. Building Conditional Task Chains
Conditional execution changes the state graph: skipped tasks appear, and joins must survive them. The default trigger rule all_success fails a join task the moment any upstream is skipped. That's the exact failure from the branching article, and it recurs in every conditional DAG until you set rules deliberately.
The production pattern is simple: guards at the head, joins with none_failed or all_done, and leaf tasks that read skip states before writing. A chain that treats skipped as a first-class state is a chain you can debug in the Grid view.
When you find yourself adding the third condition, stop. Three booleans in one DAG usually mean three DAGs, each with one job and one guard.
6. Idempotent Guards Instead of Conditional Loads
Here's the opinionated part: the best conditional execution is the one you don't need. If a load is fully idempotent — upsert on a natural key, delete-and-reload a partition — then replaying an interval is harmless, and the freshness guard becomes a nicety instead of a lifeline.
Teams that condition their loads instead of making them idempotent end up with branches for every edge case: partial files, re-sends, holiday skips. Each branch is a place where the wrong state can slip through. Each branch is also dead weight when the real fix is an upsert.
So the senior ordering is: make the load idempotent first, add the interval guard second, and keep the DAG linear. Conditional execution is a shield, not a strategy.
Yesterday's Data on Today's Run
- A green DAG run only proves execution, never freshness.
- Late schedules silently replay old intervals when nothing checks the interval.
- Guard with ShortCircuit or LatestOnly, then assert data_interval_end inside the task.
- A guard task is cheaper than a data-quality incident and far easier to audit.
| File | Command / Code | Purpose |
|---|---|---|
| interval_check.py | from datetime import datetime, timezone | 1. Why "The DAG Ran" Doesn't Mean the Data Is Fresh |
| shortcircuit_guard.py | from datetime import datetime, timedelta, timezone | 2. ShortCircuit vs Branch |
| latest_only_guard.py | from datetime import datetime, timedelta | 3. LatestOnlyOperator and Late Schedules |
| interval_assert_task.py | from datetime import datetime, timedelta, timezone | 4. Guarding on data_interval_end |
Key takeaways
Common mistakes to avoid
4 patternsUsing BranchPythonOperator for a yes/no question
Guarding on execution_date instead of data_interval_end
Adding LatestOnlyOperator to a DAG that runs backfills
Hiding the freshness check inside the load task
Interview Questions on This Topic
What is the difference between BranchPythonOperator and ShortCircuitOperator?
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