The monthly schedule
A cron every month schedule fires once per calendar month, and the conventional form puts it at
midnight on the 1st. Crontab also accepts @monthly, which expands to exactly those fields. The 1st
is not an arbitrary convention: it is the only day number that exists in all twelve months, so it is
the only one where a monthly schedule really means twelve runs a year.
Day numbers above 28 are a lottery
Pin a job to the 31st and it runs in January, March, May, July, August, October and December. The other five months have no 31st, so the schedule simply does not match, and nothing anywhere reports a missed run. The 30th loses February. The 29th loses February in three years out of four. Only the 1st through the 28th are safe in every month, and only the 1st is safe and obvious.
If a job must run near the end of the month, say so explicitly rather than hoping the day number exists.
Getting the last day without an L
The Quartz dialect used by Java schedulers has a character for the last day of the month. Crontab does not, and adding one is not on offer. The pattern that works splits the decision in two: the schedule fires on every day that could be the last one, and the script checks whether today actually is.
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /opt/app/close-month.sh
The date test asks whether tomorrow is the first, which is true exactly once per month regardless of length. Percent signs need escaping inside a crontab line, which is the detail that costs people an afternoon. On systems without GNU date, the equivalent test uses a different flag, so check yours before trusting the line.
Quarters and half years
A step in the month field counts from January. Three gives you January, April, July and October, which are the calendar quarters, and six gives you January and July. That is the right answer when your year starts in January and the wrong one when it does not — a financial year beginning in April needs the four months listed explicitly, because the step has no way of knowing where you would like it to start.
Twelve chances a year to notice
A monthly job gets twelve attempts a year, so a bug in one can survive a whole quarter before anyone connects it to the schedule. Test the line by running the command by hand first, then watch the next five runs the panel above computes and check that the dates are the ones you expected — a schedule that skips five months a year looks perfectly healthy in a log until someone asks for the missing report.
Where monthly work belongs
A monthly job is usually a close, an invoice run or a retention sweep, and all three want the timezone question settled before anything else, exactly as a daily midnight job does. If the work is weekly reporting that happens to be summarised monthly, a Monday schedule is the honest expression of it. Read the finished line back with the cron expression generator, where the next five runs will show you immediately whether the day number you chose exists as often as you expected.