Cron first Monday of the month: why 0 9 1-7 * 1 fires too often
By TechCompare · Updated · How we check
On plain cron, run the job every Monday at 9 AM and let a shell test pass only when the day of the month is 1 to 7. Cron has no schedule field for the first Monday, so the check goes in the command.
Calculator
Next runs of 0 9 * * 1
Example from Mon, Nov 9, 2026 at 8:00 AM.
- Mon, Dec 7, 2026 at 9:00 AM
- Mon, Jan 4, 2027 at 9:00 AM
- Mon, Feb 1, 2027 at 9:00 AM
- Mon, Mar 1, 2027 at 9:00 AM
- Mon, Apr 5, 2027 at 9:00 AM
Only the Monday in the first seven days of each month counts, like the shell test on the crontab line.
0 9 * * 1 runs at 9 AM every Monday. The trouble starts when you want only the first Monday. The obvious line, 0 9 1-7 * 1, runs on days 1 to 7 of each month and on every Monday, so it fires on days you didn't ask for.
How this is calculated
Both day fields hold a value rather than *, so cron runs the job when either one matches. POSIX words the rule this way: "then any day matching either the month and day of month, or the day of week, shall be matched." This is the trap to check first, because cron accepts the line without complaint.
0 9 * * 1 [ "$(date +\%d)" -le 7 ] && /path/to/jobThe minute 0 and hour 9 set the run time to 9:00 AM. The day of month and month are both *, so the weekday 1 decides the day, and cron matches Mondays only. The shell test does the rest. In the crontab line, date +\%d prints the day of the month, [ ... -le 7 ] is true for days 1 to 7, and && runs the job only when that test passes. A week has seven days, so exactly one Monday in each month falls on days 1 to 7.
The backslash in \% is required in a crontab line. The crontab(5) man page says an unescaped percent sign "will be changed into newline characters", and everything after the first one goes to the command as standard input. Without the escape, the command gets cut at the % and the test breaks. After you copy a command from a terminal, the crontab line is where it breaks, because the terminal version has a bare % in it. A script file doesn't have this problem, so the same test can live there with a plain %.
#!/bin/sh
[ "$(date +%d)" -le 7 ] || exit 0
/path/to/jobWith the script, the crontab line is just 0 9 * * 1 /path/to/first-monday.sh, with no escape needed. The leading zero from date isn't a problem either. date +%d prints 07 and 08, and [ reads those as plain decimal numbers, so [ "08" -le 7 ] is false, which is what you want. Bash and dash both handle it this way.
systemd can express the whole rule in one line. Its calendar syntax needs the weekday and the day of the month to both match, and the systemd.time(7) page documents the format. In a timer unit, this line fires only on a Monday that falls on days 1 to 7:
OnCalendar=Mon *-*-1..7 09:00:00The server's time zone is the default for cron, and date reads the same zone, so the 9 AM run and the day check agree. If your server runs on UTC but you think in local time, a 9 AM job runs at 9 AM UTC. Read the cron timezone guide before you pick the hour.
Verdict
Keep the day of month at * for a weekday job, and set both day fields only when you want the either-or behavior. If the host runs systemd, a timer is the cleaner choice, since it needs no shell test and no escaped percent sign. On plain cron, check the command in a terminal with a plain % first, then add the backslash only when you paste it into the crontab.
More Cron scenarios
Related guides
Frequently asked questions
How do I run a cron job on the first Monday of every month?
0 9 * * 1 [ "$(date +\%d)" -le 7 ] && /path/to/job in your crontab, and swap in your own command for /path/to/job. The day of month stays *, so cron matches Mondays, and the date test keeps only the Mondays that fall on days 1 to 7.Why does 0 9 1-7 * 1 run every Monday?
* in either day field makes both fields have to match, which is why the working line leaves the day of month at *.Why do I need a backslash before the percent sign in crontab?
% in the command as a line break and sends everything after it to the command as standard input. Writing \% passes a plain % to the shell. The backslash only matters in the crontab line, so a script file can use % as written.How do I run a cron job on the first Friday, or any other weekday?
0 9 * * 5 [ "$(date +\%d)" -le 7 ] && /path/to/job, since Friday is 5 in cron's numbering, where Sunday is 0. Any weekday works this way, because days 1 to 7 hold exactly one of each. The last Monday needs a different test, since it can fall anywhere from the 22nd to the 31st. The last day of the month page shows the month end version of the date check.Does the date test work on macOS?
date +\%d part is the same on macOS. Only the -d tomorrow form on the last day of the month page differs, because BSD date uses -v for date math. The FreeBSD date man page covers the -v syntax.Related tools
Unix Timestamp
Convert between Unix timestamps and human-readable dates.
Use toolCHMOD Configurator
Calculate Linux file permissions using checkboxes or octal numbers, then view the matching symbolic notation.
Use toolText Encoding Converter
Convert text to Base64, binary, hex, URL encoding and HTML entities, and back again.
Use tool