The short answer
Standard five-field cron can't say "the last day of the month": months end on the 28th, 29th, 30th or 31st, and no field knows which. The standard workaround is to schedule the job on every possible last day and let the command check whether tomorrow is the 1st:
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /usr/local/bin/month-end.sh
Cron starts the line at midnight on the 28th, 29th, 30th and 31st; the test passes only on the one day that is really the last.
| Field | Value | Meaning |
|---|---|---|
| Minute | 0 |
at minute 0 |
| Hour | 0 |
at midnight |
| Day of month | 28-31 |
the 28th to the 31st |
| Month | * |
every month |
| Day of week | * |
any day of the week |
How the test works
date -d tomorrow +%dprints tomorrow's day of the month. If it's01, today is the last day.- In a crontab,
%must be written\%: cron treats a bare%as a newline and cuts the command there. That's the usual reason this line "does nothing". -d tomorrowis GNUdate(Linux). On macOS and BSD usedate -v+1d +%d.
Midnight on the last day is the start of that day. For a month-end job that should see the whole month, run late in the day (55 23 28-31 * * with the same test) or run on the 1st instead with 0 0 1 * *, processing the previous month.
Where L works
Quartz-style schedulers and AWS have a real last-day marker:
- AWS EventBridge:
cron(0 0 L * ? *).Lin the day-of-month field is the last day of the month. - Quartz (Java, Spring's Quartz integration, many enterprise schedulers):
0 0 0 L * ?(Quartz has a seconds field first). - Not supported: classic crontab, GitHub Actions and Kubernetes CronJob reject
L. Spring's own@Scheduledcron does accept it.
Variants
| Schedule | Expression |
|---|---|
| Last day at 23:00 | 0 23 28-31 * * plus the test |
| Last day of each quarter | 0 23 28-31 3,6,9,12 * plus the test |
| The day before the last | 0 0 27-30 * * with -d '+2 days' in the test |
| First day of the month | 0 0 1 * * |
| 1st and 15th | 0 0 1,15 * * |
Where to write it
- crontab: the line with the test, as above.
- GitHub Actions:
cron: '0 0 28-31 * *'(UTC) and a first step that checks[ "$(date -u -d tomorrow +%d)" = 01 ](no escaping outside crontab), with anif:on the real job so it runs only when that check passes. - Kubernetes CronJob:
schedule: "0 0 28-31 * *"withtimeZone:, and the test at the start of the container's command. - AWS EventBridge:
cron(0 0 L * ? *), no test needed.
Look at the candidate days in the Cron Expression Parser.