Heartbeat
Heartbeat follows one finite situation until it is resolved, or performs one future action. Examples include following a refund until it arrives, coordinating one dinner, and sending a thank-you email tomorrow morning. A beat is one such instruction.
Jobs own standing responsibilities: daily backups, reviewing every new email, or a regular digest. Repeated checking does not make a temporary watch a job, and a recurring job with an end date is still a job. Work that can be finished now belongs in the conversation or the task board.
Definitions and storage
A beat has a title, current instructions, a completion condition, allowed actions, a workspace,
an agent, a schedule, and a notification destination. Its agent is saved at creation: the
workspace agent or the configured default, unless the definition supplies a complete
provider / model / effort triple. Later default-agent changes do not change that beat.
SQLite in $ENSO_HOME/enso.db owns definitions and current state. The internal tables
_enso_beats, _enso_beat_events, and _enso_beat_runs are managed through
enso heartbeat, not registered as user tables or edited with SQL. There is no parallel
BEAT.md or status file. References such as HB-001 remain unique after pruning.
A beat’s script files live under $ENSO_HOME/heartbeat/<ref>/. The optional entry point is
gate.sh; it can call Python or other helpers. Enso runs only that filename, only as a
regular file of at most 64 KiB inside the beat’s own directory, and rechecks its shell syntax
with bash -n before every check. The provider runs in the beat’s workspace, while the gate
runs in the beat’s directory. All paths derive from ENSO_HOME.
Creation and management
The user asks in ordinary language. The agent loads the bundled enso-heartbeat skill and
creates the arrangement through the CLI. The home-level AGENTS.md teaches the distinction
between finite follow-through and standing jobs; the full skill is loaded when needed.
Definitions and updates are JSON objects read from --file FILE; --file - reads stdin.
Every command accepts --json. A failed command emits one {"ok": false, "error": "…"}
object and exits 1. Declarative validation reports independent problems together.
CLI syntax errors follow the usual exit-2 convention. The
CLI reference lists every enso heartbeat command.
For example, a definition for a temporary watch is:
{
"title": "Follow the refund",
"instructions": "Watch this refund thread and report meaningful progress.",
"completion": "The refund has arrived and the user has been notified.",
"allowed_actions": "Read the thread and notify me; ask before contacting the seller.",
"workspace": "default",
"schedule": "0 * * * *",
"timezone": "America/Vancouver",
"gate": "gate.sh"
}
Every field a definition may carry:
| Field | What it does |
|---|---|
title |
Required. The short name used in lists and notifications. |
instructions |
Required. What to do at each assessment. |
completion |
Required. What must be true before the beat can be fulfilled. |
allowed_actions |
Required. What the agent may do on its own, and what it must ask about first. |
workspace |
Where the assessment runs. Defaults to the workspace of the conversation that created it, else default. |
at |
One future moment, as an ISO timestamp with an explicit UTC offset. Give exactly one of at or schedule. |
schedule |
Five-field cron for repeated checks. |
timezone |
The IANA zone the schedule is read in. Default UTC, so name a zone to keep a local wall-clock time. |
gate |
gate.sh, or absent for no gate. |
llm_checks |
Default false. A recurring beat with no gate must set it to true, which means an LLM runs at every due check. |
agent |
An object with provider, model, and effort. Defaults to the workspace agent, else the configured default. |
notify |
Where automatic notices go — failures, recoveries, expiry, and a one-shot that could not proceed: slack:C…, telegram:<id>, or a bare id with one transport. Defaults to the conversation it came from, else the default notification target. |
notify_thread |
A Slack thread timestamp; only valid with a Slack notify target. |
origin |
The conversation that asked for it (transport, user, channel, thread), filled in from the environment. |
timeout |
Seconds for one provider assessment. Default 900. |
gate_timeout |
Seconds for one gate run. Default 120. |
expires_at |
Close the beat without fulfillment at this time. Must be later than at and followup_at. |
followup_at |
Ask for another assessment then, even when nothing else is due. |
Updates merge supplied definition fields; omitted fields keep their current values, and
--if-revision rejects an edit based on an older definition. A running beat uses note and
wait to record context and follow-up timing; creation and definition edits happen from the
conversation so an old run cannot overwrite newer instructions.
Creation starts paused. This lets the agent write and validate the gate and confirm source access before activating the beat; adding or replacing a gate later means pausing it again first. Validation must not execute the future action early. The agent confirms the arrangement to the user only after activation succeeds, stating when it will check or act and when it will stop.
A definition names exactly one time source:
at: an ISO timestamp with an explicit UTC offset, for one future action.schedule: a five-field cron expression, read intimezone(defaultUTC), for repeated checks. Unlike a job’s cron, it does not follow the machine’s zone unless you name it.
A named timezone keeps a recurring wall-clock time across offset changes. The shared scheduler works at minute resolution. A missing spring time moves forward by the clock gap; a repeated autumn time runs on its first occurrence. Explicit follow-up times and deadlines can also require attention when a source has no new messages.
A recurring beat without a gate requires explicit llm_checks: true. The creating agent must
explain that each scheduled check will involve the LLM. An unavailable or failing gate never
silently switches to this mode.
A one-shot time is consumed by its check. If it cannot proceed, the beat needs attention;
Enso does not keep replaying a stale send. An unfinished one-shot run must set a future
followup_at when it waits. Pausing, resuming, or editing unrelated instructions does not
restore an already-used time; changing at deliberately schedules a new assessment.
Before the first assessment, a one-shot follow-up cannot be earlier than at.
Lifecycle and history
A beat is active, paused, fulfilled, cancelled, or expired. Attention is separate from
its lifecycle: waiting for a person keeps the situation open. Pause preserves it. Fulfillment
requires a reason and evidence that the requested outcome was reached. A successful provider
exit alone does not fulfill a beat.
The event timeline records observations, decisions, instruction changes, actions, failures, and closure. Events are append-only until the closed beat is pruned. They identify who acted, when something happened, when Enso recorded it, and the associated run when there is one.
Checking stores only the latest check’s timestamp, status and diagnostic, and the last successful check time. Quiet checks produce no timeline entries or agent-run rows. A first failure and recovery are recorded; repeating the same failure does not fill the history.
Source checkpoints live in beat state. New observations are saved before starting a provider, and remain pending until explicitly handled. Reading an event or putting it in a prompt does not acknowledge it. A run acknowledges only the input it was given; newer events remain pending for the next assessment.
Actions have stable keys and recorded outcomes. Record an attempt before the external action, then its confirmed result and receipt. A succeeded action must not be repeated; an uncertain outcome must be reconciled before retrying. This does not make arbitrary third-party tools transactional: the agent must use available receipts and source history to establish what happened.
Native Enso message sends and uploads use --action-key inside a beat and record their
attempt and receipt automatically. Other tools use heartbeat action before acting and
heartbeat action-result afterward. A known receipt can still be recorded after an edit or
cancellation; that preserves evidence without authorizing another action. If an outcome
cannot be established, notify the user and pause instead of clearing the uncertainty.
Execution
A heartbeat gate runs outside provider policies. Its assessment uses the beat’s saved provider and must pass that provider’s restricted-workspace check. A passing chat audit alone does not cover it; see background-work limits.
enso serve runs jobs and Heartbeat from one minute clock. Each system decides its own
readiness and uses the same bounded subprocess and provider execution code. A lock prevents
overlapping assessments of one beat, including across daemon processes.
A gate exits 0 for ready, 1 for quiet, and any other code for failure. Only its plain stdout
becomes fresh evidence. A safe ENSO_ERROR: … line on stderr may explain a failure; other
stderr stays out of user alerts. Gate output is bounded to 16 KiB. Gates cannot send native
Enso messages, and source checkpoints change only after explicit agent settlement.
Each assessment starts a fresh provider session with current instructions, timing and wake reason, bounded gate evidence, the latest event, history counts, and CLI lookup commands. The agent retrieves other history as needed. Due follow-ups can wake it after a quiet gate; gate failures never silently start a provider. New evidence arriving after the last run’s input cutoff can prompt another assessment without waiting for the ordinary schedule. A future one-shot is still held until its scheduled time. After a gate failure, pending evidence stays available, but retries wait for the next scheduled check or explicit follow-up instead of triggering an early check every minute.
After downtime, Enso makes one current assessment rather than replaying every missed check. A late one-shot includes its planned time and lateness so the agent can decide whether the authorized action remains useful. An explicit expiry closes the beat without fulfillment. Failures and recoveries notify the saved destination/thread; quiet checks and ordinary successful runs do not send automatic updates. The agent sends useful results explicitly.
Configuration and retention
{
"heartbeat": {
"enabled": true,
"retention_days": 30
}
}
Heartbeat is enabled by default. retention_days must be a positive integer and defaults to
30. Disabling preserves definitions and history; agents must not silently replace a disabled
heartbeat with an equivalent job. The daemon rereads this setting while it runs and stops
active heartbeat work when it is disabled.
Only closed beats are eligible for automatic pruning. Retention starts at closure, not creation. Their events, runs and script files are removed together after the retention period. Active and paused beats retain their context. Permanent outputs, such as a signed agreement, belong in the user’s requested notes or document location.
Viewer
The existing web viewer shows current and previous beats. Open one to read its instructions, latest check, audit history, and agent runs. Viewing a beat never changes it; ask Enso to manage it from the conversation.
