The expression
0 0 1,15 * *
Runs at midnight on the 1st and the 15th of every month: 24 times a year, the classic semi-monthly schedule for payroll, invoicing and reports.
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 |
at minute 0 |
| Hour | 0 |
at midnight |
| Day of month | 1,15 |
the 1st and the 15th |
| Month | * |
every month |
| Day of week | * |
any day of the week |
A comma makes a list: each value is a separate match, so 1,15 is the 1st or the 15th. Every month has both days, so neither run is ever skipped.
Variants
| Schedule | Expression |
|---|---|
| 1st and 15th at 9am | 0 9 1,15 * * |
| 1st and 16th (two equal halves) | 0 0 1,16 * * |
| 15th and last day of the month | 0 0 15,28-31 * * plus a test on the 28-31 runs |
| The 1st only | 0 0 1 * * |
| The last day only | 0 0 28-31 * * plus a test |
| Every other week instead | workaround |
For "the 15th and the last day", which is how many payrolls actually work, the 28-31 runs need the tomorrow-is-the-1st check:
0 0 15,28-31 * * [ "$(date +\%d)" = 15 ] || [ "$(date -d tomorrow +\%d)" = 01 ] && /usr/local/bin/payroll.sh
% must be escaped as \% in a crontab. On AWS EventBridge, use two rules instead: cron(0 0 15 * ? *) and cron(0 0 L * ? *).
Twice a month is not every two weeks
The 1st-and-15th schedule has gaps of 14 days (1st to 15th) and 13 to 16 days (15th to the next 1st, depending on the month). That's what semi-monthly means. If the job must run exactly every 14 days, on the same weekday, you want every other week, which cron needs a workaround for.
Where to write it
- crontab (Linux, macOS):
0 0 1,15 * * commandincrontab -e, in the server's time zone. - GitHub Actions:
cron: '0 0 1,15 * *', in UTC; the run lands on the evening before for anyone west of UTC, so consider0 12 1,15 * *. - Kubernetes CronJob:
schedule: "0 0 1,15 * *"withtimeZone:. - AWS EventBridge:
cron(0 0 1,15 * ? *), with?in the day-of-week field.
Common mistakes
- Adding a weekday.
0 0 1,15 * 1-5looks like "the 1st and 15th, if they're weekdays", but cron runs when either day field matches, so it runs on the 1st, the 15th and every weekday. To move weekend dates, keep0 0 1,15 * *and let the script decide, or on AWS use two rules with1Wand15W(the nearest weekday). 0 0 1-15 * *. A dash is a range: every day from the 1st to the 15th.
See the next runs in the Cron Expression Parser.