At 00:00 on the last day of every month (28th, 29th, 30th or 31st). Standard cron has no 'last day' value, so the line fires on days 28-31 and a guard in the command lets only the last one through.
0 0 28-31 * *
Open in Crontab Generator
Month-end closing, billing and archiving that must happen before the new month starts.
| Field | Value | Matches |
|---|---|---|
| Minute | 0 | 0 |
| Hour | 0 | 0 |
| Day of month | 28-31 | 28, 29, 30, 31 |
| Month | * | every value |
| Day of week | * | every value |
Because cron cannot express this directly, the crontab line carries a guard that skips the unwanted days:
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /path/to/job
Computed when this page loaded (2026-11-11 02:24 UTC), assuming the scheduler runs in UTC. Only the days that pass the guard are listed.
| # | UTC | Your local time |
|---|---|---|
| 1 | Mon 2026-11-30 00:00 | |
| 2 | Thu 2026-12-31 00:00 | |
| 3 | Sun 2027-01-31 00:00 | |
| 4 | Sun 2027-02-28 00:00 | |
| 5 | Wed 2027-03-31 00:00 | |
| 6 | Fri 2027-04-30 00:00 | |
| 7 | Mon 2027-05-31 00:00 | |
| 8 | Wed 2027-06-30 00:00 | |
| 9 | Sat 2027-07-31 00:00 | |
| 10 | Tue 2027-08-31 00:00 |
Kubernetes uses the same five fields. Set spec.timeZone (stable since Kubernetes 1.27), otherwise the schedule follows the time zone of kube-controller-manager.
apiVersion: batch/v1
kind: CronJob
metadata:
name: last-day-of-every-month
spec:
schedule: "0 0 28-31 * *"
timeZone: "Etc/UTC"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: job
image: debian:bookworm-slim
command: ["/bin/sh", "-c", "if [ \"$(date -d tomorrow +%d)\" = \"01\" ]; then /app/run-job.sh; fi"]
Workflow schedules use five-field cron in UTC and run only from the default branch. Runs at minute 0 are the most likely to be delayed or dropped when GitHub is busy; an odd minute such as 7 or 23 starts more reliably.
on:
schedule:
- cron: '0 0 28-31 * *' # UTC
jobs:
run:
runs-on: ubuntu-latest
steps:
- name: Only continue on the intended day
id: gate
run: [ "$(date -d tomorrow +%d)" = "01" ] && echo "go=1" >> "$GITHUB_OUTPUT" || true
- run: ./run-job.sh
if: steps.gate.outputs.go == '1'
The equivalent OnCalendar= value is *-*~01 00:00:00.
Check it with systemd-analyze calendar '*-*~01 00:00:00', which prints the next elapse time. Persistent=true runs a missed job after downtime, which classic cron never does.
[Unit]
Description=Last day of every month
[Timer]
OnCalendar=*-*~01 00:00:00
Persistent=true
[Install]
WantedBy=timers.target
EventBridge rules take six fields (minute, hour, day-of-month, month, day-of-week, year) in UTC; EventBridge Scheduler can use a time zone. One of day-of-month and day-of-week must be ?, and day-of-week runs 1-7 from Sunday, so names are used here.
cron(0 0 L * ? *)
Quartz adds a leading seconds field and needs ? in either day field; days of the week are 1 (SUN) to 7 (SAT).
0 0 0 L * ?
Spring's @Scheduled(cron = ...) also uses six fields, but numbers days of the week 0-7 with Sunday as 0 or 7, like Unix cron; the names shown above work in both.
0 0 15 * *0 0 31 * *0 0 1,15 * *0 0 1 */3 *0 0 1 1,7 *0 0 1 1 *