Heartbeat
Heartbeat runs periodic agent turns on a configured session so the model can surface anything that needs attention without spamming users. By default it runs on the agent’s main session; for high-traffic sessions, useisolatedSession: true so each heartbeat runs in a fresh sibling transcript while delivery still uses the base session.
Deciding between Heartbeat and Cron? See Cron vs Heartbeat for guidance on when to use each.
Quick start
Set a cadence
Leave heartbeats at the default (
30m, or 1h when Anthropic OAuth/setup-token is detected) or configure your own interval under agents.defaults.heartbeat.every. Use 0m to disable.Create HEARTBEAT.md (optional but recommended)
Add a small
HEARTBEAT.md checklist to the agent workspace. The default prompt tells the agent to read it on every tick. Keep it short — it becomes part of the prompt on every run.Choose a delivery target
Set
target: "last" to route alerts to the last contact. The default (target: "none") runs the turn silently with no outbound message.Config reference
All keys live underagents.defaults.heartbeat (or agents.list[].heartbeat to override per agent).
| Key | Type | Default | Description |
|---|---|---|---|
every | duration string | "30m" | Heartbeat interval. 0m disables. Default is 1h when Anthropic OAuth/setup-token is detected. |
model | string | — | Model override for heartbeat runs (provider/model). |
target | string | "none" | Delivery target: none, last, or an explicit channel id (whatsapp, telegram, discord, slack, etc.). |
to | string | — | Channel-specific recipient override (e.g. E.164 for WhatsApp, Telegram chat id). For Telegram topics: <chatId>:topic:<threadId>. |
accountId | string | — | Account id for multi-account channels (e.g. Telegram with multiple bots). |
directPolicy | allow | block | allow | Controls direct/DM delivery. block suppresses DM delivery while still running the turn. |
prompt | string | (see below) | Overrides the default prompt body (sent verbatim; not merged). |
ackMaxChars | number | 300 | Max chars allowed after HEARTBEAT_OK before the reply is delivered. |
lightContext | boolean | — | Use lightweight bootstrap context; keeps only heartbeat-specific files like HEARTBEAT.md. |
isolatedSession | boolean | — | Each tick runs in a fresh sibling :heartbeat session. Delivery and duplicate suppression still use the base session. |
skipWhenBusy | boolean | — | Skip this agent’s heartbeat while its session-keyed subagent or nested lanes are busy. |
activeHours | object | — | Restrict heartbeats to a time window. See Active hours. |
includeReasoning | boolean | false | Also deliver a separate Reasoning: message when extended thinking is available. |
session | string | "main" | Session key for the heartbeat run. When isolatedSession: true, set the base key here — the runtime appends :heartbeat. |
suppressToolErrorWarnings | boolean | — | Suppress tool-error warning payloads during heartbeat runs. |
wakeGate | object | — | Cheap declarative check that decides whether the persona turn runs. See Wake-gate. |
Scope and precedence
agents.defaults.heartbeat— global defaults applied to all agents.agents.list[].heartbeat— merges on top. If any agent has aheartbeatblock, only those agents run heartbeats.channels.defaults.heartbeat— visibility defaults for all channels.channels.<channel>.heartbeat— per-channel visibility overrides.channels.<channel>.accounts.<id>.heartbeat— per-account overrides on multi-account channels.
Response contract
The model must follow this contract on every heartbeat turn:- Nothing to report → reply with
HEARTBEAT_OK(at the start or end of the reply). The token is stripped and the reply is dropped if the remaining content is ≤ackMaxChars(default 300 chars). - Alert → return only the alert text, no
HEARTBEAT_OK. HEARTBEAT_OKin the middle of a reply is not treated specially.
HEARTBEAT_OK at the start or end of a message is stripped and logged; a message that contains only HEARTBEAT_OK is dropped.
HEARTBEAT.md
IfHEARTBEAT.md exists in the agent workspace, the default prompt tells the model to read it. Treat it as your heartbeat checklist: keep it small, stable, and safe to include every 30 minutes.
Empty-file gate: if HEARTBEAT.md exists but is effectively empty (only blank lines or bare markdown headings), the heartbeat tick is skipped to save API calls. If the file is missing entirely, the run still proceeds.
Example HEARTBEAT.md:
HEARTBEAT.md itself if you ask it to (e.g. “Update HEARTBEAT.md to add a daily calendar check”). You can also add a line to your heartbeat prompt like: “If the checklist becomes stale, update HEARTBEAT.md with a better one.”
Active hours
Restrict heartbeats to a time window usingactiveHours:
activeHours:
| Value | Behaviour |
|---|---|
Omitted / "user" | Uses agents.defaults.userTimezone if set, otherwise falls back to host system timezone |
"local" | Always uses host system timezone |
IANA identifier (e.g. "America/New_York") | Used directly; invalid values fall back to "user" behaviour |
activeHours entirely, or use { start: "00:00", end: "24:00" }.
Per-agent heartbeats
Per-agentheartbeat blocks merge on top of agents.defaults.heartbeat. When any agents.list[] entry has a heartbeat block, only those agents run heartbeats — agents without a heartbeat block are excluded.
Multi-account channels
UseaccountId to target a specific account on channels like Telegram that support multiple bots:
Visibility controls
By default,HEARTBEAT_OK acknowledgments are suppressed and only alert content is delivered. Configure this per channel or per account:
If all three flags (
showOk, showAlerts, useIndicator) are false for a channel, the Gateway skips the heartbeat run entirely — no model call is made.| Goal | Config |
|---|---|
| Default (silent OKs, alerts on) | (no config needed) |
| Fully silent (no messages, no indicator) | channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: false } |
| Indicator only (no messages) | channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: true } |
| OKs in one channel only | channels.telegram.heartbeat: { showOk: true } |
Manual wake
Trigger an immediate heartbeat on demand:--mode next-heartbeat to wait for the next scheduled tick instead.
If multiple agents have heartbeat configured, a manual wake runs all of them immediately.
Nudge: wake the main persona
A nudge injects a payload into an agent’s main session as non-user context and wakes the main persona to decide what to do. The persona produces a user-facing message only when warranted — otherwise it replies withHEARTBEAT_OK and nothing is delivered.
A nudge is not a separate signal mode — it routes through the heartbeat bridge (
enqueueSystemEvent + a targeted heartbeat wake), so every heartbeat gate applies: active hours, busy checks, skipWhenBusy, and duplicate suppression.| Field | Required | Description |
|---|---|---|
text | Yes | Human-readable context the persona sees |
data | No | Structured JSON object; rendered into the system-event text and carried raw for programmatic subscribers |
agentId / sessionKey | No | Target agent or session; defaults to the default agent’s main session |
- Cron — a job with a
wakeGatepayload (see Cron vs Heartbeat) - Heartbeat — a heartbeat configured with a
wakeGate(see Wake-gate below) - Plugin / hook —
api.runtime.signals.nudge(...)(see Agent Signals) - CLI —
openclaw agent nudge
Wake-gate
A wake-gate is a cheap check that decides whether the expensive persona turn runs at all. On a negative result, no model call is made — the turn is gated out, not silenced after running. Setagents.defaults.heartbeat.wakeGate (or per-agent agents.list[].heartbeat.wakeGate) to enable.
Command gate — cheapest, no model call
Command gate — cheapest, no model call
Runs a shell command and parses its last non-empty stdout line as JSON:A non-zero exit, timeout, unparseable output, or Implementation notes:
wakeAgent !== true all mean “do not wake.”- Stdout and stderr are drained so noisy checks do not hang the process.
- Only the final non-empty stdout line is parsed as the wake decision; retained output is bounded to the last 64 KiB.
- On timeout or runner stop, the Gateway terminates the gate process (signals the whole process group on Unix, then force-kills after a grace period). A stopped gate resolves as “do not wake.”
Model gate — isolated analysis turn
Model gate — isolated analysis turn
Runs an isolated cheap turn in a sibling session (Analysis lane) that inspects sources and returns a wake decision. The gate never touches the main session.Model gates also receive heartbeat cancellation — a Gateway shutdown or aborted run stops the isolated analysis turn instead of leaving it running in the background.
text and rendered data are folded into the heartbeat turn’s prompt before the persona runs. When the gate is negative, the run is skipped with reason wake-gate-empty.
The empty-file check for
HEARTBEAT.md is bypassed when wakeGate is configured. The gate evaluates regardless of file content; only a negative gate result skips the persona turn.Wake-gate gateway logs
| Condition | Level | Fields |
|---|---|---|
| Command gate: non-zero exit, empty output, or unparseable JSON | warn | exit, tail (last ≤512 chars of stdout) |
| Model gate: empty output | warn | — |
| Model gate: unparseable JSON | warn | tail (last ≤512 chars of output) |
Gate returned wakeAgent: false | debug | tail |
Gate returned wakeAgent: true | info | hasText, hasData (boolean flags only — never the payload values) |
| Run suppressed by a negative gate | debug | agentId, reason: "wake-gate-empty" |
Reasoning delivery
By default, heartbeats deliver only the final reply. To also deliver the model’s extended thinking (same shape as/reasoning on):
Cost awareness
Heartbeats run full agent turns. Shorter intervals burn more tokens. To keep costs down:- Keep
HEARTBEAT.mdshort. - Use a cheaper
modeloverride for heartbeat runs. - Set
target: "none"if you only want internal state updates with no outbound delivery. - Use a
wakeGatewithkind: "command"to skip turns when there is nothing actionable.