This is a small template for the common macOS pattern:
- run a task every night at a fixed local time
- if the Mac was asleep, run on wake
- if the Mac was off and later boots/logs in, run once as catch-up
- avoid custom hourly cron polling
Three files:
run-daily-catchup.sh- the generic runner
- tracks the last successful daily window
- coalesces missed days into a single catch-up execution
install-daily-catchup.sh- writes a user LaunchAgent plist
- bootstraps it with
launchctl
README.md- this usage note
The LaunchAgent uses:
StartCalendarInterval- tries to run at the desired hour/minute every day
RunAtLoad- runs when the agent is loaded again, such as after login/boot
The runner script adds the missing piece:
- it computes the current "due window"
- if the machine comes back before today's scheduled time, the due window is yesterday
- if the machine comes back after today's scheduled time, the due window is today
- if that due window already succeeded, it exits
- otherwise it runs the payload and records success
That means:
- sleep at 22:00, scheduled for 23:00, wake at 08:00
- it runs at wake
- machine off at 22:00, scheduled for 23:00, login at 08:00
RunAtLoadruns the agent- the runner sees that yesterday never completed
- it runs once as catch-up
First make your real payload script executable. The payload should do the actual work and exit:
0on success- non-zero on failure
Then install the agent:
/Users/breno/Documents/code/PROJECTS/hackathon/arc2-2026/templates/launchd-daily-catchup/install-daily-catchup.sh \
--label com.example.nightly-report \
--hour 23 \
--minute 0 \
--tz America/Sao_Paulo \
--payload /Users/me/bin/nightly-report.sh \
--state-dir /Users/me/.local/state/nightly-report \
--logs-dir /Users/me/Library/Logs/nightly-reportInspect:
launchctl print gui/$(id -u)/com.example.nightly-reportForce a run now:
launchctl kickstart -k gui/$(id -u)/com.example.nightly-reportRemove:
launchctl bootout gui/$(id -u)/com.example.nightly-report
rm ~/Library/LaunchAgents/com.example.nightly-report.plist- This is a per-user
LaunchAgent, so it catches up when your user session loads again. - It does not wake a powered-off machine by itself.
- If your payload depends on internet, the payload should handle "network still unavailable" and exit non-zero so it retries on the next trigger.
- It intentionally coalesces multiple missed days into one execution, which is usually what you want for "nightly maintenance" style tasks.