Schedule Strings and Presets
Concepts covered: Schedules and timetables
The 4 kinds of schedule value
schedule takes 4 kinds of value. A timedelta lays windows of 1 fixed length end to end, the first opening at start_date. A cron string, written inside a CronDataIntervalTimetable, pins every window boundary to a time on the clock. A preset such as @daily is a cron string with a short name and goes in the same place. None means no schedule at all: the DAG runs only when a person triggers it, and a DAG that leaves schedule out is treated as None in Airflow 3.
import pendulum
from airflow.sdk import dag, task
from airflow.timetables.interval import CronDataIntervalTimetable
@dag(
dag_id="weekday_report",
start_date=pendulum.datetime(2026, 11, 2, tz="UTC"),
schedule=CronDataIntervalTimetable("0 6 * * 1-5", timezone="UTC"),
)
def weekday_report():
@task
def build():
...
build()
weekday_report()
Reading a cron string
A cron string has 5 fields separated by spaces: minute, hour, day of month, month and day of week. A * means every value the field allows, a number means that value only, 1-5 means a range and */15 means every 15th value. Fields are checked together, so 30 2 1 * * fires only when the minute is 30, the hour is 2 and the day of the month is 1 at the same moment. The string names a set of instants on the clock, and Airflow uses each instant as a boundary between 2 windows.
| Field | Values | 0 6 * * 1-5 reads |
|---|---|---|
| minute | 0 to 59 | 0, on the hour |
| hour | 0 to 23 | 6, so 06:00 |
| day of month | 1 to 31 | every day |
| month | 1 to 12 | every month |
| day of week | 0 to 6, Sunday is 0 | 1 to 5, Monday to Friday |
| Cron string | Instants it names |
|---|---|
| `0 6 * * *` | 06:00 every day |
| `30 2 1 * *` | 02:30 on the 1st of each month |
| `*/15 * * * *` | every 15 minutes, at :00, :15, :30 and :45 |
| `0 0 * * 0` | 00:00 every Sunday |
| `0 */6 * * *` | 00:00, 06:00, 12:00 and 18:00 every day |
From instants to windows
Take 0 6 * * 1-5 through the week of 2 November 2026. Its instants fall at 06:00 on Monday through Friday. The window that opens at 06:00 on Monday closes at 06:00 on Tuesday, and the run named for Monday is created at that moment. The window that opens at 06:00 on Friday 6 November has no Saturday or Sunday instant to close it, so it stays open until 06:00 on Monday 9 November: 72 hours in a single window, and its run is created on Monday morning carrying the whole weekend's data.
| Window opens | Window closes | Run created | Length |
|---|---|---|---|
| Mon 2 Nov 06:00 | Tue 3 Nov 06:00 | Tue 3 Nov 06:00 | 24 hours |
| Tue 3 Nov 06:00 | Wed 4 Nov 06:00 | Wed 4 Nov 06:00 | 24 hours |
| Wed 4 Nov 06:00 | Thu 5 Nov 06:00 | Thu 5 Nov 06:00 | 24 hours |
| Thu 5 Nov 06:00 | Fri 6 Nov 06:00 | Fri 6 Nov 06:00 | 24 hours |
| Fri 6 Nov 06:00 | Mon 9 Nov 06:00 | Mon 9 Nov 06:00 | 72 hours |
That is the whole method for predicting a cron schedule: list the instants, pair each with the one after it, and the run for a pair is created at the later instant and named for the earlier. Windows come out equal when the instants are evenly spaced and unequal when the string skips days, and the data each run reads stretches or shrinks to match. A task in weekday_report that reads from data_interval_start to data_interval_end handles the 72 hour window without a line of special code.
Presets
| Preset | Cron string | Window |
|---|---|---|
| `@hourly` | `0 * * * *` | 1 hour, opening on the hour |
| `@daily` | `0 0 * * *` | 1 day, opening at midnight |
| `@weekly` | `0 0 * * 0` | 7 days, opening Sunday 00:00 |
| `@monthly` | `0 0 1 * *` | 1 month, opening on the 1st |
| `@yearly` | `0 0 1 1 *` | 1 year, opening 1 January |
| `@once` | none | a single run at `start_date`, then never again |
Each cron preset is read exactly as the cron string beside it, so CronDataIntervalTimetable("@daily", timezone="UTC") and the same call with "0 0 * * *" produce identical windows and identical run names. @once is the exception: it names no cron string, so it is a schedule value of its own, written as schedule="@once" and never placed inside the timetable. Pick the preset when it says what you mean and the cron string when it does not; a daily report at 06:00 has no preset and needs 0 6 * * *.
A bare string such as schedule="0 6 * * *" is accepted too. How a deployment reads a bare string is a setting on that deployment, so this lesson writes the timetable out by name and leaves the other reading to a later lesson.
About this lesson
This section is part of the Scheduling: Beginner lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.