A cron expression is a short pattern of time fields that tells a scheduler when to run a job. 0 9 * * 1-5 means “at 09:00 on Monday to Friday”. The format comes from the Unix cron daemon and its crontab files, and it now also drives GitHub Actions schedules, Kubernetes CronJobs, AWS EventBridge, Quartz and Spring. They do not all read it the same way: some add a seconds field, some number the weekdays from 1, and they disagree about what happens when both day fields are set.
This guide starts with the classic five-field form as documented in the cronie crontab(5) manual, the cron used by Fedora and RHEL, and then compares six schedulers field by field with links to their documentation. The descriptions and run times below come from the Cron Expression Parser, and the site’s tests recompute them from the parser’s code.
The Five Fields
┌───────────── minute 0-59
│ ┌─────────── hour 0-23
│ │ ┌───────── day of month 1-31
│ │ │ ┌─────── month 1-12 or jan-dec
│ │ │ │ ┌───── day of week 0-7 or sun-sat (0 and 7 are both Sunday)
│ │ │ │ │
* * * * *
Fields are separated by whitespace. Month and weekday names are the first three letters of the English name, in any case, and cronie allows them in ranges and lists, so mon-fri works. In a crontab file the command follows the fifth field. A crontab line can also start with one of the macros @reboot, @yearly, @annually, @monthly, @weekly, @daily or @hourly instead of the five fields; @daily is the same as 0 0 * * *.
A job runs when the minute, hour and month fields all match the current local time and the day fields match as described below. cron checks its tables once a minute, so a five-field expression cannot express anything finer than a minute.
Operators: Star, Lists, Ranges and Steps
| Operator | Meaning | Example |
|---|---|---|
* | Every value from first to last | * in the hour field: every hour |
, | A list | 0,30 in the minute field: at :00 and :30 |
- | An inclusive range | 9-17 in the hour field: 09 through 17 |
/ | A step through a range or * | */15 in the minute field: 0, 15, 30, 45 |
A step does not create an interval that runs across the field’s boundary. It counts from the start of the range and restarts every hour, day or month. The crontab(5) notes give the example itself: */35 in the minute field runs at :00 and :35, not every 35 minutes. The parser lists the runs after 08:00 as 08:35, 09:00 and 09:35, where the gap is 25 minutes, then 35. For a true “every 35 minutes”, run the job every minute and let it check the clock, as the man page suggests, or use a scheduler with interval support such as EventBridge rate(35 minutes).
cronie accepts a step only after * or a range. 5-59/10 gives 5, 15, 25 … 55, but 5/10 is an error in cronie, and the parser rejects it with Not valid: "5/10" in the minute field. A step must follow * or a range, for example */10 or 5-59/10. Other schedulers read 5/10 as “from 5, every 10”, so the same text is valid in one place and broken in another.
Day of Month and Day of Week Together
The rule that catches most people is in the crontab(5) note: “If both fields are restricted (i.e., do not contain the ”*” character), the command will be run when either field matches the current time.” The POSIX crontab specification says the same. So 30 4 1,15 * 5 runs on the 1st, on the 15th, and on every Friday, not only on Fridays that fall on the 1st or 15th. The parser describes it as “At 4:30, on day 1, 15 of the month or on fri”.
When one of the day fields starts with *, both conditions must hold. cronie applies this test to the first character, so */2 in the day-of-month field still counts as unrestricted, and 0 0 */2 * 1 means “odd-numbered days that are also Mondays”. The month field is always combined with AND.
Schedulers with their own dialect handle this differently, which is why the comparison table below has a row for it. Quartz and EventBridge sidestep the question by making you write ? in one of the two day fields. Spring reads both fields and requires every field to match: its CronExpression class walks the fields in turn until all of them agree (source), so a Spring expression with a day of month and a weekday means “the 1st, if it is a Monday”.
Examples with Their Next Run Times
Every row below was parsed by the tool, starting from Thursday 1 October 2026 at 08:00 local time. The description is the tool’s own wording.
| Expression | Description from the parser | Next three runs |
|---|---|---|
*/15 * * * * | At every 15 minutes past every hour | 2026-10-01 08:15, 08:30, 08:45 |
*/35 * * * * | At every 35 minutes past every hour | 2026-10-01 08:35, 09:00, 09:35 |
0 */6 * * * | At every 6 hours | 2026-10-01 12:00, 18:00, 2026-10-02 00:00 |
0 9 * * 1-5 | At 9:00, on mon through fri | 2026-10-01 09:00, 2026-10-02 09:00, 2026-10-05 09:00 |
0 12 * * sat,sun | At 12:00, on sun, sat | 2026-10-03 12:00, 2026-10-04 12:00, 2026-10-10 12:00 |
0 0 1 * * | At midnight, on day 1 of the month | 2026-11-01 00:00, 2026-12-01 00:00, 2027-01-01 00:00 |
0 0 1 1,4,7,10 * | At midnight, on day 1 of the month, in jan, apr, jul, oct | 2027-01-01 00:00, 2027-04-01 00:00, 2027-07-01 00:00 |
30 4 1,15 * 5 | At 4:30, on day 1, 15 of the month or on fri | 2026-10-02 04:30, 2026-10-09 04:30, 2026-10-15 04:30 |
0 0 29 2 * | At midnight, on day 29 of the month, in feb | 2028-02-29 00:00, 2032-02-29 00:00, 2036-02-29 00:00 |
0 0 30 2 * | At midnight, on day 30 of the month, in feb | none |
The last row is valid syntax that never runs, because February never has a 30th; the parser reports “No run time found”. There is also no five-field way to say “the last day of the month”. 0 0 28-31 * * runs on every one of those days, so the usual workaround is to run on days 28 to 31 and let the command check whether tomorrow is the 1st, for example [ "$(date -d tomorrow +%d)" = 01 ] with GNU date.
Pasting syntax from another dialect gives an error that names the problem. 0 0 * * 1#2 gives Not supported: "1#2" in the weekday field, 0 0 9 ? * MON-FRI gives Invalid: expected 5 fields, got 6., and @daily counts as one field, because the parser reads only the five-field form.
Six Schedulers, Six Dialects
| crontab (cronie) | Quartz | Spring | EventBridge | GitHub Actions | Kubernetes CronJob | |
|---|---|---|---|---|---|---|
| Fields | 5 | 6 or 7: seconds first, optional year last | 6: seconds first | 6: year last, inside cron(…) | 5 | 5 |
| Day of week | 0-7, 0 and 7 = Sunday | 1-7, 1 = Sunday | 0-7, 0 and 7 = Sunday | 1-7, 1 = Sunday | 0-6, 0 = Sunday | 0-6, 0 = Sunday |
? | No | Required in one day field | Same as * in day fields | Required in one day field | Not documented | Same as * |
L, W, # | No | Yes | Yes | Yes | No | No |
| Both day fields set | Either matches | Not allowed | Both must match | Not allowed | POSIX: either matches | Either matches |
| Macros | @daily etc., @reboot | No | @daily etc. | rate(…) instead | No | @daily etc. |
| Time zone | System time, or CRON_TZ | Trigger’s time zone | zone attribute | UTC for legacy rules; any IANA zone in Scheduler | UTC, or timezone key | Controller’s time, or .spec.timeZone |
Sources: crontab(5), the Quartz CronTrigger tutorial, Spring Framework scheduling, EventBridge Scheduler schedule types and legacy scheduled rules, GitHub Actions schedule, and Kubernetes CronJob. Kubernetes uses the robfig/cron library, whose dayMatches function applies the either-matches rule when neither day field is * or ?.
The same schedule, 09:00 on weekdays, written for each:
crontab 0 9 * * 1-5
Quartz 0 0 9 ? * MON-FRI
Spring 0 0 9 * * MON-FRI
EventBridge cron(0 9 ? * MON-FRI *)
GitHub Actions cron: '0 9 * * 1-5' (UTC unless timezone is set)
Kubernetes schedule: "0 9 * * 1-5"
Use names when you move between dialects. In Quartz and EventBridge, 2-6 is Monday to Friday and 1-5 is Sunday to Thursday, so copying 1-5 from a crontab silently shifts the job by a day. MON-FRI means the same thing everywhere it is accepted.
A few limits belong to the platform, not the syntax. GitHub runs scheduled workflows at most every 5 minutes, only from the default branch, and warns that runs “can be delayed during periods of high loads”, especially at the start of every hour, and that some queued jobs may be dropped; picking a minute such as 17 instead of 0 helps. Its documentation also shows 20/15 in the minute field as minutes 20, 35 and 50, the form that cronie rejects. In public repositories, GitHub disables scheduled workflows after 60 days without repository activity. EventBridge legacy rules have a minimum precision of one minute and always run in UTC.
Time Zones and Daylight Saving Time
A five-field expression has no time zone of its own; the scheduler supplies one.
- cronie uses the system time zone. A
CRON_TZ=Asia/Tokyoline in the crontab sets the zone for the schedule entries that follow;TZonly changes the environment the job runs in. - Kubernetes interprets schedules in the time zone of
kube-controller-managerunless.spec.timeZoneis set (stable since 1.27). PuttingCRON_TZ=orTZ=inside.spec.scheduleis rejected with a validation error. - GitHub Actions uses UTC unless the
timezonekey holds an IANA name such asAmerica/New_York. - EventBridge Scheduler takes a time zone with each schedule; legacy EventBridge rules run in UTC+0.
Daylight saving time creates hours that do not exist and hours that happen twice. cronie’s own manuals disagree on the missing hour: crontab(5) says such times “will never match”, while cron(8) says that after a forward jump of less than three hours, jobs scheduled in the skipped interval “will be run immediately”. GitHub documents that a 2:30 schedule in a zone with DST advances to 3:00 on the spring-forward day. The parser follows crontab(5): in America/New_York, where 02:00 to 03:00 on 14 March 2027 does not exist, 30 2 * * * lists 13 March and then 15 March. If a job must run exactly once a day, schedule it outside 01:00 to 03:00 local time or run the scheduler in UTC.
Checking an Expression Before It Ships
- Paste it into the Cron Expression Parser and read the description and the next ten runs. The run times are in your browser’s local time zone; convert them if the scheduler uses UTC or another zone.
- If both day fields are set, check whether your scheduler uses either-match or both-match.
- If you are copying from another platform, check the field count, the weekday numbering and whether the target accepts
?,L,Wor#. - For crontab, also check the command: cron runs it with a minimal environment and
/bin/sh, and in crontab(5) an unescaped%in the command “will be changed into newline characters”, which breaksdate +%F. Escape it as\%.
To build an expression from dropdowns instead of typing it, use the Cron Job Generator. To turn a run time into a Unix timestamp or a date in another zone, use the Timestamp Converter or the Time Zone Converter.