Community Tutorials macOS How to Manage launchd Daemons on macOS with launchctl
How to Manage launchd Daemons on macOS with launchctl
MACOS

How to Manage launchd Daemons on macOS with launchctl

SKYLINE Knowledge Base
Photo by Carl Heyerdahl on Unsplash

A field-tested, step-by-step guide. How to Manage launchd Daemons on macOS with launchctl — prerequisites, the actual commands, verification, and links to related macOS topics.

macOS uses launchd instead of systemd or SysV init. Every long-running service — Apple's, third-party, and yours — is a .plist managed by launchctl. This guide covers loading, unloading, listing, and writing your own.

Prerequisites

  • macOS 12 Monterey or newer.
  • A script or binary you want to run as a daemon — we will use /usr/local/bin/skyline-collector as the example.

Step 1: Understand the plist locations

| Path | Scope | | --- | --- | | /System/Library/LaunchDaemons/ | Apple system daemons. Don't touch. | | /Library/LaunchDaemons/ | Third-party, runs as root, on boot. | | /Library/LaunchAgents/ | Third-party, runs per-user, on user login. | | ~/Library/LaunchAgents/ | Personal, current user only. |

The two you actually write: /Library/LaunchDaemons/ for system services, ~/Library/LaunchAgents/ for personal jobs.

Step 2: Write a LaunchDaemon plist

/Library/LaunchDaemons/com.skyline.collector.plist:

<?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.skyline.collector</string>

  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/skyline-collector</string>
    <string>--config</string>
    <string>/etc/skyline/collector.yaml</string>
  </array>

  <key>RunAtLoad</key>
  <true/>

  <key>KeepAlive</key>
  <true/>

  <key>StandardOutPath</key>
  <string>/var/log/skyline-collector.out.log</string>

  <key>StandardErrorPath</key>
  <string>/var/log/skyline-collector.err.log</string>

  <key>UserName</key>
  <string>skyline</string>
</dict>
</plist>

LaunchDaemons must be root-owned with 644 perms:

sudo chown root:wheel /Library/LaunchDaemons/com.skyline.collector.plist
sudo chmod 644 /Library/LaunchDaemons/com.skyline.collector.plist

Step 3: Load and start

The modern syntax uses bootstrap + kickstart (macOS 10.10+):

sudo launchctl bootstrap system /Library/LaunchDaemons/com.skyline.collector.plist
sudo launchctl kickstart -k system/com.skyline.collector

To unload:

sudo launchctl bootout system /Library/LaunchDaemons/com.skyline.collector.plist

Step 4: Inspect status

# List one
sudo launchctl print system/com.skyline.collector | head -30

# List everything
sudo launchctl list | grep -v com.apple | head

# Find by partial name
sudo launchctl list | grep skyline

Look at the third column of launchctl list — it is the last exit code. 0 is healthy; anything else means the daemon crashed.

Step 5: Trigger on schedule (the cron equivalent)

StartCalendarInterval runs at a specific time:

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

StartInterval runs every N seconds:

<key>StartInterval</key>
<integer>300</integer>

WatchPaths triggers when a file changes:

<key>WatchPaths</key>
<array>
  <string>/var/spool/skyline/queue/</string>
</array>

Step 6: Per-user LaunchAgents

For personal jobs (auto-start an SSH tunnel when you log in):

mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/com.you.tunnel.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
  <key>Label</key><string>com.you.tunnel</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/ssh</string>
    <string>-N</string>
    <string>-L</string><string>5432:db.corp.example.sa:5432</string>
    <string>jumphost</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict></plist>
EOF

launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.you.tunnel.plist

Verify

sudo launchctl print system/com.skyline.collector
sudo log show --predicate 'subsystem == "com.skyline.collector"' --last 1h
tail -f /var/log/skyline-collector.err.log

Conclusion

launchd is more expressive than cron — calendar, interval, file-watch, plus auto-restart with crash-loop detection. The plist XML is verbose, but launchctl bootstrap/bootout/kickstart are the only three verbs you need day-to-day.

Next steps

SKYLINE Engineering

@skyline

The engineering team at SKYLINE Industrial Solutions. We publish field-tested guides drawn from real KSA and GCC deployments.

See author profile
SKYLINE engineering services

Need this implemented for you?

Reading is free — building it right takes a team. SKYLINE engineers ship macOS for Aramco vendors, banks, hospitals and government agencies across Saudi Arabia. Talk to us before you start.

Aramco Approved Contractor ISO 9001 · ISO 27001 SAMA CSF aligned NCA ECC ready 247+ KSA clients

Comments

0 total · 0 threads
Be the first to leave a comment.