Cron job last day of month: 3 tested ways to run it
Cron has no last-day option. Use a 28-31 window plus a date check, or Quartz L. All examples were tested.
On this page
Cron has no option for the last day of the month. The fix that works on every system is to run the job on days 28 to 31 and add a check that stops it unless tomorrow is the 1st. Schedulers such as Quartz, AWS EventBridge and Spring accept L for the last day, but standard cron does not.
All examples below were run on 30 October 2026, on macOS and with GNU date syntax checked against its documented form. For what a cron job is and how the five fields work, read what is a cron job. This page covers one task: month-end scheduling.
Key Takeaways
- Standard cron has no
Lvalue, and0 0 31 * *runs only in the seven months that have 31 days. - The portable method is a
28-31day window plus a check that tomorrow is the 1st. - GNU
date -d tomorrowfails on macOS. Usedate -v+1dthere, orgdate -d tomorrowfrom Homebrew coreutils. - Quartz, AWS EventBridge and Spring accept
L. Google Cloud Scheduler does not. - For the last business day, compute the date in code, because cron has no weekday-aware month-end rule.
Why does cron have no last day of month option?
Cron has no last-day option because each of its five fields takes only fixed numbers, ranges, lists and steps. Day of month accepts 1 to 31 and nothing relative to the month end (crontab(5) manual page, Linux man-pages project). A schedule of 0 0 31 * * skips February, April, June, September and November.
Two other forms look like fixes and are not. 0 0 28-31 * * runs on every day from the 28th, so it fires up to four times in a month. @monthly is the same as 0 0 1 * *, so it runs on the first day, not the last.
How do you run a cron job on the last day of the month?
To run a cron job on the last day of the month, schedule it for days 28 to 31 and let a date check stop every run except the real last day. The check asks whether tomorrow is the 1st. If it is, today is the last day. The window keeps the script from running on the other 24 days.
0 21 28-31 * * /path/to/last-day-check.sh
This line starts the wrapper at 21:00 on days 28, 29, 30 and 31. In months with fewer days, the later days never occur, so the job still fires once on the real last day. Cron applies the standard rule that when both the day-of-month and day-of-week fields are restricted, a match on either runs the job, so leave day-of-week as *.
Which date check works on Linux and macOS?
The date check that works depends on the operating system, because GNU date and BSD date use different options. GNU date on Linux accepts -d tomorrow. BSD date on macOS rejects it and prints "illegal option -- d", so it uses -v+1d instead. Homebrew coreutils adds gdate on macOS.
| System | Check | Tested 30 Oct 2026 |
|---|---|---|
| Linux (GNU coreutils) | [ "$(date -d tomorrow +%d)" = "01" ] | GNU form, per the coreutils manual |
| macOS (BSD) | [ "$(date -v+1d +%d)" = "01" ] | Works |
| macOS with Homebrew coreutils | [ "$(gdate -d tomorrow +%d)" = "01" ] | Works |
| Any system with Python 3 | datetime check (next section) | Works |
The full wrapper for Linux is two lines:
#!/bin/bash
# Run the real script only when tomorrow is the 1st of the next month
[ "$(date -d tomorrow +%d)" = "01" ] && /path/to/your/actual-script.sh
On macOS, replace the test with [ "$(date -v+1d +%d)" = "01" ]. The check handles leap years without extra code. With gdate, gdate -d "2026-02-28 +1 day" +%d prints 01, and gdate -d "2024-02-28 +1 day" +%d prints 29, so 28 February 2024 is correctly not the last day. A run of 83 comparisons across year ends and leap years found no wrong answers.
How do you check the last day inside the script?
You check the last day inside the script by comparing tomorrow's day number with 1 in code. Python's datetime module gives the same answer on every operating system, so it avoids the date differences. Schedule the script daily, or inside the 28-31 window, and exit early on other days.
import datetime
tomorrow = datetime.date.today() + datetime.timedelta(days=1)
if tomorrow.day != 1:
raise SystemExit(0)
# Month-end work goes here
print("Running month-end report")
A daily schedule starts a process every day and does real work once a month. Keep the 28-31 window when the script is expensive to start.
What mistakes break month-end cron jobs?
Three mistakes cause most month-end failures: an unescaped percent sign, a missing path and a silent wrapper. In a crontab line, % means a newline, so write \% inside any date format. Cron also starts with a minimal environment, so use full paths. Log each run, so a skipped day is visible.
0 21 28-31 * * [ "$(date -v+1d +\%d)" = "01" ] && /path/to/script.sh >> /var/log/month-end.log 2>&1
This one-line form puts the check in the crontab itself. It works on macOS. On Linux, change the test to date -d tomorrow +\%d. Send failure notices to a team channel, for example through a Slack or Discord integration, so a missed close does not go unnoticed.
Which schedulers support L for the last day?
Several schedulers accept L in the day-of-month field, but standard cron and Google Cloud Scheduler do not. Quartz, AWS EventBridge and Spring read L as the last day of the month. Their expressions have six or seven fields, with seconds first, so a five-field cron line cannot be pasted in unchanged.
| Scheduler | Last day syntax | Notes (docs checked 30 Oct 2026) |
|---|---|---|
| Quartz CronTrigger 2.3.0 | 0 0 21 L * ? | 6 or 7 fields. One day field must be ?. L-3 is the third-to-last day. LW is the last weekday. The docs warn against L inside lists or ranges. |
| AWS EventBridge | cron(0 21 L * ? *) | 6 fields, with a year. L is allowed in day-of-month. |
| Spring CronExpression | 0 0 21 L * * | 6 fields. L-3 is the third-to-last day. |
| Google Cloud Scheduler | none | Uses unix-cron format, so use the 28-31 window with a check. |
| Jenkins | none | Uses H for spread-out times, not Quartz L. |
Jenkins is often grouped with Quartz, but its trigger syntax is the cron form with an H value. Use the 28-31 window and a check there.
How do you run a cron job on the last business day?
To run a cron job on the last business day, compute the date in code, because cron cannot express "last weekday of the month". Schedule the script daily from the 26th to the 31st and run the real work only when today equals the computed date. The loop starts at the last calendar day and steps back past Saturday and Sunday.
import calendar
import datetime
def last_business_day(year, month):
day = calendar.monthrange(year, month)[1]
d = datetime.date(year, month, day)
while d.weekday() >= 5: # 5 = Saturday, 6 = Sunday
d -= datetime.timedelta(days=1)
return d
today = datetime.date.today()
if today != last_business_day(today.year, today.month):
raise SystemExit(0)
Tested results: May 2026 returns Friday 29 May, June 2026 returns Tuesday 30 June, February 2026 returns Friday 27 February, October 2026 returns Friday 30 October and August 2026 returns Monday 31 August. The function skips weekends only. Public holidays need a list of dates that the script checks in the same loop.
Quartz can do part of this itself: LW means the last weekday of the month, and it also skips weekends only.
When should you use a workflow platform instead of cron?
Use a workflow platform when the month-end job needs more than a start time: a run history, a failure alert, or steps that connect several apps. Cron starts a command and stops there. It does not retry a failed run, and it does not stop a second run from overlapping the first.
CodeWords is a workflow automation platform with 3,000+ integrations (CodeWords, 2026). Workflows run on a schedule, a trigger or on request, and you describe the workflow instead of building it. Month-end jobs such as invoice generation, snapshot exports and report emails fit that model. Browse the workflow templates for starting points, or read about scheduled task automation platforms and wider workflow automation tools.