The schedule argument of a DAG takes a cron expression, a preset such as "@daily", a timedelta or a timetable object. The table below covers the usual cases. Times are in the DAG’s timezone, which is UTC unless you set another one.

Copy and paste

Runs schedule=
Every 15 minutes "*/15 * * * *"
Every hour, on the hour "@hourly" or "0 * * * *"
Every 2 hours "0 */2 * * *"
Every day at 06:00 "0 6 * * *"
Every day at 08:00 and 20:00 "0 8,20 * * *"
Monday to Friday at 07:30 "30 7 * * 1-5"
Every Monday at 07:00 "0 7 * * 1"
First Monday of the month at 07:00 "0 7 * * 1#1"
First day of the month at 05:00 "0 5 1 * *"
Last day of the month at 23:00 "0 23 L * *"
1 January, April, July and October "@quarterly"
Every 3 days timedelta(days=3)
Every 6 hours, counted from the start date timedelta(hours=6)
Only when triggered None
Once "@once"

A complete DAG:

import pendulum
from airflow.sdk import dag

@dag(
    schedule="*/15 * * * *",
    start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
    catchup=False,
)
def every_15_minutes():
    ...

every_15_minutes()

Two times that a single cron cannot express, such as 08:30 and 17:00:

from airflow.sdk import MultipleCronTriggerTimetable

schedule = MultipleCronTriggerTimetable("30 8 * * *", "0 17 * * *", timezone="Europe/Rome")

Reading a cron expression

A cron expression has five fields: minute, hour, day of the month, month and day of the week. * means every value, */n every nth value, 1-5 a range and 8,20 a list. Days of the week go from 0 (Sunday) to 6 (Saturday), and 7 is Sunday again. The presets are shortcuts:

Preset Cron Runs
@hourly 0 * * * * every hour at minute 0
@daily 0 0 * * * every day at midnight
@weekly 0 0 * * 0 Sunday at midnight
@monthly 0 0 1 * * the 1st at midnight
@quarterly 0 0 1 */3 * 1 January, April, July and October
@yearly 0 0 1 1 * 1 January at midnight

Traps

Checked against the Airflow 3.3.2 scheduler: every expression above was expanded into its next runs.

References