Cron first Monday of the month
The cron expression 0 9 1-7 * * runs a job at 09:00, on days 1 through 7 of the month; a date guard in the command limits it to the first Monday of the month. The guard exits early on the other dates, so give the job's heartbeat monitor a period that matches the real schedule, not the cron expression.
| minute | hour | day of month | month | day of week |
|---|---|---|---|---|
| 0 | 9 | 1-7 | * | * |
Next runs (UTC)
- Sun 2026-11-01 09:00 UTC (+00:00) · in 19d 13h
- Mon 2026-11-02 09:00 UTC (+00:00) · in 20d 13h
- Tue 2026-11-03 09:00 UTC (+00:00) · in 21d 13h
- Wed 2026-11-04 09:00 UTC (+00:00) · in 22d 13h
- Thu 2026-11-05 09:00 UTC (+00:00) · in 23d 13h
- Fri 2026-11-06 09:00 UTC (+00:00) · in 24d 13h
- Sat 2026-11-07 09:00 UTC (+00:00) · in 25d 13h
- Tue 2026-12-01 09:00 UTC (+00:00) · in 49d 13h
- Wed 2026-12-02 09:00 UTC (+00:00) · in 50d 13h
- Thu 2026-12-03 09:00 UTC (+00:00) · in 51d 13h
These are the times cron wakes up; the guard in the command then decides whether the job actually runs.
Notes for this schedule
0 9 1-7 * 1 is wrong: when both day fields are set, cron runs if either matches, so it fires on days 1-7 and on every Monday.date +\%u is 1 for Monday). systemd timers use AND semantics, so Mon *-*-01..07 works directly.if ...; then job; fi && curl ..., the ping goes out on all seven wake-ups, so the heartbeat can use the same cron expression, and a failed first-Monday run still skips the ping and alerts you.Examples
0 9 1-7 * * if [ "$(date +\%u)" = 1 ]; then /usr/local/bin/my-job.sh; fi && curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/<your-token>
# /etc/systemd/system/my-job.timer [Unit] Description=Run my-job (first monday of the month) [Timer] OnCalendar=Mon *-*-01..07 09:00:00 Persistent=true [Install] WantedBy=timers.target
# /etc/systemd/system/my-job.service [Unit] Description=my-job [Service] Type=oneshot ExecStart=/usr/local/bin/my-job.sh # runs only if ExecStart succeeded ExecStartPost=/usr/bin/curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/<your-token>
Check the calendar spec with systemd-analyze calendar "Mon *-*-01..07 09:00:00", then systemctl enable --now my-job.timer. Persistent=true runs a missed job at boot.
apiVersion: batch/v1
kind: CronJob
metadata:
name: my-job
spec:
schedule: "0 9 1-7 * *"
timeZone: "Etc/UTC" # Kubernetes 1.27+; otherwise the controller's timezone
concurrencyPolicy: Forbid # don't start a new run while one is active
startingDeadlineSeconds: 300 # still start if the controller was briefly down
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: job
image: your-image:tag
command: ["/bin/sh", "-c", "if [ \"$(date +%u)\" = 1 ]; then /app/run-job.sh; fi && curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/<your-token>"]
on:
schedule:
- cron: "0 9 1-7 * *" # always evaluated in UTC
workflow_dispatch: {} # lets you run it by hand too
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: "if [ \"$(date +%u)\" = 1 ]; then ./run-job.sh; fi"
- name: Tell Vigil the job ran
if: success()
run: curl -fsS -m 10 --retry 3 -o /dev/null https://puzaricloud.in/ping/${{ secrets.VIGIL_PING_TOKEN }}
GitHub runs schedules in UTC, can delay them at busy times, and disables scheduled workflows in public repos after 60 days without activity.
Common mistakes
timedatectl or date. cronie (RHEL, Fedora, Arch) accepts CRON_TZ=Asia/Kolkata at the top of a crontab; Debian/Ubuntu cron does not.0 0 13 * 5 runs on every 13th and every Friday, not only on Friday the 13th.% is turned into a newline (everything after it goes to the command's stdin). Write date +\%F, not date +%F.PATH (often just /usr/bin:/bin), no login shell and no .bashrc. Use absolute paths and set variables at the top of the crontab.How to get alerted when it doesn't run
A cron job that stops running looks exactly like one that's working: no errors, no output. The reliable fix is a heartbeat (dead man's switch): the job pings a URL each time it finishes, and the monitor alerts you when a ping is late.
- Create a heartbeat in Vigil and choose cron schedule: 0 9 1-7 * *, timezone of your server.
- Set a grace period of about 60 minutes: long enough for a normal run to finish, short enough that you hear about it quickly.
- Add the ping to the end of the command (above). && means only successful runs ping; use /ping/<token>/$? to report the exit code, and /start to catch jobs that hang.
With the native systemd timer the ping only arrives on the first Monday (28 or 35 days apart), so give that heartbeat a fixed period of 31 days and a 5-day grace instead of the cron expression.
Vigil tells you when a run is missed
Vigil heartbeats understand cron expressions and timezones, so they know exactly when each run is due. Missed, failed or stuck runs alert you on email, Slack, Telegram, Discord or a webhook, and the history shows every run's duration.