The DAG Run Labeled 'Jan 15' Doesn't Run On Jan 15
A daily DAG is scheduled schedule="0 7 * * *". An engineer writes a
task that emails "here is the {{ ds }} sales report," assuming
{{ ds }} will render as "today's date" — the day the email actually
goes out. They're confused when the run that fires on the morning of
January 16 renders {{ ds }} as 2026-01-15, and the report it
generates only contains data through the end of January 15, nothing
from January 16.
Nothing is broken. What does {{ ds }} — and the DAG run's
logical/execution date more generally — actually represent?
The start of the data interval the run covers, not the execution day.
Airflow's scheduling model isn't "run today, dated today" — it's
interval-based. Each daily DAG run represents responsibility for one
24-hour data interval, and by design that run doesn't fire until
the interval has fully closed, so there's a complete interval's worth
of data to process. {{ ds }} (and the run's data_interval_start,
what older Airflow versions called execution_date) is the start
of that interval — for a daily schedule, midnight of the day the run
is "for" — not the calendar day the run's tasks actually execute on.
So the run that Airflow labels 2026-01-15 covers the interval
[2026-01-15 00:00, 2026-01-16 00:00). Because that interval doesn't
close until midnight on the 16th, the scheduler doesn't fire this
run's tasks until after that — at 0 7 * * *, that's 07:00 on
January 16. The run executes on the 16th, is labeled the 15th,
and processes data from the 15th — three different things that
happen to share a date only by coincidence of a daily schedule, and
diverge further for hourly, weekly, or custom-interval DAGs. The
email correctly reports "the 15th's sales" because that's the
interval this run owns — it isn't supposed to contain the 16th's
data, since the 16th's own run (which will itself execute on the
17th) is responsible for that.
This is one of the most common sources of Airflow confusion
precisely because the name execution_date (used in Airflow 1.x and
still present as a deprecated alias) actively suggests "the date this
executes," when it always meant "the start of the interval this run
covers" — Airflow 2.2+ renamed the concept to data_interval_start/
data_interval_end specifically to stop implying the wrong thing.
The practical rule: never read a DAG run's date as "when this ran" —
read it as "which interval of source data this run is accountable
for," and check the schedule to work out when it actually executed.
Share this question