Timer-based jobs run on a schedule. WatchPaths jobs run when something happens. Drop a file → AI processes it. No polling. No cron. The filesystem is the trigger.
How WatchPaths Works
launchd monitors the paths you specify. When any of them change — a file is created, modified, or deleted — your job fires. The schedule file looks like:
{"type": "on-change", "paths": ["../../workdir/inbox"]}
Relative paths get resolved against the job directory. launchd gets the absolute path in the plist.
The Pattern
Every reactive job does:
- Check what changed (scan the watched directory)
- Process each new file
- Move processed files out (so they don't trigger again)
That last step matters. WatchPaths fires on ANY change to the watched path — including your own writes. If your job writes to the directory it watches, you get an infinite loop. Always move processed files elsewhere.
Choose Your Trigger
What do you want to react to?
- Inbox classifier (default) — drop files into
workdir/inbox/, AI classifies and sorts them → built-in, already works - Downloads organizer → Downloads path — watch
~/Downloads, auto-sort by type - Screenshot OCR → Screenshot path — watch for screenshots, extract text, create searchable notes
Test the Built-in Inbox Classifier
The inbox-watcher system job already does this:
# Make sure it's synced
bun run src/cli.ts sync
# Drop a file
echo "Meeting notes from today's standup" > workdir/inbox/standup.md
# Wait 2-3 seconds for WatchPaths to fire, then check
ls workdir/sorted/
The file should be classified and moved to a category folder under workdir/sorted/.
Check the logs to see what happened:
bun run src/cli.ts logs inbox-watcher
The WatchPaths Gotcha
WatchPaths fires on ANY change. If your job creates files in the watched directory, it triggers itself. The fix: always write output to a DIFFERENT directory.
workdir/inbox/ ← watched (input)
workdir/sorted/ ← not watched (output)
Verification
# Drop multiple files of different types
echo "def hello(): print('hi')" > workdir/inbox/snippet.py
echo "Buy groceries" > workdir/inbox/todo.txt
echo '{"name": "test"}' > workdir/inbox/config.json
# Wait for WatchPaths
sleep 3
# Check classification
find workdir/sorted -type f
What You Learned
- WatchPaths turns the filesystem into an event trigger
- Reactive jobs: check → process → move (never write to the watched path)
- This is the most underrated launchd primitive — most people don't know it exists
- Combined with AI, it creates pipelines: drop a file → AI does work → result appears
Built-in path
Start with the inbox watcher that already exists in the repo. The lesson root shows the exact verification flow: sync, drop a file into workdir/inbox/, wait for WatchPaths to fire, and inspect the sorted output.