Heartbeat (Gateway)
Heartbeat vs Cron? See Cron vs heartbeat for guidance on when to use each.Heartbeat runs periodic agent turns on a configured session so the model can surface anything that needs attention without spamming you. By default that is the agent main session; for high-traffic sessions, use
isolatedSession: true
so each heartbeat runs in a fresh sibling transcript while delivery still uses
the base session.
Troubleshooting: /automation/troubleshooting
Quick start (beginner)
- Leave heartbeats enabled (default is
30m, or1hfor Anthropic OAuth/setup-token) or set your own cadence. - Create a tiny
HEARTBEAT.mdchecklist in the agent workspace (optional but recommended). - Decide where heartbeat messages should go (
target: "none"is the default; settarget: "last"to route to the last contact). - Recommended for active agents: enable
lightContext,isolatedSession, andskipWhenBusy. - Optional: enable heartbeat reasoning delivery for transparency.
- Optional: restrict heartbeats to active hours (local time).
Defaults
- Interval:
30m(or1hwhen Anthropic OAuth/setup-token is the detected auth mode). Setagents.defaults.heartbeat.everyor per-agentagents.list[].heartbeat.every; use0mto disable. - Prompt body (configurable via
agents.defaults.heartbeat.prompt):Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK. - The heartbeat prompt is sent verbatim as the user message. The system prompt includes a “Heartbeat” section and the run is flagged internally.
- Active hours (
heartbeat.activeHours) are checked in the configured timezone. Outside the window, heartbeats are skipped until the next tick inside the window.
Configuration
What the heartbeat prompt is for
The default prompt is intentionally broad:- Background tasks: “Consider outstanding tasks” nudges the agent to review follow-ups (inbox, calendar, reminders, queued work) and surface anything urgent.
- Human check-in: “Checkup sometimes on your human during day time” nudges an occasional lightweight “anything you need?” message, but avoids night-time spam by using your configured local timezone (see /concepts/timezone).
agents.defaults.heartbeat.prompt (or
agents.list[].heartbeat.prompt) to a custom body (sent verbatim).
Response contract
- If nothing needs attention, reply with
HEARTBEAT_OK. - During heartbeat runs, OpenClaw treats
HEARTBEAT_OKas an ack when it appears 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). - If
HEARTBEAT_OKappears in the middle of a reply, it is not treated specially. - For alerts, do not include
HEARTBEAT_OK; return only the alert text.
HEARTBEAT_OK at the start/end of a message is stripped
and logged; a message that is only HEARTBEAT_OK is dropped.
Config
Scope and precedence
agents.defaults.heartbeatsets global heartbeat behavior.agents.list[].heartbeatmerges on top; if any agent has aheartbeatblock, only those agents run heartbeats.channels.defaults.heartbeatsets visibility defaults for all channels.channels.<channel>.heartbeatoverrides channel defaults.channels.<channel>.accounts.<id>.heartbeat(multi-account channels) overrides per-channel settings.
Per-agent heartbeats
If anyagents.list[] entry includes a heartbeat block, only those agents
run heartbeats. The per-agent block merges on top of agents.defaults.heartbeat
(so you can set shared defaults once and override per agent).
Example: two agents, only the second agent runs heartbeats.
Active hours
Restrict heartbeats to business hours in a specific timezone:24/7 setup
If you want heartbeats to run all day, use one of these patterns:- Omit
activeHoursentirely (no time-window restriction; this is the default behavior). - Set a full-day window:
activeHours: { start: "00:00", end: "24:00" }.
start and end time (for example 08:00 to 08:00).
That is treated as a zero-width window, so heartbeats are always skipped.
Multi account example
UseaccountId to target a specific account on multi-account channels like Telegram:
Field notes
every: heartbeat interval (duration string; default unit = minutes).model: optional model override for heartbeat runs (provider/model).includeReasoning: when enabled, also deliver the separateReasoning:message when available (same shape as/reasoning on).session: optional session key for heartbeat runs.main(default): agent main session.- Explicit session key (copy from
openclaw sessions --jsonor the sessions CLI). - Session key formats: see Sessions and Groups.
- When
isolatedSession: true, configure the base session key here. Do not pre-append:heartbeat; the runtime creates and refreshes the sibling:heartbeatsession itself.
target:last: deliver to the last used external channel.- explicit channel:
whatsapp/telegram/discord/googlechat/slack/msteams/signal/imessage. none(default): run the heartbeat but do not deliver externally.
directPolicy: controls direct/DM delivery behavior:allow(default): allow direct/DM heartbeat delivery.block: suppress direct/DM delivery (reason=dm-blocked).
to: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp or a Telegram chat id). For Telegram topics/threads, use<chatId>:topic:<messageThreadId>.accountId: optional account id for multi-account channels. Whentarget: "last", the account id applies to the resolved last channel if it supports accounts; otherwise it is ignored. If the account id does not match a configured account for the resolved channel, delivery is skipped.prompt: overrides the default prompt body (not merged).ackMaxChars: max chars allowed afterHEARTBEAT_OKbefore delivery.suppressToolErrorWarnings: when true, suppresses tool error warning payloads during heartbeat runs.lightContext: when true, heartbeat runs use lightweight bootstrap context and keep only heartbeat-specific bootstrap files such asHEARTBEAT.md.isolatedSession: when true, each heartbeat runs in a fresh sibling:heartbeatsession with no prior conversation transcript. Delivery routing and duplicate suppression still use the base session. The:heartbeatsession key suffix causes the storage layer to tag these entries withtrigger_source = 'heartbeat'; session recall (readByKey) and analytics queries automatically exclude them so heartbeat turns do not appear in conversation history or inflate per-turn metrics. See Session History Hygiene.- Event ownership: the run session key is resolved before the preflight queue peek, so an isolated run only ever peeks its own
:heartbeatqueue — never the base session’s.signal:*prompt events are the exception that can transfer: when the run session holds signal events, the turn is served as a signal-event turn and may relay to the user through the base lane.
- Event ownership: the run session key is resolved before the preflight queue peek, so an isolated run only ever peeks its own
skipWhenBusy: when true, also skips this agent’s heartbeat while its session-keyed subagent or nested lanes are busy.activeHours: restricts heartbeat runs to a time window. Object withstart(HH:MM, inclusive; use00:00for start-of-day),end(HH:MM exclusive;24:00allowed for end-of-day), and optionaltimezone.- Omitted or
"user": uses youragents.defaults.userTimezoneif set, otherwise falls back to the host system timezone. "local": always uses the host system timezone.- Any IANA identifier (e.g.
America/New_York): used directly; if invalid, falls back to the"user"behavior above. startandendmust not be equal for an active window; equal values are treated as zero-width (always outside the window).- Outside the active window, heartbeats are skipped until the next tick inside the window.
- Omitted or
Delivery behavior
- Heartbeats run in the agent’s main session by default (
agent:<id>:<mainKey>), orglobalwhensession.scope = "global". Setsessionto override to a specific channel session (Discord/WhatsApp/etc.). sessiononly affects the run context; delivery is controlled bytargetandto.- To deliver to a specific channel/recipient, set
target+to. Withtarget: "last", delivery uses the last external channel for that session. - Heartbeat deliveries allow direct/DM targets by default. Set
directPolicy: "block"to suppress direct-target sends while still running the heartbeat turn. - If the main queue, cron queue, or target session lane is busy, the heartbeat is skipped and retried later.
- With
skipWhenBusy: true, this agent’s own session-keyed subagent/nested lanes also defer heartbeat runs. Other agents’ busy lanes do not defer this agent. - If
targetresolves to no external destination, the run still happens but no outbound message is sent. - Heartbeat-only replies do not keep the session alive; the last
updatedAtis restored so idle expiry behaves normally.
Identity attribution
Key:heartbeat.identity — default unset (the heartbeat runs as a pure system turn with no identity).
Set it to a registered identity key (a session.identityLinks entry, matched case-insensitively) to run the heartbeat under that user’s identity lane — the turn is attributed to them the same way their chat-initiated turns are, instead of running identity-less. An unmatched value is never fabricated into an identity: the run simply stays unattributed.
This attribute governs the run, not the destination: target / to / accountId still decide where check-ins are delivered. The companion behaviour on the cron side is identity-linked delivery, where a delivery.to holding an identity key resolves to the channel-native chat id linked to that identity — see Cron jobs: Identity attribution.
Visibility controls
By default,HEARTBEAT_OK acknowledgments are suppressed while alert content is
delivered. You can adjust this per channel or per account:
What each flag does
showOk: sends aHEARTBEAT_OKacknowledgment when the model returns an OK-only reply.showAlerts: sends the alert content when the model returns a non-OK reply.useIndicator: emits indicator events for UI status surfaces.
Per-channel vs per-account examples
Common patterns
HEARTBEAT.md (optional)
If aHEARTBEAT.md file exists in the workspace, the default prompt tells the
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
safe to include every 30 minutes.
If HEARTBEAT.md exists but is effectively empty (only blank lines and markdown
headers like # Heading), OpenClaw skips the heartbeat run to save API calls.
If the file is missing, the heartbeat still runs and the model decides what to do.
When wakeGate is configured, the empty-file check is bypassed — the gate
evaluates regardless of HEARTBEAT.md content. If the gate is negative, the
heartbeat is still skipped (with reason wake-gate-empty); if the gate is
positive, the persona turn runs.
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
Example HEARTBEAT.md:
Can the agent update HEARTBEAT.md?
Yes — if you ask it to.HEARTBEAT.md is just a normal file in the agent workspace, so you can tell the
agent (in a normal chat) something like:
- “Update
HEARTBEAT.mdto add a daily calendar check.” - “Rewrite
HEARTBEAT.mdso it’s shorter and focused on inbox follow-ups.”
HEARTBEAT.md — it becomes part of the prompt context.
Manual wake (on-demand)
You can enqueue a system event and trigger an immediate heartbeat with:heartbeat configured, a manual wake runs each of those
agent heartbeats immediately.
Use --mode next-heartbeat to wait for the next scheduled tick.
Nudge: wake the main persona
A nudge hands a payload to an agent’s main session as non-user context and wakes the main persona to decide what to do. Detection is cheap and lives in the caller; action is expensive and lives in the persona. A nudge is not a new signal mode — it is a named operation over the existing heartbeat bridge (enqueueSystemEvent + a targeted heartbeat wake), so it inherits every
heartbeat gate.
The nudge contract is one payload shared by every trigger surface:
text(required): the human-readable context the persona sees.data(optional): a structured JSON object, rendered into the system-event text and also carried raw for programmatic subscribers.- target (optional):
agentIdor an explicitsessionKey; defaults to the default agent’s main session.
HEARTBEAT_OK and no
delivery. A nudge whose check found nothing actionable sends no message.
Gates are honoured. A nudge routes through the heartbeat dispatch, so active-hours, the
main/cron/subagent/session lane busy checks, skipWhenBusy, and duplicate suppression all apply. A
nudge against a busy target session defers; it does not bypass any check.
The nudge is reachable from four trigger surfaces, all producing the same main-session injection and
persona wake:
- Cron — a job with a
wakeGatepayload (see Cron jobs) - Heartbeat — a heartbeat configured with a
wakeGate(see Wake-gate below) - Plugin / hook —
api.runtime.signals.nudge(...)(see Agent Signals) - CLI —
openclaw agent nudge
Plugin / hook example
Wake-gate: gate the expensive persona turn
A wake-gate is a cheap, declarative check that decides whether the expensive persona turn runs at all. On a negative result no main-session persona turn runs — the turn is gated, not silenced after running. Setagents.defaults.heartbeat.wakeGate (or per-agent
agents.list[].heartbeat.wakeGate) to turn a heartbeat into a wake-gate.
There are two gate kinds:
command— runs a shell command and parses its last non-empty stdout line as JSON{ "wakeAgent": boolean, "text"?: string, "data"?: unknown }. A non-zero exit, a timeout, unparseable output, orwakeAgent !== true, all mean “do not wake”. This kind invokes no model runner at all — the cheapest path, ideal for git/curl/SQL checks.model— runs an isolated cheap turn (a sibling session on theAnalysislane, with optionalmodel/provider/lightContextoverrides) that inspects sources and returns a structured wake decision. The gate evaluation stays in the isolated lane and never touches the main session.
text (and rendered data) is folded into this heartbeat
turn’s prompt before the persona runs. When the gate is negative, the heartbeat is skipped with the
reason wake-gate-empty and the persona is not invoked.
Default behaviour is unchanged: a heartbeat with no wakeGate set runs exactly as it does today.
Command wake-gate example
/bin/sh -c on Unix, cmd.exe /c on Windows).
Stdout and stderr are drained so noisy checks do not hang the process; the retained output window is
bounded to the last 64 KiB, and only the final non-empty stdout line is parsed as the wake decision.
If the command exceeds timeoutMs or the heartbeat runner is stopped, the Gateway terminates the
gate. On Unix it signals the whole process group, then force-kills stubborn processes after a short
grace period. A stopped or timed-out gate resolves as “do not wake” instead of crashing the
heartbeat runner.
Model wake-gate example
timeoutMs default as command gates (30000 ms). They also receive
heartbeat cancellation, so a Gateway shutdown or aborted heartbeat run stops the isolated analysis
turn instead of leaving it running in the background.
Wake-gate gateway logs
The Gateway emits structured log lines at the following levels so operators can observe and troubleshoot wake-gate decisions without enabling verbose debug mode:
The
info-level wake-true log intentionally carries only boolean presence flags (hasText,
hasData) to prevent payload content and secrets from appearing in the gateway log.
To see gate-false and suppression decisions in real time, set the logging level to debug:
Reasoning delivery (optional)
By default, heartbeats deliver only the final “answer” payload. If you want transparency, enable:agents.defaults.heartbeat.includeReasoning: true
Reasoning: (same shape as /reasoning on). This can be useful when the agent
is managing multiple sessions/codexes and you want to see why it decided to ping
you — but it can also leak more internal detail than you want. Prefer keeping it
off in group chats.
Cost awareness
Heartbeats run full agent turns. Shorter intervals burn more tokens. KeepHEARTBEAT.md small and consider a cheaper model or target: "none" if you
only want internal state updates.