How to Configure launchd Daemons, User Agents, and Property List Scheduling in macOS

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 (run id -u to 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.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.