Scheduler

The scheduler is a background daemon that runs bbx tick for multiple boxes on a recurring basis. It's managed via launchd on macOS.

Quick Start

# Add boxes to the scheduler's manifest
bbx boxes add ~/src/boxes/test1
bbx boxes add ~/src/boxes/hearth

# Install launchd plist (auto-starts at login)
bbx scheduler install

# Load it now
launchctl load ~/Library/LaunchAgents/com.beebox.scheduler.plist

How It Works

The daemon runs bbx tick every 60 seconds for each configured box. bbx tick checks all _config/schedules/*.scheduled-script.card files against their cron/at/rrule schedules and runs any that are due.

bbx tick --json prints the result as one object with a record per script: ran, skipped with its reason (not due, disabled, budget exceeded, system busy, a deferral), error with its message, or inconclusive. The exit status is 0 whether or not a script errored (bbx engine wakeup --skip-push followed by bbx tick in the field-test day advance relies on that), so a caller reads errorCount or the records.

Key behaviors:

The job check

Each pass, after the tick, runs a job check (src/core/schedule/job-check/). Every scheduled wakeup is connector-scoped, so a job card with no connector: (a question follow-up, scan or capture intake, a job an agent wrote) runs only in a full wakeup. The check starts one when:

The run is bbx engine wakeup --max-cycles 3. On a unit host the pass asks the box server to start it (wakeup.start, through the hub, which starts an idle box), because the pass unit's children end with the pass; elsewhere the daemon's own process starts it. Either way the pass waits only until the run's child holds the cycle lock. The attempt and today's count are saved before the start, so a failed write starts nothing; a run that found the lock held or failed before taking it is refunded. A job is given up 7 days after the check first saw it; its card stays, and the stalled-jobs health check names it.

State is .beebox/job-check.json (per job file: first sight, content hash, attempts, next time; per box: runs today), written under .beebox/job-check.lock. An unreadable file stops the check (it may hide today's count): nothing starts, and each pass with a waiting job logs a state unreadable line naming it. Deleting the file makes every waiting job due once more, with a new 7-day lifetime. Decisions are job-check lines in scheduler.jsonl, with action one of started, not started, daily bound, gave up, skipped, state unreadable, or run ended (the run's ok, jobsProcessed, jobsRemaining, and detail, written when it settles). An idle box costs one directory read and writes nothing.

Configuration

Global config at ~/.config/beebox/boxes.json (managed by bbx boxes add/remove/list — bbx scheduler add/remove/list still work but are deprecated aliases). This manifest now only feeds the scheduler; serving is routed by bbx hub via its own ~/.config/beebox/hub.json:

{
  "boxes": [
    "/Users/you/src/boxes/mybox"
  ]
}

Only explicitly added boxes run. Directories like boxes/scenarios/ won't be included unless added.

Logs

Per-box JSONL logs at <boxRoot>/.beebox/scheduler.jsonl (gitignored, auto-rotated at 1MB).

Each tick produces one JSONL entry per box:

{"ts":"2026-02-24T06:22:27Z","event":"tick","box":"/path/to/box","result":{"ran":2,"skipped":5,"errors":0,"scripts":[{"name":"check-email","status":"ran","command":"bbx wakeup --connector gmail","durationMs":4523},{"name":"check-rss","status":"skipped"}]}}

View logs:

bbx scheduler log                    # All boxes, last 20 entries
bbx scheduler log --box ~/src/boxes/test1  # One box
bbx scheduler log --errors           # Only entries with errors
bbx scheduler log --json             # Raw JSONL

The webapp also serves logs at GET /api/scheduler/log (filtered to the current box).

CLI Commands

bbx scheduler start [--interval <s>]  # Run daemon foreground
bbx boxes add <path>                  # Add box to the manifest
bbx boxes remove <path>               # Remove box
bbx boxes list                        # Show configured boxes
bbx scheduler status                  # Boxes + launchd status
bbx scheduler log [options]           # View logs
bbx scheduler install                 # Install launchd plist
bbx scheduler uninstall               # Remove launchd plist

Launchd

The plist is at ~/Library/LaunchAgents/com.beebox.scheduler.plist.

# Check status
launchctl list com.beebox.scheduler

# Start/stop
launchctl load ~/Library/LaunchAgents/com.beebox.scheduler.plist
launchctl unload ~/Library/LaunchAgents/com.beebox.scheduler.plist

# Reinstall (after code changes, nvm updates, etc.)
bbx scheduler install

The plist captures the current PATH at install time (needed for nvm). If you update node versions, re-run bbx scheduler install.

Stderr goes to ~/.local/share/beebox/scheduler-stderr.log (for crash diagnostics only — structured logs are in per-box JSONL files).

One pass, one box: bbx engine scheduler pass

The daemon's per-box body (admission, the dirty-repo note, the heartbeat, the growth probe, the tick, the job check, the alerts) is bbx engine scheduler pass <boxRoot> (src/core/schedule/scheduler/pass.ts). On a machine without units the daemon runs it in process. On a unit host (docs/plans/per-box-units.md) the daemon runs as beebox-sched, whose manifest is /var/lib/beebox-sched/.config/beebox/boxes.json, and runs each box's pass as that box's own user through sudo bbx-box-run <slug> scheduler-pass, the one root path it may call, which starts beebox-pass@<slug>.service inside the box sandbox and waits for it. The wrapper's presence at /usr/local/sbin/bbx-box-run selects that path.