Why your scheduled job fails silently on macOS

A background job that is supposed to text you can hang forever on a permission prompt it cannot display. No error. No message. And the monitor keeps reporting itself healthy.

What actually happens

Under launchd, a job that drives Messages through osascript blocks on a TCC Automation prompt. A background context has no way to show that prompt, so nothing resolves it. Measured on a real system: a 90 second timeout, no error returned, and no message sent.

The failure is not the hang. It is that silence looks identical to nothing happened. A watchdog wired to text you would go completely quiet overnight while its own health check stayed green — the exact failure such a system exists to catch, sitting inside its own notification path.

The related trap: cron cannot reach your keychain

Jobs run from cron have no keychain access, so anything needing a stored credential fails with errors like could not read Username: Device not configured. And on recent macOS, scheduled jobs are blocked from reading ~/Documents entirely, which produces a partial view of the filesystem rather than a clean permission error.

A partial view is worse than no view. Code that sees half a directory usually concludes the missing half does not exist, and reports that conclusion confidently.

How to tell whether it is really working

Exit code zero is not delivery. To know a message actually arrived, check the store that records it rather than the tool that sent it:

# osascript exiting 0 proves nothing was raised — not that anything was delivered.
# Messages writes the row, so poll for the row.
sqlite3 ~/Library/Messages/chat.db \
  "select count(*) from message where is_from_me=1 and date > ?;"

Three rules that come out of this, in order of how much they save:

  1. Anything that pushes or sends belongs in a launchd agent, not cron — and it needs a user session with Automation permission actually granted.
  2. Split sending in two. A queue step that always succeeds and never blocks, and a drain step that only runs where permission exists. Undelivered items then accumulate visibly instead of evaporating.
  3. Never let a job report health it has not verified. If it cannot see everything, it should stop and say so rather than converge on a partial view.

Why we know this

Because it happened to us. These are notes from running an estate of machines that watch each other, where a silent notification path is the one failure that cannot be tolerated.

Work gets done without you thinking about it. A RappterBox runs this kind of job overnight, refuses to claim success it has not verified, and texts you one brief in the morning. Join the waitlist →
RappterBox LLC · home · for AI agents