Automation
Botholomew no longer ships an OS-level watchdog. Earlier versions installed a launchd plist or a systemd user service that kept a single daemon alive; we dropped that because the install was heavy and opaque. Instead, you choose how and when workers run.
This doc covers the common patterns. None of them are installed for you — you copy the recipe that matches your needs.
The shape of a scheduled run
botholomew worker run (one-shot, default mode) does one thing and exits:
- Write a worker pidfile to
workers/<id>.jsonwith PID and heartbeat metadata. - Start a heartbeat
setIntervalso other workers know it's alive. - Evaluate any due schedules and enqueue their tasks.
- Claim the next eligible pending task.
- Run the LLM tool loop until the task is complete / failed / waiting.
- Mark the worker
stoppedand exit.
If there's no eligible task, the worker exits immediately — safe to run on a tight cron without overlapping concerns.
Two things make this safe to run concurrently with other workers:
- Task claims are atomic — the worker
open()s anO_EXCLlockfile undertasks/.locks/<id>.lock; only one worker wins. - Schedule evaluation is gated by an
O_EXCLlockfile claim underschedules/.locks/<id>.lockplus a minimum-interval window, so two workers can't enqueue duplicate task batches from the same schedule.
See architecture.md.
Pattern: cron (recommended)
One line. Put this in crontab -e:
# Every 5 minutes, advance one task in ~/projects/inbox-bot
*/5 * * * * cd ~/projects/inbox-bot && /usr/local/bin/botholomew worker run >> logs/cron.log 2>&1- Fire as often as you like; each fire is one task at most.
- Overlap is fine. If two fires start close together, one will claim the task and the other will exit without work.
- Resolve
botholomewwith a full path. cron'sPATHis minimal;which botholomewfrom your shell gives you the right answer. - Redirect to
logs/cron.log(or anywhere you like) so you can see what happened if a run misbehaves.
More aggressive variants
If you have a backlog you want drained quickly, spawn background workers every minute:
* * * * * cd ~/projects/inbox-bot && botholomew worker start >> logs/cron.log 2>&1Each worker still exits after one task; they just overlap freely. A crashed worker is reaped within ~60s and its task goes back into the queue.
Nightly reflection (dream)
botholomew dream reviews recent threads and consolidates what the agent learned into the knowledge store and its prompts. It's a one-shot command, so cron it like any other run. Match the cadence to dream_lookback_hours (the 24h default pairs with a daily dream):
0 3 * * * cd ~/projects/inbox-bot && botholomew dream >> logs/dream.log 2>&1See reflection.md for what a dream does and the --since / --dry-run flags.
Pattern: a single long-running worker
Simplest UX for a workstation that's on most of the day: open a tmux or screen pane and run a persist worker in it.
tmux new -s botholomew
botholomew worker run --persist
# Ctrl+B, D to detachIt'll tick every tick_interval_seconds (default 300) when the queue is empty and back-to-back while there's work. Ctrl+C to stop cleanly (the shutdown handler marks the worker stopped).
No cron, no watchdog, no systemd — and when you want to upgrade, you stop the pane and start it again.
Pattern: launchd (macOS, optional)
If you want Botholomew to survive logouts and start on boot without cron or tmux, a minimal ~/Library/LaunchAgents/com.example.botholomew.plist looks like:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.example.botholomew</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/botholomew</string>
<string>--dir</string>
<string>/Users/you/projects/inbox-bot</string>
<string>worker</string>
<string>run</string>
</array>
<key>StartInterval</key><integer>300</integer>
<key>StandardOutPath</key>
<string>/Users/you/projects/inbox-bot/logs/launchd.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/projects/inbox-bot/logs/launchd.log</string>
</dict>
</plist>Then:
launchctl load ~/Library/LaunchAgents/com.example.botholomew.plistThis runs worker run every 300s. You own the plist; Botholomew doesn't touch it. If Botholomew lives in a folder launchd can't read (e.g., under ~/Desktop on newer macOS), grant Full Disk Access to whichever program invokes the binary.
Pattern: systemd user timer (Linux, optional)
Two files in ~/.config/systemd/user/:
botholomew-inbox.service:
[Unit]
Description=Run one Botholomew worker tick
[Service]
Type=oneshot
WorkingDirectory=/home/you/projects/inbox-bot
ExecStart=/usr/local/bin/botholomew worker run
StandardOutput=append:/home/you/projects/inbox-bot/logs/systemd.log
StandardError=append:/home/you/projects/inbox-bot/logs/systemd.logbotholomew-inbox.timer:
[Unit]
Description=Run Botholomew every 5 minutes
[Timer]
OnBootSec=60
OnUnitActiveSec=5min
Unit=botholomew-inbox.service
[Install]
WantedBy=timers.targetEnable with:
systemctl --user daemon-reload
systemctl --user enable --now botholomew-inbox.timerSame concurrency story as cron: each fire is one task at most.
Troubleshooting
- "Nothing's happening."
botholomew worker listshows every worker pidfile underworkers/. Filter with--status runningto see who's alive right now. If you see zero running and a non-empty queue, spawn one:botholomew worker start --persist. - "I see dead workers piling up." Reaped crashes keep their pidfiles on disk as forensic evidence; only clean exits (
status='stopped') get auto-pruned by the reaper afterworker_stopped_retention_seconds(default 1 hour). If dead pidfiles are bothering you, runbotholomew worker reap— it walksworkers/and unlinks both stale dead workers and stopped pidfiles past the retention window. You can alsorm workers/<id>.jsondirectly.botholomew worker list --status deadshows the list first. - "Cron runs aren't firing." Check
grep CRON /var/log/syslog(Linux) orlog show --predicate 'process == "cron"'(macOS). Common causes: minimalPATH, or a relative path tobotholomew. - "Two workers keep claiming the same task." They don't — by design. The lockfile body holds the worker id and
claimed_attimestamp, so only one worker wins theO_EXCLopen. If you're seeing duplicate output, it's because the task was re-run after its worker was reaped — checkworker list --status dead. - "The log is getting huge." Rotate it yourself (logrotate, newsyslog). Botholomew used to do this inside the old watchdog; it no longer does.
Why no built-in watchdog?
Feedback from early users: installing launchctl/systemctl entries was heavy, platform-specific, and opaque — and because it was installed per project, it accumulated in ~/Library/LaunchAgents/ faster than users expected. Replacing it with "run worker run however you already run things" makes the footprint predictable and the failure modes familiar. If you do want boot-time survival, the templates above give you what the old watchdog provided, without the magic.