How to Set Up Cron Jobs on macOS (crontab & launchd)
Learn how to schedule tasks on macOS using crontab and launchd. Covers crontab setup, Full Disk Access, Launch Agents, launchctl, and comparison of both approaches.
Prerequisites
- macOS 13+ (Ventura or later)
- Terminal access
- Admin privileges (for launchd)
In this guide
Cron on macOS
macOS still supports crontab, but Apple recommends launchd as the preferred scheduling mechanism. Both work, but launchd integrates better with macOS power management, sleep/wake cycles, and system events.
For simple tasks, crontab works fine. For tasks that need to survive sleep, run at login, or have macOS-specific triggers, use launchd.
Quick Start with crontab
crontab -eAdd a job:
*/5 * * * * /usr/local/bin/python3 /Users/me/scripts/task.py >> /tmp/cron.log 2>&1Important on macOS: You'll need to grant Full Disk Access to cron (or the shell) if your script accesses protected directories like Desktop, Documents, or Downloads.
Full Disk Access Permission
macOS privacy protections restrict cron from accessing many directories. To fix this:
1. Open System Settings > Privacy & Security > Full Disk Access 2. Click the + button 3. Press Cmd+Shift+G and navigate to /usr/sbin/cron 4. Add it to the list
Without this, cron jobs that access protected directories will silently fail or produce permission errors.
launchd: The macOS Way
launchd uses XML property list (plist) files to define scheduled tasks. Tasks are called Launch Agents (per-user) or Launch Daemons (system-wide).
Create a Launch Agent:
<?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.myapp.backup</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/python3</string>
<string>/Users/me/scripts/backup.py</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/tmp/backup.log</string>
<key>StandardErrorPath</key>
<string>/tmp/backup.err</string>
</dict>
</plist>Save as ~/Library/LaunchAgents/com.myapp.backup.plist.
launchctl Commands
Load (start) a Launch Agent:
launchctl load ~/Library/LaunchAgents/com.myapp.backup.plistUnload (stop):
launchctl unload ~/Library/LaunchAgents/com.myapp.backup.plistModern syntax (macOS 13+):
# Bootstrap (load)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.myapp.backup.plist
# Bootout (unload)
launchctl bootout gui/$(id -u)/com.myapp.backup
# List loaded services
launchctl list | grep myapp
# Check status
launchctl print gui/$(id -u)/com.myapp.backupCommon Schedules
crontab expressions (same as Linux):
*/5 * * * *— Every 5 minutes0 * * * *— Every hour0 9 * * 1-5— Weekdays at 9 AM0 0 * * *— Daily at midnight
launchd StartCalendarInterval:
<!-- Every hour -->
<key>StartCalendarInterval</key>
<dict>
<key>Minute</key>
<integer>0</integer>
</dict>
<!-- Weekdays at 9 AM -->
<key>StartCalendarInterval</key>
<array>
<dict><key>Weekday</key><integer>1</integer><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Weekday</key><integer>2</integer><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Weekday</key><integer>3</integer><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Weekday</key><integer>4</integer><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
<dict><key>Weekday</key><integer>5</integer><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
</array>launchd StartInterval (seconds-based):
<!-- Every 5 minutes (300 seconds) -->
<key>StartInterval</key>
<integer>300</integer>Every weekday at 9:00 AM
Next runs (UTC):
Mon, May 18, 2026 09:00
Tue, May 19, 2026 09:00
Wed, May 20, 2026 09:00
crontab vs launchd
| Feature | crontab | launchd | |---------|---------|---------| | Syntax | 5-field cron | XML plist | | Sleep handling | Skips missed runs | Catches up after wake | | Run at login | No | Yes (RunAtLoad) | | Logging | Manual redirect | Built-in (StandardOutPath) | | macOS integration | Limited | Full (disk access, energy) | | Complexity | Simple | More setup |
Recommendation: Use crontab for simple recurring tasks. Use launchd for tasks that need macOS integration (surviving sleep, running at login, energy-aware scheduling).
Debugging
crontab debugging:
# Check if cron is running
sudo launchctl list | grep cron
# View cron log
log show --predicate 'process == "cron"' --last 1h
# Test your command manually first
/usr/local/bin/python3 /Users/me/scripts/task.pylaunchd debugging:
# Check job status
launchctl list | grep com.myapp
# View detailed info
launchctl print gui/$(id -u)/com.myapp.backup
# Check logs
cat /tmp/backup.log
cat /tmp/backup.errCommon issues: plist syntax errors (validate with plutil -lint file.plist), missing Full Disk Access, wrong file permissions.