Day 17 of 29 · Tuesday · Learning

LaunchAgents & launchd — how your cron jobs actually runConcept

Your scheduled jobs are run by macOS's launchd, configured by little XML files called LaunchAgents. Today you'll learn to read one, list what's actually loaded, and spot the silent-unload failure mode that just cost you three days of plan-day runs.

Catch-up progress
17/29
Why this matters to you

Your Today list this morning is a wall of 🚨 — plan-day failed 6/13 and 6/14, learning-email and language pre-render too. Yet plan-day runs perfectly when you fire it by hand (you're reading the proof). That gap between *scheduled* and *manual* is a launchd story, and the same silent-unload bug bit you on Jun 5 in the notification audit. You can't fix what you can't read.

launchd is the traffic controller macOS starts before anything else and never stops. Every recurring job you have — the 06:15 plan-day, the 10:00 language email, the Sunday weekly-review — is not a 'cron job' in the Linux sense; it's a *LaunchAgent*: a small XML file (a .plist) in ~/Library/LaunchAgents/ that tells launchd 'run THIS program at THIS time.' launchd reads the file, schedules it, and runs it as you.

The key mental model: there are two states, and they're different. A plist file existing on disk is NOT the same as the job being *loaded* into launchd. You 'load' it once (launchctl load) and launchd remembers it across reboots. But a job can silently fall out of the loaded set — a malformed edit, a macOS update, a path that moved — and then the file still sits there looking healthy while nothing runs. That's the trap: the artifact looks fine, the work never happens.

Your jobs live under two naming conventions because they were added at different times: the older com.tom.pdb-* ones (com.tom.pdb-plan-day, com.tom.pdb-language-lesson, com.tom.pdb-prep-big3) and the newer app.zorc.* ones (app.zorc.learning-lesson, app.zorc.weekly-review, app.zorc.dropzone-router). They all answer to the same launchctl commands.

Worked example

Three commands that tell you the truth about what's actually scheduled — run them in order:

launchctl list | grep -i -E 'zorc|pdb'

ls -1 ~/Library/LaunchAgents/ | grep -i -E 'zorc|pdb'

plutil -p ~/Library/LaunchAgents/com.tom.pdb-plan-day.plist
▶ Do it now
  1. Run command 1 above. Read off which of your jobs are loaded and what their last exit code was — any non-zero is a job that fired and failed.
  2. Run command 2 and diff it against command 1 by eye. If a plist exists on disk but isn't in the loaded list, that's your silent unload — note which one.
  3. Open the plist for plan-day (command 3) and find its StandardErrorPath. Then `tail -20` that log file — that's the actual error text behind the three 🚨 plan-day-failed tasks, and reading it is how you stop guessing.
  4. If a job is unloaded, reload it: launchctl load ~/Library/LaunchAgents/<name>.plist — then re-run command 1 to confirm it now appears.

Gotchas

Go deeper: launchd.info — the canonical plain-English guide · Apple: Creating Launch Daemons and Agents
One-card takeaway

A plist on disk is a promise; launchctl list is the receipt. When a scheduled job 'fails silently,' the first move is always: is it even loaded, and what was its last exit code?