Troubleshooting

Common issues, error messages, launchd gotchas, and diagnostic commands.

Common Issues

Agent not loading

Symptoms: Job shows as "Unloaded" in the dashboard after saving. launchctl list does not show the job label.

Possible causes:

  1. Plist file permissions are wrong. launchd silently ignores plist files with incorrect permissions. The file must be owned by the current user and have permissions 644 or stricter.
    # Check permissions
    ls -la ~/Library/LaunchAgents/com.launchpad.myjob.plist
    
    # Fix if needed
    chmod 644 ~/Library/LaunchAgents/com.launchpad.myjob.plist
  2. Malformed plist XML. Validate the plist with plutil:
    plutil -lint ~/Library/LaunchAgents/com.launchpad.myjob.plist
  3. Duplicate label. Another agent with the same label is already registered. Check with launchctl list | grep myjob.

Permission denied errors

Symptoms: Job loads but exits immediately with exit code 78 or 1. Stderr shows "Permission denied".

Solution: Ensure the executable has execute permissions:

chmod +x /path/to/your/script.sh

Also check that the script's shebang line is correct (e.g., #!/bin/bash or #!/usr/bin/env python3).

Schedule not firing

Symptoms: Job is loaded and enabled but never runs at the expected time.

Possible causes:

  • Mac was asleep. StartCalendarInterval does not fire while the machine is asleep. If the Mac was sleeping at the scheduled time, the job runs once when the Mac wakes up (launchd coalesces missed runs).
  • Using StartInterval and StartCalendarInterval together. This has undefined behavior. Use only one. LaunchPad prevents this at the form level, but manually-edited plists may have this issue.
  • Incorrect Weekday values. In launchd, Sunday is 0 and Saturday is 6. This is different from cron on some systems where Sunday can be 7.
  • ThrottleInterval throttling. launchd enforces a minimum 10-second interval between job launches. If your schedule would trigger more frequently, launchd silently throttles it.

Log file missing

Symptoms: Job runs but log viewer shows "No output captured".

Possible causes:

  • No StandardOutPath/StandardErrorPath in the plist. LaunchPad auto-defaults these for managed jobs, but external jobs may not have them set. Without these keys, stdout/stderr goes to /dev/null.
  • Log directory doesn't exist. LaunchPad creates ~/Library/Logs/LaunchPad/ automatically, but if the directory was deleted, log writes will fail silently.
  • Disk full. If the disk is full, log files cannot be written. Check available space with df -h.

Dashboard port conflict

Symptoms: LaunchPad fails to start with "EADDRINUSE" error.

Solution: Another process is using port 24680. Find it:

# Find the process using port 24680
lsof -i :24680

# Or change the port via environment variable
LAUNCHPAD_PORT=24681 pnpm start

launchd Gotchas

launchd has been stable for 15+ years, but it has quirks that trip up even experienced macOS users.

Domain targets (modern vs. legacy syntax)

Apple has deprecated the load/unload subcommands in favor of the domain-target syntax. Both still work, but you may see deprecation warnings.

# Legacy (still works, prints deprecation warning)
launchctl load ~/Library/LaunchAgents/com.launchpad.myjob.plist
launchctl unload ~/Library/LaunchAgents/com.launchpad.myjob.plist

# Modern (recommended)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.launchpad.myjob.plist
launchctl bootout gui/$(id -u)/com.launchpad.myjob

# Check if a job is loaded (modern)
launchctl print gui/$(id -u)/com.launchpad.myjob

Unload before reload

When editing a plist, you must unload (bootout) the job before loading (bootstrapping) the updated version. Loading a new plist without unloading the old one causes undefined behavior -- sometimes the old version keeps running, sometimes launchd returns a "service already loaded" error.

LaunchPad handles this automatically during job edits.

ProgramArguments vs. Program

The ProgramArguments key takes an array where the first element is the executable and subsequent elements are arguments. The Program key takes a single string (the executable path) with no arguments. If you use Program, you cannot pass arguments.

Common mistake

Using both Program and ProgramArguments in the same plist causes confusing behavior. Program overrides ProgramArguments[0] as the executable, but the arguments from ProgramArguments are still passed. LaunchPad always uses ProgramArguments for consistency.

StartCalendarInterval array wrapping

A single StartCalendarInterval dict works fine for one schedule. But if you want multiple schedules (e.g., weekdays at 9 AM), each day needs its own dict in an array. Forgetting the array wrapper causes only the first schedule to fire.

Environment differences from Terminal

LaunchAgents run with a minimal environment -- not your shell profile (.zshrc, .bashrc). This means:

  • PATH is minimal (/usr/bin:/bin:/usr/sbin:/sbin)
  • Homebrew paths (/opt/homebrew/bin) are not included
  • Custom environment variables from your shell are not available

Fix: Use absolute paths for executables (e.g., /opt/homebrew/bin/python3 instead of python3) or set EnvironmentVariables in the plist with a custom PATH.

Diagnostic Commands

Useful terminal commands for debugging LaunchAgent issues.

# List all loaded user LaunchAgents
launchctl list

# Get detailed info about a specific job
launchctl print gui/$(id -u)/com.launchpad.myjob

# Check if a plist is valid XML
plutil -lint ~/Library/LaunchAgents/com.launchpad.myjob.plist

# Convert plist to readable JSON for inspection
plutil -convert json -o - ~/Library/LaunchAgents/com.launchpad.myjob.plist | python3 -m json.tool

# List all plist files in user LaunchAgents directory
ls -la ~/Library/LaunchAgents/

# Check system log for launchd errors
log show --predicate 'subsystem == "com.apple.xpc.launchd"' --last 5m

# Check what port LaunchPad is running on
lsof -i :24680

# View LaunchPad server logs
tail -f ~/Library/Logs/LaunchPad/server.log

# Check SQLite database integrity
sqlite3 ~/.launchpad/launchpad.db "PRAGMA integrity_check;"

# Count total jobs in database
sqlite3 ~/.launchpad/launchpad.db "SELECT COUNT(*) FROM jobs WHERE archived_at IS NULL;"

Error Messages

ErrorCauseFix
EADDRINUSEPort 24680 is in useKill the existing process or change the port
DUPLICATE_LABELA job with this label already existsChoose a different label or edit the existing job
PLIST_WRITE_ERRORCannot write to ~/Library/LaunchAgents/Check directory permissions and disk space
LAUNCHCTL_ERRORlaunchctl command failedCheck stderr output in the dashboard for details
SCHEDULE_CONFLICTBoth interval and calendar schedule setUse only StartInterval or StartCalendarInterval, not both
READ_ONLY_JOBAttempted to modify a system/external jobOnly dashboard-created jobs can be edited
AUTH_REQUIREDTouch ID verification neededAuthenticate with Touch ID or disable in Settings
Exit code 78Configuration error in plistValidate plist with plutil -lint
Exit code 126Permission denied on executablechmod +x the script
Exit code 127Command not foundUse absolute path to executable

Frequently Asked Questions

No. LaunchPad only manages user-level LaunchAgents. LaunchDaemons run as root and require elevated privileges that a localhost web app should not have. System-level agents in /Library/LaunchAgents/ are displayed as read-only for visibility.
Absolutely not. LaunchPad is 100% local. The server binds exclusively to 127.0.0.1 and is inaccessible from the network. There are no cloud services, no telemetry, no analytics, no external API calls. All data stays in a local SQLite file at ~/.launchpad/launchpad.db.
Set the LAUNCHPAD_PORT environment variable before starting the server, or change it in Settings within the dashboard. If you're using the auto-start LaunchAgent, update the EnvironmentVariables section of ~/Library/LaunchAgents/com.launchpad.server.plist.
Execution logs are automatically pruned based on the log retention setting (default: 90 days). A daily background task deletes execution records older than the threshold. You can adjust retention in Settings or manually clear logs for specific jobs.
Yes. Go to Settings and toggle off "Require Touch ID for destructive operations". This removes the biometric confirmation step for deleting, editing, and enabling/disabling jobs. Note that this lowers the safety barrier against accidental destructive actions.
LaunchAgents run in a minimal environment without your shell profile. The most common issue is that PATH doesn't include Homebrew, pyenv, nvm, or other tools. Use absolute paths to executables (e.g., /opt/homebrew/bin/python3) and set any needed environment variables explicitly in the LaunchPad job configuration.
  1. Unload the LaunchPad server: launchctl bootout gui/$(id -u)/com.launchpad.server
  2. Unload the menubar: launchctl bootout gui/$(id -u)/com.launchpad.menubar
  3. Remove LaunchPad plists: rm ~/Library/LaunchAgents/com.launchpad.server.plist ~/Library/LaunchAgents/com.launchpad.menubar.plist
  4. Remove the database: rm -rf ~/.launchpad/
  5. Remove logs: rm -rf ~/Library/Logs/LaunchPad/
  6. If installed via Homebrew: brew uninstall launchpad

Your managed LaunchAgent plists (the jobs you created) remain in ~/Library/LaunchAgents/ and continue to work independently of LaunchPad.