---
title: "macOS launchd as Cron Replacement with RunCatchUpIfMissed"
aliases: [launchd-cron-replacement, launchd-runcatchupmissed, macos-launchd-agent]
tags: [macos, launchd, cron, scheduling, plist]
sources:
  - "daily/2026-05-07.md"
created: 2026-05-07
updated: 2026-05-08
---

# macOS launchd as Cron Replacement with RunCatchUpIfMissed

## Why launchd Instead of Cron

macOS cron silently drops jobs when the machine is asleep at trigger time. launchd with `RunCatchUpIfMissed=true` fires the job **on wake** if the scheduled window was missed. This is the correct scheduling mechanism for MacBooks.

See: [[wiki/concepts/macos-cron-sleep-missed-jobs]]

## Plist Structure

Create `~/Library/LaunchAgents/com.user.myscript.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.sync-repos</string>

    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/aimpress/scripts/sync_to_forgejo.sh</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>2</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>

    <key>RunCatchUpIfMissed</key>
    <true/>

    <key>StandardOutPath</key>
    <string>/tmp/sync-repos.log</string>

    <key>StandardErrorPath</key>
    <string>/tmp/sync-repos-err.log</string>
</dict>
</plist>
```

## launchctl Commands

```bash
# Load (activate) the agent
launchctl load ~/Library/LaunchAgents/com.user.sync-repos.plist

# Unload (deactivate)
launchctl unload ~/Library/LaunchAgents/com.user.sync-repos.plist

# Run immediately (for testing)
launchctl start com.user.sync-repos

# Check status
launchctl list | grep com.user.sync-repos

# Remove old cron entry first
crontab -l           # list current
crontab -e           # edit and delete the old line
```

## RunCatchUpIfMissed Behavior

When `RunCatchUpIfMissed=true`:
- If the trigger time was missed (machine asleep), the job fires **once** on next wake
- It does not fire multiple times to "catch up" — just once for the most recent missed interval

## Key Notes

- Plist files must be in `~/Library/LaunchAgents/` (user-level) or `/Library/LaunchDaemons/` (system-level)
- Agents in `~/Library/LaunchAgents/` only run while the user is logged in
- `StartCalendarInterval` is the equivalent of cron time expressions
- Always use absolute paths in `ProgramArguments` — shell PATH is not inherited

## Related Concepts

- [[wiki/concepts/macos-cron-sleep-missed-jobs]] — why cron fails on macOS laptops
- [[wiki/concepts/macos-python-version-hooks]] — macOS path gotchas in scripts

## Sources

- [[daily/2026-05-07.md]] — Replaced cron job for sync_to_forgejo.sh with launchd plist using RunCatchUpIfMissed=true; cleared crontab entry; confirmed on-wake catch-up behavior
