intermediate 12 min read

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)

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

bash
crontab -e

Add a job:

bash
*/5 * * * * /usr/local/bin/python3 /Users/me/scripts/task.py >> /tmp/cron.log 2>&1

Important 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
<?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:

bash
launchctl load ~/Library/LaunchAgents/com.myapp.backup.plist

Unload (stop):

bash
launchctl unload ~/Library/LaunchAgents/com.myapp.backup.plist

Modern syntax (macOS 13+):

bash
# 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.backup

Common Schedules

crontab expressions (same as Linux):

  • */5 * * * * — Every 5 minutes
  • 0 * * * * — Every hour
  • 0 9 * * 1-5 — Weekdays at 9 AM
  • 0 0 * * * — Daily at midnight

launchd StartCalendarInterval:

xml
<!-- 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):

xml
<!-- 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:

bash
# 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.py

launchd debugging:

bash
# 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.err

Common issues: plist syntax errors (validate with plutil -lint file.plist), missing Full Disk Access, wrong file permissions.

Frequently Asked Questions