Heartbeat System
The heartbeat is a periodic agent turn that lets OpenClaw act without being prompted. Since v2026.8.1 it is a system-owned automation: the same Automations scheduler that runs your cron jobs keeps one monitor job per heartbeat-enabled agent. This page explains the moving parts. For setup and recipes, see the Heartbeat Guide.
How it works
- Config is desired state.
agents.defaults.heartbeatandagents.entries.<id>.heartbeatdescribe what you want. The Gateway writes them through to monitor rows at startup and on config reload.openclaw doctor --fixcan create missing or stale rows before the next start. Edit the config, not the job. - The monitor job owns the tick. It is listed in
openclaw cron list --allasHeartbeat (<agent-id>), with its own run history (openclaw cron runs <jobId>). - Each tick is one turn in the main session (
agent:<id>:mainby default). The prompt is sent verbatim as the user message; there is no heartbeat-specific system prompt. WithisolatedSession: truethe turn runs in a fresh session instead, and withlightContext: truethe workspace bootstrap files are skipped. - The reply contract is
NO_REPLY(orheartbeat_respondwithnotify: false) for nothing to report, and plain alert text (ornotify: truewithnotificationText) otherwise.
Scheduled heartbeats depend on automations. If cron.enabled is false or OPENCLAW_SKIP_CRON=1 is set, they do not run. There is no separate heartbeat timer.
What goes into the prompt
The default prompt tells the agent to follow the monitor scratch when present, keep recurring work in automations, not to revive old tasks from earlier chats, and to reply NO_REPLY when nothing needs attention. You can replace it with heartbeat.prompt; the replacement is not merged with the default.
The monitor scratch is a private text record per monitor job, stored in the shared state database and appended to the prompt. It replaced HEARTBEAT.md, which the runtime no longer reads. Manage it with openclaw cron scratch <jobId>; the agent can rewrite it by passing scratch to heartbeat_respond. If scratch exists but is effectively empty, the run is skipped (reason=empty-heartbeat-file).
Scratch is context, not a schedule. Anything that needs its own cadence ("every morning", "hourly") belongs in its own automation job. Doctor converts old HEARTBEAT.md tasks: blocks into such jobs; see Migrating from HEARTBEAT.md, including the warning that bare legacy interval numbers are read as minutes.
Scheduling and guards
- Cadence:
every, default30m(or1hwhen Anthropic auth resolves to OAuth/token andeveryis unset). A duration string; a bare number is minutes."0m"disables the recurring cadence but keeps the job, its scratch and event-driven wakes. - Active hours:
activeHours: { start, end, timezone }is an allow-window. Ticks outside it are skipped until the next tick inside. Equalstartandendmeans a zero-width window, so the heartbeat never runs. - Busy deferral: a plain scheduled poll does not queue behind other work. If the main queue or automation work is active or queued, the agent has an active run, or the target session has queued work, the poll is recorded as skipped and waits for its next tick. Wakes that carry queued events still retry.
- Event wakes: background exec and node completions, and similar events, can wake the heartbeat between ticks, even with
every: "0m". They are rate-limited per agent: at least 30 seconds apart and a flood guard after five starts in 60 seconds. - Channel readiness: if the readiness check blocks an alert, OpenClaw records the non-delivery and runs the heartbeat again after a one-minute grace period, without consuming the cadence slot.
Delivery routing
target | Delivers to |
|---|---|
owner (default) | The first concrete entry in commands.ownerAllowFrom, then a channel allowFrom. Never a group. No owner means reason=no-route and no model call |
last | The last external conversation, including groups |
<channel id> | That channel, with to as the recipient (for example target: "telegram", to: "123456789") |
none | Nothing external; internal state only |
directPolicy: "block" suppresses DM deliveries (reason=dm-blocked). What reaches the chat is further controlled by channels.*.heartbeatVisibility (showOk, showAlerts, useIndicator); with all three off, the run is skipped (reason=alerts-disabled).
Multi-agent
agents.defaults.heartbeat applies to all agents and per-agent blocks merge on top. If any agent in agents.entries has a heartbeat block, only those agents run heartbeats. Each agent's monitor job has its own scratch and schedule.
{
agents: {
ownership: "explicit",
defaults: {
systemAgent: { agentId: "alex" },
heartbeat: { every: "30m", isolatedSession: true },
},
entries: {
alex: {
workspace: "~/.openclaw/workspace",
model: "anthropic/claude-opus-5-5",
heartbeat: { every: "30m" },
},
monitor: {
workspace: "~/.openclaw/workspace-monitor",
model: "anthropic/claude-haiku-5-5",
heartbeat: { every: "10m", lightContext: true },
},
reporter: {
workspace: "~/.openclaw/workspace-reporter",
model: "anthropic/claude-sonnet-5-5",
},
},
},
}
alex and monitor run heartbeats; reporter does not, and its weekly report would be an automation job created with --agent reporter.
Cost
Each heartbeat is a full agent turn, so cost scales with cadence × prompt size × model price. The prompt is dominated by session history unless isolatedSession is on (upstream estimates about 100K tokens down to 2–5K per run). The levers, in rough order of impact: isolatedSession, a cheaper model, a longer every, activeHours, lightContext, and a small scratch. Worked settings are in the Heartbeat Guide. This page does not quote model prices; check your provider's pricing page.
Without isolatedSession, a heartbeat on a small model can leave that model on the shared main session, which may then overflow on your next turn ("heartbeat model bleed").
Dreaming
Dreaming (memory consolidation) is owned by the memory-core plugin and has its own schedule (plugins.entries.memory-core.config.dreaming.frequency). It is independent of the heartbeat and has been on by default since v2026.8.1. See Dreaming.
Debugging
openclaw logs --follow # gateway log
openclaw system heartbeat last --json # last heartbeat outcome
openclaw cron list --all # find Heartbeat (<agent>) and its id
openclaw cron runs <jobId> --limit 20 # run history, including skip reasons
openclaw system event --text "Check now" --mode now # immediate heartbeat
| Problem | Likely cause | Fix |
|---|---|---|
| Never fires | No monitor row, cron.enabled: false, every: "0m", or another agent has a heartbeat block and this one does not | openclaw doctor --fix; check config |
Skipped with no-route | No resolvable owner | Set commands.ownerAllowFrom, or target + to |
Skipped with empty-heartbeat-file | Scratch is blank or only headings | Write a real checklist or --unset it |
Old HEARTBEAT.md ignored | Retired file | openclaw doctor --fix imports it into scratch |
| Fires at the wrong time | activeHours.timezone or agents.defaults.userTimezone | Set an IANA timezone |
| Config rejected | Top-level heartbeat, interval, quiet_hours, enabled | Use agents.defaults.heartbeat.every / activeHours |
See also
- Heartbeat Guide: setup, recipes, full field reference
- Automation Guide: user-authored automation jobs
- Architecture Overview: where the heartbeat fits
- Multi-Agent Workflows: per-agent configuration
- Official docs: Heartbeat, Automations