Apache Airflow Schedule Interval

Cron, Data Interval, Logical Date

The Apache Airflow schedule interval is the rule in a Dag's schedule argument that decides when the scheduler creates a run: a cron expression, a timedelta, a preset or a timetable. Each run covers a data interval, and under a data interval timetable it starts after that interval ends. Its logical date, the old execution date, is the start of the range, while the run itself happens later.

Last updated: Proudly published by: Jeff Wahl8 min read

What is the Apache Airflow schedule interval?

In Apache Airflow, the schedule interval is the rule that decides when the scheduler creates a Dag run. You set it with the schedule argument of a Dag, which accepts a cron expression, a timedelta, a cron preset such as @daily, or a timetable object. Airflow 3.0 removed the older schedule_interval and timetable arguments, so schedule is now the only one.

Every run also carries a data interval, the time range of data it is meant to process. The scheduler creates a run only after its data interval has ended when you use a data interval timetable, and that delay is the source of most confusion about the schedule. A run named for 00:00 on day 1 starts at 00:00 on day 2.

Statements here come from the Airflow timetables documentation, which is versioned 3.3.2 at the time of writing.

Prepare for the interview
01 / Open invite
02min.

Know Airflow schedule interval the way the interviewer who asks it knows it.

a Airflow schedule interval query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1source → bronze → silver → gold
2 ingest : CDC + Kafka
3 transform : dbt + Airflow
4 serve : Snowflake
5
Execute your solution0.4s avg.
DatabricksInterview question
Solve a Airflow schedule interval problem

What is the data interval in Airflow?

The data interval is the range of time a Dag run is responsible for, exposed to your tasks as data_interval_start and data_interval_end. For a daily cron schedule, 0 0 * * *, each interval is 24 hours long. The run for the interval that begins at 00:00 on day 1 is created at 00:00 on day 2, when the last of its data has arrived.

That's the behavior of a data interval timetable, and it's why a Dag scheduled for midnight seems to run a day late. A trigger timetable works differently. It fires at the cron time and gives the run an interval whose start and end are both that trigger time, so the interval is 0 hours long unless you ask for one with the interval argument.

2 lanes on one time axis from 00:00 day 1 to 00:00 day 2: CronDataIntervalTimetable draws a 24 hour data interval with the run starting at its end, and CronTriggerTimetable starts its run at the same moment with a 0 hour interval2 lanes on one time axis from 00:00 day 1 to 00:00 day 2: CronDataIntervalTimetable draws a 24 hour data interval with the run starting at its end, and CronTriggerTimetable starts its run at the same moment with a 0 hour interval

The practical rule is to write tasks against data_interval_start and data_interval_end instead of the wall clock. A task that filters rows between the 2 values returns the same rows when you rerun it next week, and a task that reads now() does not.

Logical date vs execution date in Airflow

The logical date is the new name for what Airflow 2 called the execution date, and the name was always misleading, because it isn't when the run executes. The Dag run documentation says the logical date "denotes the start of the data interval, not when the Dag is actually executed". Airflow 3.0 removed the execution_date context variable, along with next_execution_date, prev_execution_date, next_ds and prev_ds, and tells you to read dag_run.logical_date instead.

The Airflow 3 upgrade guide lists those removals and warns that leaving them in a Dag will cause Dag errors. Manual runs changed too: Airflow no longer accepts a logical date in the future, and logical_date=None triggers a run with the current timestamp.

For a manually triggered run, don't assume the data interval follows from the logical date you supplied. If a Dag needs the date the user chose, read logical_date explicitly. If it needs the range of data to process, read the data interval.

The 4 timetables behind the schedule argument

CronTrigger
  • TakesCron expression
  • Data intervalStart equals end
  • Run is createdAt the trigger
  • Default for cronYes, Airflow 3
CronDataInterval
  • TakesCron expression
  • Data intervalBetween cron ticks
  • Run is createdAt interval end
  • Default for cronNo
DeltaTrigger
  • TakesA timedelta
  • Data intervalStart equals end
  • Run is createdOnce a delta passes
  • Default for cronNo
DeltaDataInterval
  • TakesA timedelta
  • Data intervalOne delta wide
  • Run is createdAt interval end
  • Default for cronNo

Trigger vs data interval timetables, and which one a cron string gets

A string such as 0 0 * * * is shorthand for a timetable, and which one it becomes is set by the create_cron_data_intervals option in the [scheduler] section. It was added in Airflow 2.9.0. Its default is False, which means a bare cron string becomes a CronTriggerTimetable. Set to True, it becomes a CronDataIntervalTimetable. The Airflow 3 upgrade guide notes the default change, and says it only affects Dags that pass a bare cron string, so a Dag that names its timetable is unaffected.

The difference shows in the first run. The timetables documentation enables a daily Dag at 15:00 on January 31. The trigger timetable creates its first run at midnight on February 1, while the data interval timetable creates a run immediately, with a run id timestamp of midnight on January 31. If the Dag is paused on January 31 and enabled again on February 2, the trigger timetable skips the runs for February 1 and 2, and the data interval timetable skips only February 1 and triggers February 2 at once.

If you want a trigger timetable that still carries a real data interval, pass interval. The documented example, 0 18 * * 5 with timedelta(days=4, hours=9), runs on Fridays at 18:00 and covers Monday 09:00 to that Friday, which is 4 x 24 + 9 = 105 hours. For several cron expressions on 1 Dag, MultipleCronTriggerTimetable runs whenever any of them matches.

Writing the schedule as a timetable object

import pendulum
from datetime import timedelta
from airflow.sdk import DAG
from airflow.timetables.interval import CronDataIntervalTimetable
from airflow.timetables.trigger import CronTriggerTimetable

# A trigger timetable: runs at 18:00 on Fridays.
# interval= gives each run a data interval that ends at the trigger time.
with DAG(
    dag_id="weekly_report",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    schedule=CronTriggerTimetable(
        "0 18 * * 5", timezone="UTC",
        interval=timedelta(days=4, hours=9),
    ),
    catchup=False,
):
    ...

# A data interval timetable: each run covers the span between 2 cron ticks.
with DAG(
    dag_id="daily_load",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    schedule=CronDataIntervalTimetable("0 0 * * *", timezone="UTC"),
    catchup=False,
):
    ...

Airflow cron expression and presets

An Airflow cron expression has 5 fields: minute, hour, day of month, month and day of week. Airflow parses it with croniter, so extended forms work, for instance 0 0 * * MON#1 for midnight on the first Monday of the month. The expression */30 * * * * fires every 30 minutes on the clock, which is different from a 30 minute timedelta, and Airflow also accepts None to never schedule and @once to schedule a single run.

Presets are named cron expressions. Their documented descriptions say "at midnight (24:00)" for the daily, weekly, monthly, quarterly and yearly presets, and "at the end of the hour" for @hourly. Read these with the timetable in mind: under a data interval timetable the run for a day starts after that day ends.

A preset or cron string is the shortest way to write a schedule, but it hides the timetable choice. A Dag that depends on the data interval should name its timetable instead of relying on the create_cron_data_intervals setting of whoever deploys it.

Airflow cron presets and the expressions they stand for

@hourly
0 * * * *, once an hour
@daily
0 0 * * *, once a day at midnight
@weekly
0 0 * * 0, Sunday at midnight
@monthly
0 0 1 * *, the first day of the month
@quarterly
0 0 1 */3 *, the first day of each quarter
@yearly
0 0 1 1 *, January 1
@continuous
Run as soon as the previous run finishes
@once
Schedule once and only once

Airflow catchup, start date and missed intervals

By default the scheduler creates a run only for the latest interval. Airflow 3.0 made catchup_by_default False, so a Dag that doesn't set catchup will no longer backfill missed intervals. With catchup=True, the scheduler starts a run for every past interval since the start date that hasn't run, and it does the same for the gap when a paused Dag is switched on again. Several of those runs can overlap, bounded by max_active_runs_per_dag, which defaults to 16.

Leave catchup off when the Dag already works out its own backlog, for example a task that loads everything newer than the last row it saw. Turn it on when each run owns 1 interval of data and a missed interval is a hole in a table. Either way, the runs must be safe to repeat, which is the property described in idempotent pipelines.

Timedelta schedules and the clock

A cron expression produces intervals aligned to the clock boundaries it names, whatever the start date. A timedelta does not. With catchup=False, a 30 minute delta aligns to the current time: if the Dag is enabled at 01:05, the first interval runs from 00:35 to 01:05, as the timetables documentation shows. The documentation also says DeltaDataIntervalTimetable doesn't necessarily align to arbitrary bounds such as the start of a day or an hour.

Pick a cron expression when the schedule has to match the clock, such as a report at 06:00, and a delta when only the spacing matters, such as polling every 10 minutes. If a Dag has to start the moment the last one ends, @continuous does exactly that.

When you move an existing Dag from a trigger timetable to a data interval timetable, the documentation says 1 scheduled run is skipped. The reverse change doesn't skip one.

Schedule questions in a data engineering interview

Interviewers ask about the schedule because it exposes whether you know what a run represents. Common prompts: why does the run for yesterday start today, what's the data interval of a run you triggered by hand, and how would you reload 3 missed days without duplicating rows. The answers, in order: the interval ends before the run starts, a manual run has its own logical date, and each run must overwrite only its own interval.

Practice them with the Airflow interview questions and by designing the pipelines themselves in the data pipeline practice problems. The data pipeline interview questions cover the orchestration questions around them, and the system design round guide shows where scheduling comes up in the loop. For the structure of the Dag itself, see Airflow Dags explained.

Airflow schedule interval FAQ

What is the schedule interval in Apache Airflow?+
It's how often a Dag run is created. Since Airflow 3.0 the single schedule argument takes a cron expression, a timedelta, a cron preset such as @daily, a timetable object, or None for runs you trigger yourself. The older schedule_interval argument was removed in 3.0.
What does a cron expression mean in Airflow?+
It's a 5 field expression for minute, hour, day of month, month and day of week, parsed with croniter. The expression 0 0 * * * fires at midnight every day, and croniter also accepts extended forms such as 0 0 * * MON#1 for the first Monday of the month.
What is the data interval in Airflow?+
The time range a Dag run operates on. A run for the daily interval that starts at 00:00 on day 1 and ends at 00:00 on day 2 is usually scheduled after that interval ends, so it starts at 00:00 on day 2 and processes the 24 hours that just finished.
What is the difference between logical date and execution date?+
They're the same idea under 2 names. Execution date was the older name, and the execution_date context variable was removed in Airflow 3.0 in favor of dag_run.logical_date. Under a data interval timetable, the logical date marks the start of the data interval, not the moment the run executes.
Why did my daily Airflow Dag not run at the start date?+
With a data interval timetable, the first run is created after the first interval ends, so a Dag whose start date is midnight runs for the first time a full interval later. A trigger timetable runs at the next trigger time instead of after an interval.
Does Airflow backfill missed runs by default?+
No. From Airflow 3.0 the catchup_by_default setting is False, so a Dag that doesn't set catchup creates a run only for the latest interval. Set catchup=True on the Dag to create runs for every past interval that has not run.
How do I run an Airflow Dag every 30 minutes?+
Use the cron expression */30 * * * *, or a timedelta of 30 minutes. The 2 behave differently: a cron expression aligns to the clock, while a timedelta with catchup off aligns to the moment the Dag was enabled.
02 / Why practice

The candidate who gets the offer

  1. 01

    Reading a solution is not the same as writing one

    Every engineer who has frozen on a query they had read a dozen times knows the gap. The only preparation that closes it is producing the answer yourself, under time, before the interview does it for you

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    System design comes down to the calls you defend out loud

    Ingestion, batch vs streaming, the bronze/silver/gold layers, idempotency, backfill and replay. Sketching the pipeline and naming the failure modes is the signal, not the boxes

Related guides