Skip to main content

Heartbeat monitoring

User-defined health checks evaluated on a schedule. Configure checks in /Users/{user_id}/{bot_dir}/config/HEARTBEAT.md.

Heartbeat needs a configured on-disk workspace. The config file lives there, so load_heartbeat_config returns nothing and the whole subsystem is inert when workspace_path is unset.

Check types​

TypeWhat it does
file-watchMonitors file age and existence
shell-commandRuns a command and evaluates a condition (admin-only)
url-healthChecks HTTP status codes
calendar-conflictsDetects overlapping calendar events
task-deadlineFinds overdue items in TASKS.md
self-checkSystem diagnostics: Claude binary, bwrap, DB connectivity, task failure rate, and a live sandboxed execution test

shell-command checks run with build_stripped_env(), but arbitrary shell is still arbitrary shell — a non-admin's check returns unhealthy with "shell-command checks are admin-only" rather than running.

shell-command runs under bash -o pipefail -c, so a probe ending in a pipe reports the first stage that failed rather than the last command. Two things follow. A non-final stage that exits non-zero to report something — grep with no match, diff — now makes the check unhealthy; append || true where that is not what you mean. And a probe ending in head or grep -q reports status 141, which is the pipe being closed early rather than a failure; the alert message says so. Note also that these run under bash rather than /bin/sh, so echo no longer expands backslash escapes — if a check compares output against a condition, confirm it still matches.

The self-check execution test actually spawns a sandboxed claude and asks it to echo a known string, which is the only step that proves the whole path works end to end. It is on by default; set execution_test = false on the check to skip it.

Per-check controls​

SettingPurpose
interval_minutesRun expensive checks less frequently
cooldown_minutesPrevent alert fatigue (minimum time between alerts)
channelDeliver this check's alerts to an explicit surface, overriding the user's routing

Global settings​

Under [settings] in HEARTBEAT.md:

SettingPurpose
quiet_hoursSuppress alerts during off-hours (cross-midnight supported). Global, not per-check
default_cooldown_minutesCooldown for checks that don't set their own (default 60)
conversation_tokenDefault room for this user's heartbeat alerts

State is tracked in the heartbeat_state table: last check time, last alert time, last healthy time, last error time, and consecutive error count.

Alerts​

When a check fails, the alert is routed through surface_for_purpose(config, user_id, "alert") — the user's purpose-keyed routing table, or the check's own channel when it sets one. See per-user delivery routing.

If the resolved channel isn't configured, the alert is skipped with a warning and the consecutive-error count is not incremented — an undeliverable alert is a deployment gap, not a failing check.

The !check command in Talk runs an immediate inline health check (the same diagnostics as self-check, independent of the heartbeat schedule).

Configuration​

Heartbeat checks are defined in HEARTBEAT.md as markdown with embedded TOML, similar to CRON.md. The scheduler evaluates checks every heartbeat_check_interval (default 60s).