Airflow cron format
The schedule argument takes a five-field cron string, laid out like a crontab line:
| Field | Allowed values | Special characters |
|---|---|---|
| Minute | 0–59 | , - * / |
| Hour | 0–23 | , - * / |
| Day of month | 1–31 | , - * ? / L W |
| Month | 1–12 or JAN–DEC | , - * / |
| Day of week | 0–7 or SUN–SAT (0 and 7 = Sunday) | , - * ? / L # |
Special characters
| Symbol | Meaning |
|---|---|
@ | Presets: @hourly, @daily, @weekly, @monthly, @quarterly (0 0 1 */3 *) and @yearly. @once runs the DAG a single time, @continuous starts a new run as soon as the last one ends, and schedule=None leaves the DAG to manual or API triggers. |
L | The last day of the month in day-of-month: 0 0 L * *. In day-of-week croniter puts L first, so L5 is the last Friday of the month; the Quartz form 5L is rejected. |
# | The nth weekday of the month: MON#1 is the first Monday and 5#3 the third Friday. |
W | The weekday nearest to a date: 15W. It takes a single day, not a list or a range. |
? | Same as *, allowed on its own in either day field. |
/ | Steps: */30 in the minute field runs at minutes 0 and 30, and 0 */4 * * * every four hours from midnight. |
Airflow cron examples
Click an expression to load it into the generator above.
| Expression | Schedule |
|---|---|
| At 12:00 AM. | |
| At 6:00 AM. | |
| At 6:00 AM, Monday through Friday. | |
| Every 30 minutes. | |
| At 0 minutes past the hour, every 4 hours. | |
| At 12:00 AM, on day 1 of the month, only in January, April, July, and October. | |
| At 12:00 AM, on the last day of the month. | |
| At 9:00 AM, on the first Monday of the month. | |
| At 6:00 PM, on the last Friday of the month. | |
| At 9:00 AM, on day 1 of the month or on Monday. | |
| Once only: Airflow creates a single run and never schedules the DAG again. | |
| Continuously: a new run starts as soon as the previous one finishes. |
When does an Airflow cron schedule run?
- In Airflow 3, a cron string uses
CronTriggerTimetable:0 6 * * 1-5starts a run at 06:00 on weekdays, and the run’s logical date is that same moment. - In Airflow 2, and in Airflow 3 with
[scheduler] create_cron_data_intervals = True, the string usesCronDataIntervalTimetableinstead, and each run starts at the end of its data interval. The run stamped Friday 06:00 covers Friday to Monday, so it starts on Monday at 06:00. - When both day fields are set, a run starts if either one matches, as in crontab:
0 9 1 * 1runs on the 1st of the month and on every Monday. - Times follow the time zone of
start_date, including daylight saving time. A naivestart_dateuses[core] default_timezone, which is UTC unless you change it. catchupdefaults toFalsein Airflow 3, so turning a DAG on does not backfill the runs it missed. Airflow 2 defaulted toTrue.
Use it in a DAG
Pass the expression to schedule. The time zone of start_date decides when it fires:
import pendulum
from airflow.sdk import dag, task
@dag(
schedule="0 6 * * 1-5",
start_date=pendulum.datetime(2026, 1, 1, tz="Asia/Riyadh"),
catchup=False,
)
def weekday_report():
@task
def build_report():
...
build_report()
weekday_report()
To fix when runs start, whatever the Airflow version or settings, pass a timetable instead of a string:
from airflow.sdk import CronDataIntervalTimetable, CronTriggerTimetable
# Starts at 06:00; the logical date is 06:00 (Airflow 3 default)
schedule=CronTriggerTimetable("0 6 * * 1-5", timezone="Asia/Riyadh")
# Each run covers the interval that just ended (Airflow 2 default)
schedule=CronDataIntervalTimetable("0 6 * * 1-5", timezone="Asia/Riyadh")
On Airflow 2, import dag and task from airflow.decorators, and the timetables from airflow.timetables.trigger and airflow.timetables.interval. schedule_interval, deprecated since Airflow 2.4, is gone in Airflow 3: use schedule.
Frequently asked questions
Why didn’t my Airflow DAG run at the scheduled time?
With a data-interval timetable (the Airflow 2 default), a run starts when its interval ends: the @daily run for 1 March starts at midnight on 2 March. Also check that start_date is in the past, that the DAG is unpaused, and which time zone start_date uses.
What is the difference between @once, @continuous and None?
@once creates a single run and never schedules the DAG again. @continuous starts a new run as soon as the previous one finishes, and needs max_active_runs=1. schedule=None creates no runs at all, so the DAG runs only when you trigger it by hand or through the API.
Does Airflow support L, W and # in cron expressions?
Yes, because Airflow parses cron with croniter: 0 0 L * * is the last day of the month, 0 9 * * MON#1 the first Monday and 0 18 * * L5 the last Friday. Quartz forms such as 5L, LW and L-2 are rejected.
Which time zone does an Airflow cron schedule use?
The time zone of the DAG’s start_date. With pendulum.datetime(2026, 1, 1, tz="Asia/Riyadh"), 0 6 * * * runs at 06:00 Riyadh time. A naive start_date uses [core] default_timezone, which is UTC by default.
Cron generators for other platforms
*/15 8-18 * * 1-5
Quartz Cron Expression Generator
0 0 8 ? * MON-FRI
Spring Cron Expression Generator
0 0 8 * * MON-FRI
AWS Cron Expression Generator
0 8 ? * MON-FRI *
Jenkins Cron Expression Generator
H 2 * * 1-5
Salesforce Cron Expression Generator
0 0 8 ? * MON-FRI
Hangfire Cron Expression Generator
0 8 * * 1-5
node-cron Expression Generator
0 30 8 * * 1-5