Skip to content

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:

  1. Register a worker row in the DB.
  2. Start a heartbeat setInterval so other workers know it's alive.
  3. Evaluate any due schedules and enqueue their tasks.
  4. Claim the next eligible pending task.
  5. Run the LLM tool loop until the task is complete / failed / waiting.
  6. Mark the worker stopped and 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 (UPDATE ... WHERE status='pending' RETURNING *).
  • Schedule evaluation is gated by an atomic claim + a minimum-interval window, so two workers can't enqueue duplicate task batches from the same schedule.

See architecture.md.


One line. Put this in crontab -e:

cron
# Every 5 minutes, advance one task in ~/projects/inbox-bot
*/5 * * * * cd ~/projects/inbox-bot && /usr/local/bin/botholomew worker run >> .botholomew/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 botholomew with a full path. cron's PATH is minimal; which botholomew from your shell gives you the right answer.
  • Redirect to .botholomew/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:

cron
* * * * * cd ~/projects/inbox-bot && botholomew worker start >> .botholomew/cron.log 2>&1

Each 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.


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.

bash
tmux new -s botholomew
botholomew worker run --persist
# Ctrl+B, D to detach

It'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
<?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/.botholomew/launchd.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/you/projects/inbox-bot/.botholomew/launchd.log</string>
</dict>
</plist>

Then:

bash
launchctl load ~/Library/LaunchAgents/com.example.botholomew.plist

This 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:

ini
[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/.botholomew/systemd.log
StandardError=append:/home/you/projects/inbox-bot/.botholomew/systemd.log

botholomew-inbox.timer:

ini
[Unit]
Description=Run Botholomew every 5 minutes

[Timer]
OnBootSec=60
OnUnitActiveSec=5min
Unit=botholomew-inbox.service

[Install]
WantedBy=timers.target

Enable with:

bash
systemctl --user daemon-reload
systemctl --user enable --now botholomew-inbox.timer

Same concurrency story as cron: each fire is one task at most.


Troubleshooting

  • "Nothing's happening." botholomew worker list shows every worker the DB has ever seen. Filter with --status running to 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 stay in the table as forensic evidence; only clean exits (status='stopped') get auto-pruned (after worker_stopped_retention_seconds, default 1 hour). If dead rows are bothering you, DELETE FROM workers WHERE status='dead' clears them safely. botholomew worker list --status dead shows the list first.
  • "Cron runs aren't firing." Check grep CRON /var/log/syslog (Linux) or log show --predicate 'process == "cron"' (macOS). Common causes: minimal PATH, or a relative path to botholomew.
  • "Two workers keep claiming the same task." They don't — by design. The claimed_by column is stamped by the atomic UPDATE, so only one wins. If you're seeing duplicate output, it's because the task was re-run after its worker was reaped — check worker 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.

Released under the MIT License.