skip to content

What replaced execution_date and schedule_interval in Airflow 3?

level: middleimportance: should knowfreq 50%

answer

  1. two long deprecations finally removed
  2. one names a timestamp, one names a schedule
  3. the timestamp got an explicit window instead
  4. one argument now takes cron, timedelta, timetable or assets

basics

~20 s

Airflow 3 removed execution_date in favour of logical_date plus the data_interval_start and data_interval_end pair, and replaced schedule_interval and the separate timetable argument with one schedule argument that accepts a cron string, a timedelta, a Timetable, or assets.

solid answer

~40 s

Two long-running deprecations landed for good in Airflow 3.0. First, `execution_date` — which held the *start* of the period being processed while the run executed at its end — is gone; runs carry `logical_date` plus explicit `data_interval_start`/`data_interval_end`, a model introduced in Airflow 2.2. Derived macros such as `next_ds` and `prev_ds` went with it. Second, `schedule_interval` and the separate `timetable=` argument are replaced by a single `schedule=` argument, added in Airflow 2.4, which accepts a cron string, a `timedelta`, a `Timetable` object, or a list of assets for data-driven scheduling. Airflow 3 also renamed Datasets to **Assets**, and relaxed the run model so `logical_date` can be null for runs that are not tied to a schedule, with a `run_after` field ordering runs instead. `{{ ds }}` and `{{ ts }}` still work.

code

python · 7 lines
python
# legacy (removed in Airflow 3)
dag = DAG("orders", schedule_interval="@daily", start_date=datetime(2024, 1, 1))
# template: {{ execution_date }} .. {{ next_ds }}

# current
dag = DAG("orders", schedule="@daily", start_date=pendulum.datetime(2024, 1, 1, tz="UTC"))
# template: {{ data_interval_start }} .. {{ data_interval_end }}

go deeper

for a junior

Recall the modern spelling: schedule= rather than schedule_interval=, and data_interval_start/data_interval_end rather than execution_date. Knowing the current syntax is enough at this level.

for a middle

Explain why the old names misled — execution_date was the period start, not the execution moment — and be able to map each legacy macro onto its replacement during a migration.

for a senior

Show you would audit a legacy codebase for execution_date filters and next_ds bounds before a 3.0 upgrade, and know that logical_date is nullable for non-scheduled runs so partition logic must not assume it exists.

for a principal

Own the upgrade as a programme: an inventory of DAGs using removed symbols, a house convention on interval-templated filters, and a decision on whether to standardise on time-driven or asset-driven scheduling while you are already touching every DAG.

## Why these renames happened Both changes fix names that actively taught the wrong mental model. `execution_date` was never the date a run executed. It was the *start of the period being processed*, while the run itself executed after that period ended. Every Airflow newcomer met the field, assumed it meant "now", filtered data with it, and was then baffled that the DAG appeared a day behind. `schedule_interval` was similarly narrow: once Airflow gained custom timetables and then data-driven triggering, a parameter named "interval" that also had to accept a `Timetable` object or a list of datasets no longer described what it did — and having *two* parameters (`schedule_interval` and `timetable`) for one concept guaranteed confusion about precedence. ## The timeline - **Airflow 2.2** introduced the explicit data-interval model: `data_interval_start`, `data_interval_end`, and `logical_date` as the new name for `execution_date`, which was deprecated but kept working. Custom `Timetable` classes became a supported extension point. - **Airflow 2.4** introduced the unified `schedule=` argument and Datasets (`Dataset`, plus `outlets=` on producing tasks) for data-driven scheduling. - **Airflow 2.9** added conditional dataset expressions (`DatasetAny`, `DatasetAll`) and combined time-plus-dataset scheduling. - **Airflow 3.0** removed the deprecated names outright: no `execution_date`, no `schedule_interval`, no separate `timetable=`. It renamed Datasets to **Assets** and loosened the DAG-run model so a run need not have a schedule-derived logical date at all. ## What the code looks like Before (Airflow 1.x / early 2.x style): ```python dag = DAG( "orders", schedule_interval="@daily", start_date=datetime(2024, 1, 1), ) # task template: {{ execution_date }} / {{ ds }} / {{ next_ds }} ``` After: ```python dag = DAG( "orders", schedule="@daily", start_date=pendulum.datetime(2024, 1, 1, tz="UTC"), ) # task template: {{ data_interval_start }} / {{ data_interval_end }} / {{ ds }} ``` The migration is mostly mechanical, but not blindly so. `{{ execution_date }}` maps to `{{ logical_date }}` *or* to `{{ data_interval_start }}` — identical for cron and timedelta schedules, but the interval fields are what you actually want in a `WHERE` clause, and they are the ones that stay correct if the DAG later moves to a custom timetable. `{{ next_ds }}`, which people used as the upper bound of a window, maps to `{{ data_interval_end }}` (as a date, `{{ data_interval_end | ds }}`) — clearer and no longer version-dependent. ## What `schedule=` accepts One argument, four kinds of value: - a cron string — `"0 3 * * *"`, or a preset such as `"@daily"`, `"@hourly"`, `"@monthly"`; - a `timedelta` — `timedelta(hours=6)`, meaning "six hours after the previous interval ended"; - a `Timetable` instance — for schedules cron cannot express (business days only, an events calendar, or `CronTriggerTimetable` when you want to fire *at* a cron time rather than at the end of an interval); - a list of assets (datasets in Airflow 2.4–2.10) — the DAG runs when its upstream data is declared updated, with no clock involved. `None` means the DAG is never scheduled and runs only when triggered. ## The looser run model in Airflow 3 Airflow 2 required every DAG run to have a unique `execution_date`/`logical_date` per DAG — which meant you could not trigger the same DAG twice "for now" without contriving distinct timestamps. Airflow 3 removed that unique constraint and made `logical_date` nullable for runs that are not schedule-derived, such as manual and asset-triggered runs, ordering runs by a `run_after` field instead. The practical consequence: a run that has no window genuinely reports that it has no window, rather than being handed a fabricated timestamp that downstream partition logic then misuses. ## Interview framing What is being probed is rarely trivia about which release dropped which symbol. It is whether you understand *why* Airflow moved from a single ambiguous timestamp to an explicit interval, and whether you would notice a legacy DAG that still filters on `execution_date`. If you are asked to state a version, say what you are assuming — "this is Airflow 2.9 syntax; on 3.0 the deprecated names are gone" — because the honest version-aware answer is what distinguishes someone who has actually run a migration from someone reciting a changelog.

  • When migrating a DAG, does {{ execution_date }} map to {{ logical_date }} or {{ data_interval_start }}?
    Both are equal for cron and timedelta schedules, but prefer `{{ data_interval_start }}` in data filters. It states the intent — the window being processed — and stays correct if the DAG later moves to a custom timetable where logical_date and the interval start need not coincide. Reserve `{{ logical_date }}` for identifying the run itself.
  • What can you pass to Airflow's schedule argument besides a cron string?
    A `timedelta` for elapsed-time intervals, a `Timetable` instance for schedules cron cannot express (business days, an events calendar, or `CronTriggerTimetable` to fire at the cron time rather than at an interval end), a list of assets for data-driven triggering, or `None` for a DAG that only ever runs when triggered.
  • Why did Airflow 3 make logical_date nullable?
    Because runs that are not schedule-derived — manual triggers and asset-triggered runs — have no data window, and Airflow 2's unique-per-DAG execution_date constraint forced a fabricated timestamp onto them, which downstream partition logic then misread. Making it nullable, with a run_after field for ordering, lets such a run honestly report that it covers no interval.

saying these in an interview costs you the question

  • Says execution_date and logical_date mean different points in time for a cron schedule
  • Believes schedule_interval still works in Airflow 3 as a deprecated alias
  • Thinks the rename changed when runs fire rather than what they are called
  • Claims Datasets and Assets are two different Airflow features
  • States a version confidently without saying which release is assumed

context