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.

  1. Mon, Dec 7, 2026 at 9:00 AM
  2. Mon, Jan 4, 2027 at 9:00 AM
  3. Mon, Feb 1, 2027 at 9:00 AM
  4. Mon, Mar 1, 2027 at 9:00 AM
  5. 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.

Schedule pattern
First Monday of the month
first-monday-of-month
Category
Common Patterns
Standard cron expression patterns

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.

crontab
0 9 * * 1 [ "$(date +\%d)" -le 7 ] && /path/to/job

The 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 %.

Shell
#!/bin/sh
[ "$(date +%d)" -le 7 ] || exit 0
/path/to/job

With 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:

systemd
OnCalendar=Mon *-*-1..7 09:00:00

The 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

Every 5 minutes
Running a cron job every 5 minutes uses the step operator in the minute field: */5 * * * *.
View details
Every hour
To run a job at the top of every hour, use 0 * * * *.
View details
Daily at midnight
0 0 * * * runs a job at exactly midnight (00:00) every day.
View details

Frequently asked questions

How do I run a cron job on the first Monday of every month?
Use 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?
Both day fields are set, so cron runs the job when either one matches. Days 1 to 7 match through the day of month, and every Monday matches through the day of week. A * 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?
Cron reads an unescaped % 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?
Change the day of week number and keep the same test. The first Friday is 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?
The 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.