Skip to main content

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)

  1. Leave heartbeats enabled (default is 30m, or 1h for Anthropic OAuth/setup-token) or set your own cadence.
  2. Create a tiny HEARTBEAT.md checklist in the agent workspace (optional but recommended).
  3. Decide where heartbeat messages should go (target: "none" is the default; set target: "last" to route to the last contact).
  4. Recommended for active agents: enable lightContext, isolatedSession, and skipWhenBusy.
  5. Optional: enable heartbeat reasoning delivery for transparency.
  6. Optional: restrict heartbeats to active hours (local time).
Example config:

Defaults

  • Interval: 30m (or 1h when Anthropic OAuth/setup-token is the detected auth mode). Set agents.defaults.heartbeat.every or per-agent agents.list[].heartbeat.every; use 0m to 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).
If you want a heartbeat to do something very specific (e.g. “check Gmail PubSub stats” or “verify gateway health”), set 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_OK as 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_OK appears in the middle of a reply, it is not treated specially.
  • For alerts, do not include HEARTBEAT_OK; return only the alert text.
Outside heartbeats, stray 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.heartbeat sets global heartbeat behavior.
  • agents.list[].heartbeat merges on top; if any agent has a heartbeat block, only those agents run heartbeats.
  • channels.defaults.heartbeat sets visibility defaults for all channels.
  • channels.<channel>.heartbeat overrides channel defaults.
  • channels.<channel>.accounts.<id>.heartbeat (multi-account channels) overrides per-channel settings.

Per-agent heartbeats

If any agents.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:
Outside this window (before 9am or after 10pm Eastern), heartbeats are skipped. The next scheduled tick inside the window will run normally.

24/7 setup

If you want heartbeats to run all day, use one of these patterns:
  • Omit activeHours entirely (no time-window restriction; this is the default behavior).
  • Set a full-day window: activeHours: { start: "00:00", end: "24:00" }.
Do not set the same 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

Use accountId 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 separate Reasoning: 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 --json or 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 :heartbeat session 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. When target: "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 after HEARTBEAT_OK before 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 as HEARTBEAT.md.
  • isolatedSession: when true, each heartbeat runs in a fresh sibling :heartbeat session with no prior conversation transcript. Delivery routing and duplicate suppression still use the base session. The :heartbeat session key suffix causes the storage layer to tag these entries with trigger_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 :heartbeat queue — 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.
  • 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 with start (HH:MM, inclusive; use 00:00 for start-of-day), end (HH:MM exclusive; 24:00 allowed for end-of-day), and optional timezone.
    • Omitted or "user": uses your agents.defaults.userTimezone if 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.
    • start and end must 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.

Delivery behavior

  • Heartbeats run in the agent’s main session by default (agent:<id>:<mainKey>), or global when session.scope = "global". Set session to override to a specific channel session (Discord/WhatsApp/etc.).
  • session only affects the run context; delivery is controlled by target and to.
  • To deliver to a specific channel/recipient, set target + to. With target: "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 target resolves 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 updatedAt is 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:
Precedence: per-account → per-channel → channel defaults → built-in defaults.

What each flag does

  • showOk: sends a HEARTBEAT_OK acknowledgment 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.
If all three are false, OpenClaw skips the heartbeat run entirely (no model call).

Per-channel vs per-account examples

Common patterns

HEARTBEAT.md (optional)

If a HEARTBEAT.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.md to add a daily calendar check.”
  • “Rewrite HEARTBEAT.md so it’s shorter and focused on inbox follow-ups.”
If you want this to happen proactively, you can also include an explicit line in your heartbeat prompt like: “If the checklist becomes stale, update HEARTBEAT.md with a better one.” Safety note: don’t put secrets (API keys, phone numbers, private tokens) into 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:
If multiple agents have 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): agentId or an explicit sessionKey; defaults to the default agent’s main session.
Silent-unless-it-decides. The persona receives the payload as non-user context and produces a user-facing message only when warranted, otherwise acknowledges with 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 wakeGate payload (see Cron jobs)
  • Heartbeat — a heartbeat configured with a wakeGate (see Wake-gate below)
  • Plugin / hookapi.runtime.signals.nudge(...) (see Agent Signals)
  • CLIopenclaw 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. Set agents.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, or wakeAgent !== 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 the Analysis lane, with optional model/provider/lightContext overrides) that inspects sources and returns a structured wake decision. The gate evaluation stays in the isolated lane and never touches the main session.
When the gate signals wake, its 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

Command wake-gates run through the Gateway shell (/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.
Command wake-gates execute with the same host permissions as the Gateway. Only configure commands you trust, keep them idempotent, and prefer short timeouts for checks that call external services.

Model wake-gate example

Model wake-gates use the same 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
When enabled, heartbeats will also deliver a separate message prefixed 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. Keep HEARTBEAT.md small and consider a cheaper model or target: "none" if you only want internal state updates.