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:

  1. Check what changed (scan the watched directory)
  2. Process each new file
  3. 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 organizerDownloads path — watch ~/Downloads, auto-sort by type
  • Screenshot OCRScreenshot 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