59

I'd like to run a job from cron at 8.30 on the first Monday of every month. The cron Wikipedia page says

While normally the job is executed when the time/date specification fields all match the current time and date, there is one exception: if both "day of month" and "day of week" are restricted (not "*"), then either the "day of month" field (3) or the "day of week" field (5) must match the current day.

(my emphasis)

Does that mean I can't do the first Monday of the month, I can only do the first (or whatever) day of the month? I can't think of a way round it.

2

19 Answers 19

58

You can put the condition into the actual crontab command (generic way):

[ "$(date '+%u')" = "1" ] && echo "It's Monday"

if your locale is EN/US, you can also compare strings (initial answer):

[ "$(date '+%a')" = "Mon" ] && echo "It's Monday"

Now, if this condition is true on one of the first seven days in a month, you have its first Monday. Note that in the crontab, the percent-syntax needs to be escaped though (generic way):

0   12  1-7 *   *   [ "$(date '+\%u')" = "1" ] && echo "It's Monday"

if your locale is EN/US, you can also compare strings (initial answer):

0   12  1-7 *   *   [ "$(date '+\%a')" = "Mon" ] && echo "It's Monday"

Replace the echo command with the actual command you want to run. I found a similar approach too.

5
  • actually, slhck, i spoke too soon - this isn't working in my crontab: it seems to have a problem with the date test for some reason. This works in the command line [ "$(date '+%a')" = "Mon" ] && echo "It's Monday" as does [ $(date '+%a') = "Mon" ] && echo "It's Monday" But, in the crontab it seems to be failing at this test. I can't get at any useful feedback from cron either: i'm trying this [ "$(date '+%a')" = "Tue" ] >> /home/deploy/cron.log 2>&1 to capture any error output into a log but that's not working either :/ May 29, 2012 at 8:53
  • Hm. Can you check whether the date output is correct? For example, just let a cron command run that outputs this to a file: date '+%a' > ~/datetest 2>&1 – then check if that resulted in the right string to compare against.
    – slhck
    May 29, 2012 at 8:58
  • 3
    I found the problem - the percent in +%a needed escaping when it was in the crontab, ie the line needed to be this: [ "$(date '+\%a')" = "Mon" ] && echo "It's Monday". If you could update your answer with that it would be helpful to future readers :) Thanks again - max May 29, 2012 at 9:33
  • Oh, now I see why. Glad it works now, thanks for trying it out!
    – slhck
    May 29, 2012 at 9:35
  • 1
    Beware that for alpine's CRON implementation, the percent sign needs not to be escaped.
    – helvete
    Feb 17, 2021 at 14:55
29

I have a computer with locale on Spanish, so, this approach isn't working for me because mon changes to lun

Other languages would fail as well, so, I did a slight variation on the accepted answer that takes out the language barrier:

 0 9 1-7 * *   [ "$(date '+\%u')" = "1" ] && echo "¡Es lunes!"
1
  • 1
    This should be the accepted answer. I have a Dutch locale which also uses different names for weekdays. And using numbers also feels cleaner than comparing strings to determine the day of the week.
    – 0ne_Up
    Nov 28, 2018 at 10:54
11

I find it easier when there's no need to handle day numbers.

Run First Monday of month:

0 2 * * 1 [ `date '+\%m'` == `date '+\%m' -d "1 week ago"` ] || /path/to/command

i.e. if the month 1 week ago is not the same as the current month then we are on the 1st day 1 (= Monday) of the month.

Similarly, for the Third Friday

0 2 * * 6 [ `date '+\%m'` == `date '+\%m' -d "3 weeks ago"` ] || /path/to/command

i.e. if the month 3 weeks ago is different to current month then we are on the 3rd day 6 (= Friday) of the month

1
  • 1
    Day 6 of week is Saturday not Friday.
    – Robert
    Jun 5, 2019 at 9:23
7

Since I interpret my cron statements using php and js, I can't use bash. Finally I found that it is in fact possible with just cron:

0 30 8 * 1/1 MON#1

Hope this helps someone else. Regardless, I wish you all a beautiful day. :-)

2
  • 1
    This is not cron, it's possibly quartz
    – sivann
    Nov 29, 2019 at 7:16
  • 1
    According to crontab.cronhub.io it is cron. Can also try 0 0 8 ? * 1#1 *
    – dbinott
    Oct 26, 2022 at 15:12
5

I have scheduled a job to run on the 4th Monday of every month at 4:00 PM as follows:

0 16 22-28 * Mon [ "$(date '+\%a')" == "Mon" ] && touch /home/me/FourthMonOfMonth.txt
1
  • Would have the same effect as 0 16 22-28 * * , but would run four times a month more. Mar 7, 2023 at 5:53
5

There is a hacky way to do this with a classic (Vixie, Debian) cron:

30 8 */100,1-7 * MON

The day-of-month field starts with a star (*), and so cron considers it "unrestricted" and uses the AND logic between the day-of-month and the day-of-week fields.

*/100 means "every 100 days starting from date 1". Since there are no months with more than 100 days, */100,1-7 effectively means "on dates 1 to 7".

Here's my article with more details: Schedule Cronjob for the First Monday of Every Month, the Funky Way

2

I recommend to use

"$(/bin/date '+%\w')" = "1"

instead of

"$(date '+\%a')" = "Mon"

to avoid locale problem.

1

As far as I know it is NOT possible using only crontab, however one can use a wrapper function to pick the correct day from a "first seven days of month" contab entry; see this from entry.

The wrapper script would be

#! /usr/bin/ksh
day=$(date +%d)
if ((day <= 7)) ; then
   exec somecommand
fi
exit 1

and you would need to run it (assuming it is called wrapper.sh and globally available) using the crontab entry

0 0 * * 1 wrapper.sh
1
  • thanks elemaki. I thought about a similar solution using some extra code inside the task that gets run, getting it to test whether it's the first monday of the month inside the script, and then calling it every monday in crontab. It's a ruby script so it's easy to test the day of th week. But i was hoping there'd be a crontab only way. May 25, 2012 at 15:56
1

On Solaris 10 I had to format the condition as follows:

[ `date +\%a` = "Sat" ] && echo "It's Saturday"
1

You can try running cronjob for first seven days of month and let it execute only on Monday.

30 8 * * 1 [`date +\%d` -le 07] && <job>

Above should work for you.

1
  • +1 more success hit ration then to run for the first week and check for Monday. :-)
    – xmedeko
    Mar 22, 2017 at 7:28
1

The day of month field here is */100,1-7, meaning “every 100 days starting from date 1, and also on dates 1-7”. Since there are no months with 100+ days, this again is a trick to say “on dates 1 to 7” but with a leading star. Because of the star, cron will run the command on dates 1 to 7 that are also Monday.

0 22 */100,1-7 * 2

On the cron guru example below, you can see the result and the next run date.

https://crontab.guru/#0_0_/100,1-7__MON

1
  • As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
    – Community Bot
    Jan 7, 2023 at 21:02
1

This answer expands on @ChiragPansheriya's answer to this same question.

tl;dr

Do a quick test of your cron implementation by pasting this into your crontab:

# For 1st Monday (or Tuesday, etc) of month, use */32,1-7 for days of month
# For 2nd Monday (or Tuesday, etc) of month, use */32,8-14 for days of month
# For 3rd Monday (or Tuesday, etc) of month, use */32,15-21 for days of month
# For 4th Monday (or Tuesday, etc) of month, use */32,22-28 for days of month
# For 5th Monday (or Tuesday, etc) of month, use */32,29-31 for days of month
# Explanation: https://superuser.com/a/1813556
* * */32,1-7   * 0  echo "First Sunday" >>cron_out.txt
* * */32,8-14  * 0  echo "Second Sunday" >>cron_out.txt
* * */32,15-21 * 0  echo "Third Sunday" >>cron_out.txt
* * */32,22-28 * 0  echo "Fourth Sunday" >>cron_out.txt
* * */32,29-31 * 0  echo "Fifth Sunday" >>cron_out.txt
* * */32,1-7   * 1  echo "First Monday" >>cron_out.txt
* * */32,8-14  * 1  echo "Second Monday" >>cron_out.txt
* * */32,15-21 * 1  echo "Third Monday" >>cron_out.txt
* * */32,22-28 * 1  echo "Fourth Monday" >>cron_out.txt
* * */32,29-31 * 1  echo "Fifth Monday" >>cron_out.txt
* * */32,1-7   * 2  echo "First Tuesday" >>cron_out.txt
* * */32,8-14  * 2  echo "Second Tuesday" >>cron_out.txt
* * */32,15-21 * 2  echo "Third Tuesday" >>cron_out.txt
* * */32,22-28 * 2  echo "Fourth Tuesday" >>cron_out.txt
* * */32,29-31 * 2  echo "Fifth Tuesday" >>cron_out.txt
* * */32,1-7   * 3  echo "First Wednesday" >>cron_out.txt
* * */32,8-14  * 3  echo "Second Wednesday" >>cron_out.txt
* * */32,15-21 * 3  echo "Third Wednesday" >>cron_out.txt
* * */32,22-28 * 3  echo "Fourth Wednesday" >>cron_out.txt
* * */32,29-31 * 3  echo "Fifth Wednesday" >>cron_out.txt
* * */32,1-7   * 4  echo "First Thursday" >>cron_out.txt
* * */32,8-14  * 4  echo "Second Thursday" >>cron_out.txt
* * */32,15-21 * 4  echo "Third Thursday" >>cron_out.txt
* * */32,22-28 * 4  echo "Fourth Thursday" >>cron_out.txt
* * */32,29-31 * 4  echo "Fifth Thursday" >>cron_out.txt
* * */32,1-7   * 5  echo "First Friday" >>cron_out.txt
* * */32,8-14  * 5  echo "Second Friday" >>cron_out.txt
* * */32,15-21 * 5  echo "Third Friday" >>cron_out.txt
* * */32,22-28 * 5  echo "Fourth Friday" >>cron_out.txt
* * */32,29-31 * 5  echo "Fifth Friday" >>cron_out.txt
* * */32,1-7   * 6  echo "First Saturday" >>cron_out.txt
* * */32,8-14  * 6  echo "Second Saturday" >>cron_out.txt
* * */32,15-21 * 6  echo "Third Saturday" >>cron_out.txt
* * */32,22-28 * 6  echo "Fourth Saturday" >>cron_out.txt
* * */32,29-31 * 6  echo "Fifth Saturday" >>cron_out.txt

Explanation

One might wonder:

The */32 element in the days of month field is always false (no month has more than 31 days), so why include it?

It turns out that, if the first character of the days of month or days of week fields is an asterisk (*), cron switches from taking the union of the days of month and days of week fields, to taking their intersection. The */32 element is there simply to trigger this change in behavior.

This is hinted at in the man page for cronie née vixie-cron:

Note: The day of a command's execution can be specified in the following two fields — 'day of month', and 'day of week'. If both fields are restricted (i.e., do not contain the "*" character), the command will be run when either field matches the current time. For example, "30 4 1,15 * 5" would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
https://www.mankier.com/5/crontab

The converse is that, if day of month or day of week do contain the * character, the command will be run only when both fields match the current time.

You can see how the presence of the leading * character in the days of month field changes crontab.guru's interpretation:

crontab entry specification crontab guru's interpretation
0 8 1-7 * 1 At 08:00 on every day-of-month from 1 through 7 and on Monday.
0 8 */32,1-7 * 1 At 08:00 on every 32nd day-of-month and every day-of-month from 1 through 7 if it's on Monday.

Examining the source code

The phrase used in the man page, "contain the * character," is ambiguous. Does it mean, consist solely of the * character? Or does it mean, contain the * character at any point within the field? Consulting the source code for vixie cron (entry.c), we see that the DOM_STAR and DOW_STAR flags get set if the first character of their respective fields is an asterisk (*).

/* DOM (days of month)
*/

if (ch == '*')
    e->flags |= DOM_STAR;                    ①
ch = get_list(e->dom, FIRST_DOM, LAST_DOM,
              PPC_NULL, ch, file);
if (ch == EOF) {
    ecode = e_dom;
    goto eof;
}

[...]

/* DOW (days of week)
*/

if (ch == '*')
    e->flags |= DOW_STAR;                    ②
ch = get_list(e->dow, FIRST_DOW, LAST_DOW,
              DowNames, ch, file);
if (ch == EOF) {
    ecode = e_dow;
    goto eof;
}

① The DOM_STAR flag gets set if the first character of the days of month field is a * character
② The DOW_STAR flag gets set if the first character of the days of week field is a * character

Later, in cron.c, if either of those two flags are set, cron switches the logical operator between the days of month and days of week fields from an OR to an AND:

/* the dom/dow situation is odd.  '* * 1,15 * Sun' will run on the
 * first and fifteenth AND every Sunday;  '* * * * Sun' will run *only*
 * on Sundays;  '* * 1,15 * *' will run *only* the 1st and 15th.  this
 * is why we keep 'e->dow_star' and 'e->dom_star'.  yes, it's bizarre.
 * like many bizarre things, it's the standard.
 */
for (u = db->head;  u != NULL;  u = u->next) {
    for (e = u->crontab;  e != NULL;  e = e->next) {
        Debug(DSCH|DEXT, ("user [%s:%d:%d:...] cmd=\"%s\"\n",
                          env_get("LOGNAME", e->envp),
                          e->uid, e->gid, e->cmd))
        if (bit_test(e->minute, minute) &&
                bit_test(e->hour, hour) &&
                bit_test(e->month, month) &&
                ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR))        ①
                  ? (bit_test(e->dow,dow) && bit_test(e->dom,dom))        ②
                  : (bit_test(e->dow,dow) || bit_test(e->dom,dom)))) {    ③
            if ((doNonWild && !(e->flags & (MIN_STAR|HR_STAR)))
                    || (doWild && (e->flags & (MIN_STAR|HR_STAR))))
                job_add(e, u);
        }
    }
}

① The DOM_STAR and DOW_STAR flags are tested
② OR logical operator causes a union of the days of month and days of week fields
③ AND logical operator causes an intersection of the days of month and days of week fields

We've seen here that the "contain the * character" test mentioned in the man page, is actually a test of whether the * character is the first character of the field.

Caution: This behavior is not mandated by POSIX, as far as I can tell. (See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html). YMMV depending on your cron implementation. I can confirm that the technique described in this answer works on cronie (a vixie-cron derivative). It is also understood by crontab.guru (example: 0 8 */32,1-7 * 1).

Some background information on this behavior is included in the FEATURES file in the vixie cron source code:

--  the dom/dow situation is odd.  '* * 1,15 * Sun' will run on the
    first and fifteenth AND every Sunday;  '* * * * Sun' will run *only*
    on Sundays;  '* * 1,15 * *' will run *only* the 1st and 15th.  this
    is why we keep 'e->dow_star' and 'e->dom_star'.  I didn't think up
    this behaviour; it's how cron has always worked but the documentation
    hasn't been very clear.  I have been told that some AT&T crons do not
    act this way and do the more reasonable thing, which is (IMHO) to "or"
    the various field-matches together.  In that sense this cron may not
    be completely similar to some AT&T crons.

Comparison

Here's a comparison of how the two approaches look in a crontab:

0 8 1-7 * *       [ "$(date '+\%u')" = "1" ] && echo "First Monday"
0 8 */32,1-7 * 1  echo "First Monday"

(The first line shows the approach of the accepted answer).

Related discussions

0

I made a general solution for this kind of problems, it works for first, second, third..... last weekday of the month.

You can use it like this:

30 06 * * Mon run-if-today 1 "Mon" && echo "First Monday"
30 06 * * Thu run-if-today 3 "Thu" && echo "Third Thursday"
30 06 * * Sun run-if-today L "Sun" && echo "Last Sunday"

The script run-if-today check for both the weekday and the desired week date range, if both match then it returns 0, otherwise 1.

Check the code here. https://github.com/xr09/cron-last-sunday

0

This usage should be most universal and avoids the locale issue.

[ `/bin/date +\%u` -eq 1 ]

the first Monday of the month at 6am would look like this in /etc/crontab

00 6 1-7 * *    root    [ `/bin/date +\%u` -eq 1 ] && /run/yourjob.sh
0
crontab 30 8 */27 * 1

At 08:30 AM, [ones] every 27 days, and on Monday (see crontab expression generator)

Day of the month is unencumbered (is *), so the either logic/exception does not apply.

0

I just ran into this and here is what I came up with:

0 11 * * 1#1 - Run at 11 am on the first Monday of every month
0 11 * * 1#2 - Run at 11 am on the second Monday of every month
0 11 * * 1#3 - Run at 11 am on the third Monday of every month

If you wanted to run on Tuesday instead:

0 11 * * 2#1 - Run at 11 am on the first Tuesday of every month
0 11 * * 2#2 - Run at 11 am on the second Tuesday of every month
0 11 * * 2#3 - Run at 11 am on the third Tuesday of every month
1
  • Is it some cron dialect? What sort of cron is it for? This fails check in vixie-cron. Mar 7, 2023 at 6:17
-1

I believe this solves the problem more elegantly:

30 8 1-7 * 1 /run/your/job.sh
4
  • 1
    Doh! : "If both fields are restricted (ie, aren't *), the command will be run when either field matches the current time."
    – symcbean
    Jul 10, 2014 at 8:35
  • Also on February (the one that ends 28th) it will not be run every 4 years.
    – igraczech
    Oct 5, 2016 at 11:00
  • "either" means any of them, not "both". this will run every day for the 1st 7 days of the month.
    – minusf
    Aug 30, 2018 at 14:42
  • 1
    This row would run a command on the first to seventh of each month, as well as on every Monday Nov 30, 2018 at 5:23
-1

I needed the same result, but wanted to use pure cron. I hope this improves on the accepted answer.

For me I needed it to run the First Monday of each month at noon.

So this should run: At Noon, in the first 7 days of the month, on a Monday: 0 12 1-7 * 1

https://crontab.guru/#0_12_1-7_*_1

1
  • from the guru: “At 12:00 on every day-of-month from 1 through 7 and on Monday.” it will run every day of the week AND on Monday.
    – Art3mix
    Sep 24, 2021 at 7:24
-2
0 9 1-7 * 1 * 

This will work for every monday of every month.

1
  • An answer has been accepted, is your answer better, different or useful on a newer version, etc.
    – mic84
    Sep 4, 2018 at 9:32

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .