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

On traditional UNIX and Linux systems, background automation, task scheduling, and socket activation have historically been distributed across disparate subsystems: cron for periodic tasks, init or systemd for system daemons, and inetd for on-demand socket listening. In macOS, Apple unified all background process orchestration under a single, high-performance init process: launchd (operating as PID 1).

Whether automating scheduled database backups, running continuous telemetry collectors, or ensuring an enterprise VPN daemon auto-restarts upon failure, system engineers must interface directly with launchd. By authoring declarative Property List (.plist) XML manifests and managing their lifecycle with the modern launchctl domain engine, administrators can orchestrate robust background services across both system-wide and user-specific contexts in macOS.

Architectural Overview: LaunchDaemons vs. LaunchAgents

The launchd architecture divides services into two distinct execution domains depending on security privileges and user session states:

Service Type Standard Directory Path User & Security Context Execution Lifecycle
System Daemons (LaunchDaemons) /Library/LaunchDaemons/ Runs as root (or a designated system service user). No access to GUI window server. Starts at system boot; executes continuously regardless of whether any user is logged in.
Global User Agents (LaunchAgents) /Library/LaunchAgents/ Runs in the context of each logged-in user. Has access to the GUI window server. Starts automatically when any user logs in; terminates when the user logs out.
Per-User Agents (LaunchAgents) ~/Library/LaunchAgents/ Runs strictly under the specific user’s UID. Accesses user files and local GUI environment. Starts at user login; governed entirely by the individual account.

Structure of a launchd Property List (plist)

Unlike crontab one-liners, launchd jobs are defined using structured Apple XML Property Lists. Below is an architectural breakdown of the core keys:

Property Key Value Type Operational Role
Label String Unique reverse-DNS identifier (e.g. com.company.backup-sync) used by launchctl to track the job.
ProgramArguments Array of Strings Absolute paths to the executable binary and its positional arguments (e.g. /usr/local/bin/backup.sh).
RunAtLoad Boolean If set to true, executes the task immediately when the job is loaded into the domain.
KeepAlive Boolean / Dict Monitors process state; automatically respawns the binary if it exits or crashes.
StartInterval Integer Runs the job repeatedly every N seconds (e.g. 3600 for hourly execution).
StartCalendarInterval Dictionary Cron-like calendar scheduling (e.g. specifying exact Hour, Minute, or Weekday).
StandardOutPath / StandardErrorPath String (Path) Redirects STDOUT and STDERR output to dedicated filesystem log files for auditing.

Step-by-Step Procedure: Creating and Authoring a launchd User Agent

Follow this exact technical workflow to create an automated hourly file synchronisation and health check agent.

Step 1: Create the User Agents Directory

Ensure the local user agent directory exists in your home folder:

mkdir -p ~/Library/LaunchAgents
mkdir -p ~/Library/Logs/EnterpriseAutomation

Step 2: Author the Property List (.plist)

Create and edit ~/Library/LaunchAgents/com.enterprise.healthsync.plist:

nano ~/Library/LaunchAgents/com.enterprise.healthsync.plist

Insert the following XML specification:

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

    <key>ProgramArguments</key>
    <array>
        <string>/bin/zsh</string>
        <string>-c</string>
        <string>/usr/bin/logger -t EnterpriseSync "Running hourly health verification" && /usr/bin/tmutil status</string>
    </array>

    <key>StartInterval</key>
    <integer>3600</integer>

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

    <key>StandardOutPath</key>
    <string>/Users/Shared/EnterpriseAutomation/stdout.log</string>

    <key>StandardErrorPath</key>
    <string>/Users/Shared/EnterpriseAutomation/stderr.log</string>
</dict>
</plist>

Step 3: Enforce Strict POSIX File Permissions

launchd strictly refuses to execute manifests that possess insecure permissions (such as group-writeable flags):

# For user agents: owned by user, permissions 644
chmod 644 ~/Library/LaunchAgents/com.enterprise.healthsync.plist

Note for system daemons under /Library/LaunchDaemons/: Files must be owned by root:wheel with chmod 644:

sudo chown root:wheel /Library/LaunchDaemons/com.enterprise.systemservice.plist
sudo chmod 644 /Library/LaunchDaemons/com.enterprise.systemservice.plist

Managing Services with Modern launchctl Syntax

While legacy tutorials recommend launchctl load and unload, Apple has deprecated those commands in modern macOS releases in favour of the domain-based bootstrap and bootout subcommands.

Step 1: Identify Your User Domain Target

Query your active user GUI domain UID:

id -u
# Typically outputs: 501

The GUI domain target is formatted as gui/<UID> (e.g. gui/501).

Step 2: Bootstrap (Load and Activate) the Agent

launchctl bootstrap gui/501 ~/Library/LaunchAgents/com.enterprise.healthsync.plist

Step 3: Verify Active Service Status and Inspection

Query launchd to inspect execution status, exit codes, and memory state:

launchctl print gui/501/com.enterprise.healthsync

Review the output to confirm:

  • state = running or state = waiting
  • last exit code = 0
  • runs = 1

Step 4: Manually Trigger Job Execution (Kickstart)

To run the job immediately without waiting for the timer interval:

launchctl kickstart -k gui/501/com.enterprise.healthsync

The -k flag gracefully kills any existing instance before starting a fresh run.

Step 5: Bootout (Unload and Terminate) the Service

To disable the job and stop its schedule:

launchctl bootout gui/501/com.enterprise.healthsync

Scheduling Jobs via Calendar Syntax (Replacing Cron)

To execute a script at a precise time—such as every Monday morning at 03:30 AM—replace StartInterval with StartCalendarInterval in the plist:

<key>StartCalendarInterval</key>
<dict>
    <key>Weekday</key>
    <integer>1</integer> <!-- 1 = Monday -->
    <key>Hour</key>
    <integer>3</integer>
    <key>Minute</key>
    <integer>30</integer>
</dict>

If the Mac is sleeping when the scheduled minute arrives, launchd automatically executes the job immediately upon system wake, eliminating missed runs.

Troubleshooting launchd Operational Failures

Consult this diagnostic reference table to debug service launch failures and unexpected crashes:

Issue / Exit Code Underlying Root Cause Remediation Procedure
Bootstrap failed: 5: Input/output error File permissions are incorrect, file is not owned by root (for daemons), or XML syntax is malformed. Validate XML syntax with plutil -lint ~/Library/LaunchAgents/<file>.plist. Ensure permissions are set strictly to 644.
Service repeatedly restarts in a crash loop (Exit code 78 or 127). Binary path in ProgramArguments is relative, or required environment variables (like PATH) are missing. Always specify full absolute paths (e.g. /usr/local/bin/node). Define custom paths using the <key>EnvironmentVariables</key> dictionary.
launchctl print reports: service was throttled. Job crashed repeatedly within a short window, causing launchd to apply exponential backoff throttling (10s to 120s). Fix the underlying script error. Run launchctl kickstart -k to reset the throttling timer.
Job fails to write logs to disk (Permission Denied). Target log directory does not exist or user account lacks write permissions. Ensure destination paths in StandardOutPath and StandardErrorPath exist and are writeable by the service user.

Mastering launchd Property List architecture and the modern launchctl domain commands provides macOS systems administrators with enterprise-grade process management, guaranteeing precise, resilient background task automation across the operating system.

Get the best tech tips delivered straight to your inbox.

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