2025-12-09 17:51:05 +00:00
---
2026-01-08 23:06:56 +01:00
summary: "Heartbeat polling messages and notification rules"
2025-12-09 17:51:05 +00:00
read_when:
- Adjusting heartbeat cadence or messaging
2026-01-23 19:00:17 +00:00
- Deciding between heartbeat and cron for scheduled tasks
2026-01-31 16:04:03 -05:00
title: "Heartbeat"
2025-12-09 17:51:05 +00:00
---
2026-01-31 21:13:13 +09:00
2025-12-26 02:35:21 +01:00
# Heartbeat (Gateway)
2025-11-26 17:05:09 +01:00
2026-01-23 19:00:17 +00:00
> **Heartbeat vs Cron?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each.
2026-01-08 23:06:56 +01:00
Heartbeat runs **periodic agent turns** in the main session so the model can
surface anything that needs attention without spamming you.
2025-11-26 17:05:09 +01:00
2026-02-07 10:28:19 -05:00
Troubleshooting: [/automation/troubleshooting ](/automation/troubleshooting )
2026-01-10 22:26:20 +00:00
## Quick start (beginner)
2026-01-21 20:23:30 +00:00
1. Leave heartbeats enabled (default is `30m` , or `1h` for Anthropic OAuth/setup-token) or set your own cadence.
2026-01-10 22:26:20 +00:00
2. Create a tiny `HEARTBEAT.md` checklist in the agent workspace (optional but recommended).
2026-02-25 00:33:32 +00:00
3. Decide where heartbeat messages should go (`target: "none"` is the default; set `target: "last"` to route to the last contact).
2026-01-10 22:26:20 +00:00
4. Optional: enable heartbeat reasoning delivery for transparency.
2026-01-21 20:30:29 +00:00
5. Optional: restrict heartbeats to active hours (local time).
2026-01-10 22:26:20 +00:00
Example config:
```json5
{
agents: {
defaults: {
heartbeat: {
every: "30m",
2026-02-25 00:33:32 +00:00
target: "last", // explicit delivery to last contact (default is "none")
2026-01-21 20:33:19 +00:00
// activeHours: { start: "08:00", end: "24:00" },
2026-01-10 22:26:20 +00:00
// includeReasoning: true, // optional: send separate `Reasoning:` message too
2026-01-31 21:13:13 +09:00
},
},
},
2026-01-10 22:26:20 +00:00
}
```
2026-01-06 22:28:32 +00:00
## Defaults
2026-01-08 23:06:56 +01:00
2026-01-21 20:23:30 +00:00
- 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.
2026-01-09 12:44:23 +00:00
- Prompt body (configurable via `agents.defaults.heartbeat.prompt` ):
2026-01-16 00:46:07 +00:00
`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.`
2026-01-08 23:06:56 +01:00
- The heartbeat prompt is sent **verbatim** as the user message. The system
prompt includes a “Heartbeat” section and the run is flagged internally.
2026-01-21 20:30:29 +00:00
- Active hours (`heartbeat.activeHours` ) are checked in the configured timezone.
Outside the window, heartbeats are skipped until the next tick inside the window.
2026-01-08 23:06:56 +01:00
2026-01-10 22:26:20 +00:00
## What the heartbeat prompt is for
The default prompt is intentionally broad:
2026-01-31 21:13:13 +09:00
2026-01-10 22:26:20 +00:00
- **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 ](/concepts/timezone )).
If you want a heartbeat to do something very specific (e.g. “check Gmail PubSub
2026-01-16 00:46:07 +00:00
stats” or “verify gateway health”), set `agents.defaults.heartbeat.prompt` (or
`agents.list[].heartbeat.prompt` ) to a custom body (sent verbatim).
2026-01-10 22:26:20 +00:00
2026-01-08 23:06:56 +01:00
## Response contract
- If nothing needs attention, reply with ** `HEARTBEAT_OK` **.
2026-01-30 03:15:10 +01:00
- During heartbeat runs, OpenClaw treats `HEARTBEAT_OK` as an ack when it appears
2026-01-08 23:06:56 +01:00
at the **start or end** of the reply. The token is stripped and the reply is
2026-01-12 11:06:37 +00:00
dropped if the remaining content is ** ≤ `ackMaxChars` ** (default: 300).
2026-01-08 23:06:56 +01:00
- 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.
2026-01-05 19:43:54 +01:00
2025-12-26 02:35:21 +01:00
## Config
```json5
{
2026-01-09 12:44:23 +00:00
agents: {
defaults: {
heartbeat: {
2026-01-31 21:13:13 +09:00
every: "30m", // default: 30m (0m disables)
2026-02-05 16:54:44 -05:00
model: "anthropic/claude-opus-4-6",
2026-01-10 22:26:20 +00:00
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
2026-02-25 00:33:32 +00:00
target: "last", // default: none | options: last | none | < channel id > (core or plugin, e.g. "bluebubbles")
2026-01-31 21:13:13 +09:00
to: "+15551234567", // optional channel-specific override
2026-02-05 06:49:12 +09:00
accountId: "ops-bot", // optional multi-account channel id
2026-01-16 00:46:07 +00:00
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.",
2026-01-31 21:13:13 +09:00
ackMaxChars: 300, // max chars allowed after HEARTBEAT_OK
},
},
},
2025-12-26 02:35:21 +01:00
}
```
2026-01-24 09:30:55 +00:00
### 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.
2026-01-16 00:46:07 +00:00
### 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.
```json5
{
agents: {
defaults: {
heartbeat: {
every: "30m",
2026-02-25 00:33:32 +00:00
target: "last", // explicit delivery to last contact (default is "none")
2026-01-31 21:13:13 +09:00
},
2026-01-16 00:46:07 +00:00
},
list: [
{ id: "main", default: true },
{
id: "ops",
heartbeat: {
every: "1h",
target: "whatsapp",
to: "+15551234567",
2026-01-31 21:13:13 +09:00
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.",
},
},
],
},
2026-01-16 00:46:07 +00:00
}
```
2026-02-05 21:18:57 -05:00
### Active hours example
Restrict heartbeats to business hours in a specific timezone:
```json5
{
agents: {
defaults: {
heartbeat: {
every: "30m",
2026-02-25 00:33:32 +00:00
target: "last", // explicit delivery to last contact (default is "none")
2026-02-05 21:18:57 -05:00
activeHours: {
start: "09:00",
end: "22:00",
timezone: "America/New_York", // optional; uses your userTimezone if set, otherwise host tz
},
},
},
},
}
```
Outside this window (before 9am or after 10pm Eastern), heartbeats are skipped. The next scheduled tick inside the window will run normally.
2026-02-20 05:03:57 +04:00
### 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.
2026-02-05 06:49:12 +09:00
### Multi account example
Use `accountId` to target a specific account on multi-account channels like Telegram:
```json5
{
agents: {
list: [
{
id: "ops",
heartbeat: {
every: "1h",
target: "telegram",
2026-02-18 11:01:01 +01:00
to: "12345678:topic:42", // optional: route to a specific topic/thread
2026-02-05 06:49:12 +09:00
accountId: "ops-bot",
},
},
],
},
channels: {
telegram: {
accounts: {
"ops-bot": { botToken: "YOUR_TELEGRAM_BOT_TOKEN" },
},
},
},
}
```
2026-01-08 23:06:56 +01:00
### Field notes
- `every` : heartbeat interval (duration string; default unit = minutes).
2025-12-26 02:35:21 +01:00
- `model` : optional model override for heartbeat runs (`provider/model` ).
2026-01-10 22:26:20 +00:00
- `includeReasoning` : when enabled, also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on` ).
2026-01-19 15:42:07 -05:00
- `session` : optional session key for heartbeat runs.
- `main` (default): agent main session.
2026-01-30 03:15:10 +01:00
- Explicit session key (copy from `openclaw sessions --json` or the [sessions CLI ](/cli/sessions )).
2026-02-07 15:40:35 -05:00
- Session key formats: see [Sessions ](/concepts/session ) and [Groups ](/channels/groups ).
2026-01-08 23:06:56 +01:00
- `target` :
2026-02-25 00:33:32 +00:00
- `last` : deliver to the last used external channel.
2026-01-23 16:45:37 -06:00
- explicit channel: `whatsapp` / `telegram` / `discord` / `googlechat` / `slack` / `msteams` / `signal` / `imessage` .
2026-02-25 00:33:32 +00:00
- `none` (default): run the heartbeat but **do not deliver** externally.
2026-02-18 11:01:01 +01:00
- `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>` .
2026-02-05 06:49:12 +09:00
- `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.
2026-01-08 23:06:56 +01:00
- `prompt` : overrides the default prompt body (not merged).
- `ackMaxChars` : max chars allowed after `HEARTBEAT_OK` before delivery.
2026-02-16 13:29:24 -06:00
- `suppressToolErrorWarnings` : when true, suppresses tool error warning payloads during heartbeat runs.
2026-02-20 05:03:57 +04:00
- `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` .
2026-02-05 21:18:57 -05:00
- 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.
2026-02-20 05:03:57 +04:00
- `start` and `end` must not be equal for an active window; equal values are treated as zero-width (always outside the window).
2026-02-05 21:18:57 -05:00
- Outside the active window, heartbeats are skipped until the next tick inside the window.
2025-12-26 02:35:21 +01:00
2026-01-08 23:06:56 +01:00
## Delivery behavior
2026-01-19 15:42:07 -05:00
- 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.
2026-01-08 23:06:56 +01:00
- If the main queue is busy, the heartbeat is skipped and retried later.
- 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.
2026-01-06 21:54:19 +00:00
2026-01-24 09:06:07 +00:00
## Visibility controls
By default, `HEARTBEAT_OK` acknowledgments are suppressed while alert content is
delivered. You can adjust this per channel or per account:
```yaml
channels:
defaults:
heartbeat:
2026-01-31 21:13:13 +09:00
showOk: false # Hide HEARTBEAT_OK (default)
showAlerts: true # Show alert messages (default)
2026-01-24 09:06:07 +00:00
useIndicator: true # Emit indicator events (default)
telegram:
heartbeat:
2026-01-31 21:13:13 +09:00
showOk: true # Show OK acknowledgments on Telegram
2026-01-24 09:06:07 +00:00
whatsapp:
accounts:
work:
heartbeat:
showAlerts: false # Suppress alert delivery for this account
```
Precedence: per-account → per-channel → channel defaults → built-in defaults.
2026-01-24 09:30:55 +00:00
### 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.
2026-01-30 03:15:10 +01:00
If **all three** are false, OpenClaw skips the heartbeat run entirely (no model call).
2026-01-24 09:30:55 +00:00
### Per-channel vs per-account examples
```yaml
channels:
defaults:
heartbeat:
showOk: false
showAlerts: true
useIndicator: true
slack:
heartbeat:
showOk: true # all Slack accounts
accounts:
ops:
heartbeat:
showAlerts: false # suppress alerts for the ops account only
telegram:
heartbeat:
showOk: true
```
### Common patterns
2026-01-31 21:13:13 +09:00
| Goal | Config |
| ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| Default behavior (silent OKs, alerts on) | _(no config needed)_ |
2026-01-24 09:30:55 +00:00
| Fully silent (no messages, no indicator) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: false }` |
2026-01-31 21:13:13 +09:00
| Indicator-only (no messages) | `channels.defaults.heartbeat: { showOk: false, showAlerts: false, useIndicator: true }` |
| OKs in one channel only | `channels.telegram.heartbeat: { showOk: true }` |
2026-01-24 09:30:55 +00:00
2026-01-06 21:54:19 +00:00
## HEARTBEAT.md (optional)
2026-01-08 23:06:56 +01:00
2026-01-06 21:54:19 +00:00
If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
2026-01-10 22:26:20 +00:00
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
safe to include every 30 minutes.
2026-01-24 04:19:01 +00:00
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
2026-01-30 03:15:10 +01:00
headers like `# Heading` ), OpenClaw skips the heartbeat run to save API calls.
2026-01-24 04:19:01 +00:00
If the file is missing, the heartbeat still runs and the model decides what to do.
2026-01-10 22:26:20 +00:00
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
Example `HEARTBEAT.md` :
```md
# Heartbeat checklist
- Quick scan: anything urgent in inboxes?
- If it’ s daytime, do a lightweight check-in if nothing else is pending.
2026-01-31 21:13:13 +09:00
- If a task is blocked, write down _what is missing_ and ask Peter next time.
2026-01-10 22:26:20 +00:00
```
### 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:
2026-01-31 21:13:13 +09:00
2026-01-10 22:26:20 +00:00
- “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.
2026-01-06 21:54:19 +00:00
2026-01-08 23:06:56 +01:00
## Manual wake (on-demand)
You can enqueue a system event and trigger an immediate heartbeat with:
```bash
2026-01-30 03:15:10 +01:00
openclaw system event --text "Check for urgent follow-ups" --mode now
2026-01-08 23:06:56 +01:00
```
2026-01-16 00:46:07 +00:00
If multiple agents have `heartbeat` configured, a manual wake runs each of those
agent heartbeats immediately.
2026-01-08 23:06:56 +01:00
Use `--mode next-heartbeat` to wait for the next scheduled tick.
2026-01-10 22:26:20 +00:00
## Reasoning delivery (optional)
By default, heartbeats deliver only the final “answer” payload.
If you want transparency, enable:
2026-01-31 21:13:13 +09:00
2026-01-10 22:26:20 +00:00
- `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.
2026-01-08 23:06:56 +01:00
## 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.