Diagnose by surface
The homelab has four scheduling surfaces. The “didn’t fire” question depends on which one.
| Surface | Where it lives | How to check |
|---|---|---|
| Mac launchd | ~/Library/LaunchAgents/*.plist |
launchctl list | grep -i <name> shows last exit code; tail -50 ~/Library/Logs/<job>.log |
| Mac user cron | crontab -l on Mac Studio |
tail -50 /tmp/cron-*.log if the job writes there; otherwise add MAILTO="" and re-run |
| CT100 cron | pct exec 100 -- crontab -l |
Errors route to Gotify via cron-gotify-wrapper.sh (priority 5 โ Telegram). Check the telegram bot. |
| hpve cron | crontab -l on pve as root |
Same wrapper as CT100 โ errors โ Gotify |
| Cowork scheduled tasks | Cowork Settings โ Scheduled Tasks | lastRunAt timestamp on each task; audit log in ~/Library/Application Support/Claude/local-agent-mode-sessions/... |
launchd didn't fire
# Show last exit code (column 1) and PID (column 2 โ - means not running)
launchctl list | grep -i com.bee
# Manually kickstart (run now)
launchctl kickstart -k gui/$(id -u)/com.bee.<job-name>
# If kickstart errors with "Could not find specified service", the plist isn't loaded:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.bee.<job-name>.plist
# If the plist has a typo, log will say so
log show --predicate 'subsystem == "com.apple.xpc.launchd"' --info --last 1h | grep com.bee.<job-name>
Common launchd gotcha: the plist points at a script that doesn’t exist (e.g., com.bee.rebuild-mcp-venvs.plist references ~/scripts/rebuild-mcp-venvs.sh โ make sure that file exists and is executable). The launchd job loads fine but every run silently fails.
cron didn't fire
# On CT100 or hpve โ list crontab
crontab -l
# The cron-gotify-wrapper.sh captures stderr to Gotify. If you don't see an
# error, the job ran cleanly OR the wrapper isn't installed.
# Verify wrapping:
crontab -l | head -10 # should see /usr/local/bin/cron-gotify-wrapper.sh <cmd>
# Validate the cron expression
# mcp__cron-validator__cron_validate("0 4 * * *")
Cowork scheduled task didn't fire
# Find today's session for the task
python3 << 'PY'
import json, os, glob, datetime
base = os.path.expanduser('~/Library/Application Support/Claude/local-agent-mode-sessions')
today = datetime.date.today().isoformat()
task = 'daily-briefing' # change as needed
for f in glob.glob(f'{base}/*/*/local_*.json'):
try: d = json.load(open(f))
except: continue
if d.get('scheduledTaskId') != task: continue
if datetime.datetime.fromtimestamp(d['createdAt']/1000).date().isoformat() != today: continue
print(datetime.datetime.fromtimestamp(d['createdAt']/1000), '->', datetime.datetime.fromtimestamp(d['lastActivityAt']/1000), f)
PY
If no session for today appeared, the Cowork scheduler didn’t fire โ check Cowork Settings โ Scheduled Tasks. If a session DID appear but the expected output is missing, read its audit.jsonl to see where it got stuck.
Fix
Most often the fix is one of:
- Re-bootstrap the launchd plist after editing
- Re-run manually to confirm the job works, then wait for next scheduled run
- Update the Cowork task definition (Settings โ Scheduled Tasks โ Edit) if the cron expression is wrong
For broken behavior despite the job firing, see the specific runbooks (doc-sync auth fail, daily briefing not arriving, dictation not processing).