In the Darwin operating system underlying macOS, process supervision, background daemon lifecycle management, and scheduled task execution are unified under a single parent process: launchd. Operating as PID 1, launchd supersedes traditional UNIX subsystems including init, cron, inetd, and atrun. By handling task dispatching through reactive socket activation, filesystem event triggers, and time-based calendar scheduling, launchd guarantees that system resources remain conserved until an exact trigger condition is satisfied.
For system administrators, DevOps engineers, and advanced macOS users, configuring background automations requires mastering the structural differences between Launch Daemons (which execute system-wide with elevated root privileges independently of user logins) and Launch Agents (which run within the security context of an active logged-in graphical user). Both are declared using declarative Property List (.plist) XML manifests managed via the modern launchctl control utility.
Architectural Overview: Launch Daemons vs. Launch Agents
macOS defines strict execution domains and filesystem locations for launchd configuration plists:
| Descriptor Type | Configuration Directory | Execution Context | Typical Use Case |
|---|---|---|---|
| User Agent | ~/Library/LaunchAgents |
Logged-in user GUI session (console domain) | User backup sync scripts, menu bar helpers, personal automated workflows. |
| Global Agent | /Library/LaunchAgents |
Any user GUI session upon login | Enterprise endpoint management utilities, company notification prompts. |
| System Daemon | /Library/LaunchDaemons |
Root / dedicated service account (system domain) | Network listeners, system log rotations, automated hardware maintenance. |
| Apple System Jobs | /System/Library/Launch* |
Root / Apple sandbox (SIP Protected) | Core OS daemons and agents. Read-only under System Integrity Protection (SIP). |
Property List Schema and XML Keys Reference
A launchd job is defined using an XML Property List. The most critical configuration keys include:
| Property Key | Type | Operational Role |
|---|---|---|
Label |
String | Unique reverse-DNS identifier for the job (e.g. com.enterprise.backup.sync). |
ProgramArguments |
Array of Strings | Command binary path followed by runtime arguments (must use absolute paths). |
StartInterval |
Integer | Schedules recurring execution every N seconds (equivalent to simple repeating timers). |
StartCalendarInterval |
Dictionary or Array | Cron-style scheduling defining specific Minute, Hour, Day, or Weekday. |
WatchPaths |
Array of Strings | Triggers execution whenever files or directories at specified paths are modified. |
RunAtLoad |
Boolean | Executes the job immediately when registered into launchd, in addition to schedules. |
KeepAlive |
Boolean or Dictionary | Restarts the process automatically if it crashes or exits unexpectedly. |
StandardOutPath / StandardErrorPath |
String | Redirects stdout and stderr streams to designated log files. |
Creating a Scheduled User Launch Agent
Follow these steps to construct, validate, and register a user-level launch agent that executes a Python maintenance script daily at 02:30 AM.
Step 1: Construct the Property List XML Manifest
Create the directory if it does not already exist, and create the file ~/Library/LaunchAgents/com.user.maintenance.plist:
mkdir -p ~/Library/LaunchAgents
nano ~/Library/LaunchAgents/com.user.maintenance.plist
Insert the following configuration:
<?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.maintenance</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/admin/scripts/maintenance_sync.py</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>30</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/admin/Library/Logs/maintenance.log</string>
<key>StandardErrorPath</key>
<string>/Users/admin/Library/Logs/maintenance_err.log</string>
</dict>
</plist>
Step 2: Enforce Correct Permissions and Lint the Manifest
launchd will refuse to load configuration files with insecure write permissions or malformed XML structures:
# Enforce standard file permissions (readable by user, non-writable by others)
chmod 644 ~/Library/LaunchAgents/com.user.maintenance.plist
# Lint the XML property list for syntax validity
plutil -lint ~/Library/LaunchAgents/com.user.maintenance.plist
If the command outputs OK, the manifest syntax is valid.
Managing Services with Modern launchctl Commands
Apple has deprecated legacy subcommands such as launchctl load and launchctl unload. Modern macOS releases require the domain-aware bootstrap and bootout primitives.
Step 1: Identify Your Target Execution Domain
Execution domains in launchctl map to specific runtime contexts:
gui/<UID>: User graphical agent domain (runid -uto find your UID, e.g.501).system: Root system daemon domain.
Step 2: Register and Trigger the Service
Execute the following commands in Terminal:
# Register the service into the user graphical domain
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.maintenance.plist
# Verify that the service is registered
launchctl print gui/$(id -u)/com.user.maintenance
# Manually trigger a run out-of-schedule for verification
launchctl kickstart -k gui/$(id -u)/com.user.maintenance
# Check the trailing application logs
tail -n 20 ~/Library/Logs/maintenance.log
Step 3: Unregistering or Disabling the Service
When decommissioning a job:
# Boot the service out of the launchd domain
launchctl bootout gui/$(id -u)/com.user.maintenance
# Alternatively, boot out directly by targeting the plist file
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.maintenance.plist
Creating a Root System Daemon in /Library/LaunchDaemons
For operations requiring system-level access (such as log pruning or hardware interface monitoring), deploy a system daemon.
Step 1: Write the Daemon Plist
Create /Library/LaunchDaemons/com.enterprise.sysaudit.plist owned strictly by root:wheel:
sudo nano /Library/LaunchDaemons/com.enterprise.sysaudit.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.enterprise.sysaudit</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/audit_engine</string>
<string>--full-check</string>
</array>
<key>StartInterval</key>
<integer>3600</integer>
<key>StandardOutPath</key>
<string>/var/log/sysaudit.log</string>
<key>StandardErrorPath</key>
<string>/var/log/sysaudit_err.log</string>
</dict>
</plist>
Step 2: Bootstrap into the System Domain
# Strictly enforce root:wheel ownership and permissions
sudo chown root:wheel /Library/LaunchDaemons/com.enterprise.sysaudit.plist
sudo chmod 644 /Library/LaunchDaemons/com.enterprise.sysaudit.plist
# Bootstrap the daemon into the system domain
sudo launchctl bootstrap system /Library/LaunchDaemons/com.enterprise.sysaudit.plist
# Verify daemon status
sudo launchctl print system/com.enterprise.sysaudit
Troubleshooting launchd Failures on macOS
Consult this diagnostic reference table to debug service registration rejections and execution errors:
| Error / Symptom | Underlying Root Cause | Corrective Action |
|---|---|---|
Bootstrap failed: 5: Input/output error |
Property list contains syntax errors, or permissions violate security requirements (e.g. group-writable). | Run plutil -lint <path> to detect malformed XML. Ensure file permissions are set to 644 and owned by the respective user (or root:wheel for daemons). |
Job exits repeatedly with status 78 (EX_CONFIG). |
Target binary path or script does not exist, lacks execute permissions (chmod +x), or contains an invalid shebang. |
Verify absolute paths in ProgramArguments. Ensure the referenced executable has chmod +x and all script interpreter paths are absolute (e.g. /usr/bin/python3). |
| Job executes but fails with “Operation not permitted”. | macOS Transparency, Consent, and Control (TCC) restrictions prevent the background process from accessing Protected Data (e.g. Desktop, Documents). | Grant Full Disk Access to the binary or wrapper script under System Settings > Privacy & Security > Full Disk Access. |
launchctl print returns state = not running after schedule passes. |
Machine was asleep during the scheduled StartCalendarInterval, and WakeSystem was not configured. |
launchd jobs scheduled via StartCalendarInterval run once the machine wakes. To force the hardware to wake for scheduled runs, configure power management using pmset repeat wakeorpoweron. |
Service cannot be loaded into requested domain. |
Attempting to bootstrap a LaunchDaemon into a gui/ domain or a LaunchAgent into the system domain. |
Match domains strictly: LaunchDaemons belong to system; LaunchAgents belong to gui/<UID>. |
Mastering launchd property list configurations and modern launchctl domain commands empowers administrators to construct robust, high-performance background services that integrate flawlessly with macOS security architectures.