Skip to content

Keep Your Agents Proactive with Heartbeat

Heartbeat vs Cron? See Cron vs Heartbeat for guidance on when to use each.

Heartbeat runs periodic agent turns in your main session. This allows the model to surface anything that needs your attention without sending too many messages.

A heartbeat is a scheduled turn in the main session. It does not create background task records. You should use task records for detached work like ACP runs, subagents, or isolated cron jobs.

If you need help, check out the troubleshooting guide: /automation/troubleshooting

  1. Leave heartbeats enabled. The default is 30m, or 1h if you use Anthropic OAuth/setup-token. You can also set your own cadence.
  2. You should create a small HEARTBEAT.md checklist in your agent workspace. This is optional but recommended.
  3. Pick where heartbeat messages go. The default is target: "none", but you can set target: "last" to route them to the last contact.
  4. You can enable heartbeat reasoning delivery if you want more transparency.
  5. You can use lightweight bootstrap context if your heartbeat runs only need HEARTBEAT.md.
  6. You can enable isolated sessions. This avoids sending the full conversation history with each heartbeat.
  7. You can restrict heartbeats to your active hours in local time.

Example config:

{
agents: {
defaults: {
heartbeat: {
every: "30m",
target: "last", // explicit delivery to last contact (default is "none")
directPolicy: "allow", // default: allow direct/DM targets; set "block" to suppress
lightContext: true, // optional: only inject HEARTBEAT.md from bootstrap files
isolatedSession: true, // optional: fresh session each run (no conversation history)
// activeHours: { start: "08:00", end: "24:00" },
// includeReasoning: true, // optional: send separate `Reasoning:` message too
},
},
},
}
  • Interval: 30m. It changes to 1h when the system detects Anthropic OAuth/setup-token auth. You can set agents.defaults.heartbeat.every or use agents.list[].heartbeat.every for specific agents. Set it to 0m to turn it off.
  • Prompt body: You can configure this via agents.defaults.heartbeat.prompt. The default is: 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 system sends the heartbeat prompt verbatim as a user message. The system prompt includes a “Heartbeat” section and the run is flagged internally.
  • The system checks your active hours (heartbeat.activeHours) using your configured timezone. If you are outside the window, it skips heartbeats until the next scheduled time inside the window.

The default prompt is broad to cover two main areas:

  • Background tasks: It uses “Consider outstanding tasks” to nudge the agent. This helps it review follow-ups like your inbox, calendar, or queued work and bring up urgent items.
  • Human check-in: It uses “Checkup sometimes on your human during day time” for light check-ins. It avoids sending messages at night by using your local timezone (see /concepts/timezone).

Heartbeat can react to background tasks that are finished, but the heartbeat run itself does not create a task record.

If you need a heartbeat to do a specific job, like checking Gmail stats or verifying gateway health, you can set a custom body in agents.defaults.heartbeat.prompt or agents.list[].heartbeat.prompt. This is sent verbatim.

  • If the agent finds nothing that needs attention, it should reply with HEARTBEAT_OK.
  • During these runs, OpenClaw looks for HEARTBEAT_OK at the start or end of the reply. It strips the token and drops the reply if the remaining text is ≤ ackMaxChars (the default is 300).
  • If HEARTBEAT_OK appears in the middle of a reply, the system does not treat it as a special signal.
  • When sending alerts, the agent should not include HEARTBEAT_OK. It should only return the alert text.

If a stray HEARTBEAT_OK appears at the start or end of a message outside of a heartbeat run, the system strips and logs it. If the message is only HEARTBEAT_OK, it is dropped.

You can set up heartbeats in your configuration to keep your agents active and responsive. Here is how the basic structure looks in your config.json5:

{
agents: {
defaults: {
heartbeat: {
every: "30m", // default: 30m (0m disables)
model: "anthropic/claude-opus-4-6",
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "bluebubbles")
to: "+15551234567", // optional channel-specific override
accountId: "ops-bot", // optional multi-account channel id
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.",
ackMaxChars: 300, // max chars allowed after HEARTBEAT_OK
},
},
},
}

You should keep in mind how these settings layer over each other:

  • agents.defaults.heartbeat handles your global heartbeat behavior.
  • agents.list[].heartbeat merges on top. If you give an agent a heartbeat block, only those agents will run heartbeats.
  • channels.defaults.heartbeat sets the visibility defaults for all your channels.
  • channels.<channel>.heartbeat lets you override those channel defaults.
  • channels.<channel>.accounts.<id>.heartbeat is for multi-account channels and overrides per-channel settings.

If you include a heartbeat block in any agents.list[] entry, the system assumes only those specific agents need to run heartbeats. This block merges with your agents.defaults.heartbeat, so you can define shared settings once and just tweak what you need for each agent.

In this example, only the second agent runs heartbeats:

{
agents: {
defaults: {
heartbeat: {
every: "30m",
target: "last", // explicit delivery to last contact (default is "none")
},
},
list: [
{ id: "main", default: true },
{
id: "ops",
heartbeat: {
every: "1h",
target: "whatsapp",
to: "+15551234567",
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.",
},
},
],
},
}

You might want to limit heartbeats to business hours in a specific timezone. You can do that like this:

{
agents: {
defaults: {
heartbeat: {
every: "30m",
target: "last", // explicit delivery to last contact (default is "none")
activeHours: {
start: "09:00",
end: "22:00",
timezone: "America/New_York", // optional; uses your userTimezone if set, otherwise host tz
},
},
},
},
}

If it is before 9am or after 10pm Eastern, the system skips the heartbeat. The next scheduled tick inside that window will run normally.

If you need heartbeats running around the clock, you have two options:

  • Just leave out activeHours entirely. This is the default behavior.
  • Set a full-day window: activeHours: { start: "00:00", end: "24:00" }.

Avoid setting the start and end to the exact same time (like 08:00 to 08:00). The system sees that as a zero-width window, so heartbeats are always skipped.

When you use multi-account channels like Telegram, use accountId to point to a specific account:

{
agents: {
list: [
{
id: "ops",
heartbeat: {
every: "1h",
target: "telegram",
to: "12345678:topic:42", // optional: route to a specific topic/thread
accountId: "ops-bot",
},
},
],
},
channels: {
telegram: {
accounts: {
"ops-bot": { botToken: "YOUR_TELEGRAM_BOT_TOKEN" },
},
},
},
}
  • every: This is your heartbeat interval. It is a duration string, and the default unit is minutes.
  • model: Use this if you want to override the model for heartbeat runs using the provider/model format.
  • includeReasoning: When you turn this on, you get a separate Reasoning: message when it is available (same as /reasoning on).
  • lightContext: When this is true, the heartbeat uses a lightweight bootstrap context and only keeps HEARTBEAT.md from your workspace files.
  • isolatedSession: This runs each heartbeat in a fresh session with no prior history. It uses the same isolation pattern as cron sessionTarget: "isolated". This is great for saving tokens. You can pair it with lightContext: true for maximum savings. Note that delivery routing still uses the main session context.
  • session: This is an optional session key for your heartbeat runs.
    • main (default): The agent’s main session.
    • You can also use an explicit session key from openclaw sessions --json or the sessions CLI.
    • Check Sessions and Groups for key formats.
  • target:
    • last: Sends to the last external channel you used.
    • Explicit channel: Any configured channel or plugin ID, like discord, matrix, telegram, or whatsapp.
    • none (default): The heartbeat runs, but do not deliver externally.
  • directPolicy: This manages DM delivery behavior:
    • allow (default): DMs are allowed.
    • block: Stops DM delivery with a reason=dm-blocked status.
  • to: An optional recipient override. This could be a channel-specific ID, like E.164 for WhatsApp or a Telegram chat ID. For Telegram topics, use <chatId>:topic:<messageThreadId>.
  • accountId: Use this for multi-account channels. If you use target: "last", it applies to the resolved channel if that channel supports accounts. If the ID doesn’t match a configured account, the delivery is skipped.
  • prompt: This replaces the default prompt body and is not merged.
  • ackMaxChars: This sets the max characters allowed after HEARTBEAT_OK before delivery happens.
  • suppressToolErrorWarnings: Set this to true to hide tool error warning payloads during heartbeat runs.
  • activeHours: This limits heartbeats to a specific window. It is an object with start (HH:MM, inclusive), end (HH:MM, exclusive), and an optional timezone.
    • If you omit this or use "user", it uses your agents.defaults.userTimezone or the host system time.
    • "local" always uses the host system timezone.
    • You can use any IANA identifier like America/New_York.
    • Remember that start and end cannot be equal.
    • Heartbeats are skipped outside this window until the next tick inside the window.
  • By default, heartbeats run in the agent’s main session (agent:<id>:<mainKey>). If session.scope is set to "global", they run there instead. You can use session to point to a specific channel session like Discord or WhatsApp.
  • The session setting only changes the context of the run. Where the message actually goes is controlled by target and to.
  • If you want to send to a specific place, set both target and to. Using target: "last" will send the message to the last external channel used in that session.
  • Heartbeats allow DM targets by default. You can use directPolicy: "block" to stop these sends while still letting the heartbeat run.
  • If the main queue is busy, the system skips the heartbeat and tries again later.
  • If your target doesn’t resolve to a real destination, the heartbeat still runs, but no message is sent out.
  • Heartbeat-only replies do not keep a session alive. The system restores the last updatedAt timestamp so your idle expiry settings still work correctly.
  • If you have detached background tasks, they can trigger a system event to wake the heartbeat if the main session needs to notice something fast. This wake doesn’t force the heartbeat to run a background task itself.

Managing how often your agent pings you is key to avoiding notification fatigue. By default, OpenClaw keeps things quiet by suppressing HEARTBEAT_OK acknowledgments while still delivering actual alert content. You can customize this behavior for specific channels or even individual accounts:

channels:
defaults:
heartbeat:
showOk: false # Hide HEARTBEAT_OK (default)
showAlerts: true # Show alert messages (default)
useIndicator: true # Emit indicator events (default)
telegram:
heartbeat:
showOk: true # Show OK acknowledgments on Telegram
whatsapp:
accounts:
work:
heartbeat:
showAlerts: false # Suppress alert delivery for this account

The settings follow a clear hierarchy: per-account overrides per-channel, which overrides channel defaults, which finally overrides the built-in defaults.

  • 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 you set all three to false, OpenClaw skips the heartbeat run entirely. This means no model call happens, saving you API costs.

You can mix and match these settings to ensure you only get notified where it matters. For instance, you might want OK confirmations on Slack but not on other platforms.

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
GoalConfig
Default behavior (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 onlychannels.telegram.heartbeat: { showOk: true }

If you place a HEARTBEAT.md file in your workspace, the default prompt tells the agent to read it. You can think of this as your “heartbeat checklist”—a small, stable set of reminders that are safe to include every 30 minutes.

If HEARTBEAT.md exists but is effectively empty (containing only blank lines or markdown headers like # Heading), OpenClaw skips the heartbeat run to save API calls. If the file is missing entirely, the heartbeat still runs and the model decides what to do based on its general instructions.

You should keep this file tiny. A short checklist or a few reminders work best to avoid prompt bloat.

Example HEARTBEAT.md:

# Heartbeat checklist
- Quick scan: anything urgent in inboxes?
- If it’s daytime, do a lightweight check-in if nothing else is pending.
- If a task is blocked, write down _what is missing_ and ask Peter next time.

Yes, you can ask it to do this.

Since HEARTBEAT.md is just a normal file in the agent workspace, you can tell the agent in a standard chat to modify it. You might say:

  • “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 without your intervention, you can include an explicit instruction in your heartbeat prompt like: “If the checklist becomes stale, update HEARTBEAT.md with a better one.”

One safety note: do not put secrets like API keys, phone numbers, or private tokens into HEARTBEAT.md. It becomes part of the prompt context and could be exposed.

Sometimes you need to trigger a heartbeat right away instead of waiting for the schedule. You can enqueue a system event and force an immediate response using this command:

Terminal window
openclaw system event --text "Check for urgent follow-ups" --mode now

If you have multiple agents with heartbeat configured, running a manual wake triggers each of those agent heartbeats at the same time.

If you aren’t in a rush, you can use --mode next-heartbeat to wait for the next scheduled tick.

By default, heartbeats only send you the final “answer” payload.

If you want more transparency into how the agent is thinking, you can enable this option:

  • agents.defaults.heartbeat.includeReasoning: true

When you turn this on, heartbeats deliver a separate message prefixed with Reasoning: (it follows the same format as /reasoning on). This is helpful if the agent is managing several sessions or codexes and you want to see exactly why it decided to ping you. Keep in mind that this can reveal more internal details than you might want, so you should probably keep it turned off in group chats.

Heartbeats run full agent turns. This means shorter intervals will burn through your tokens faster. To keep your costs under control, you should try these settings:

  • Use isolatedSession: true. This avoids sending the full conversation history. You can see usage drop from roughly 100K tokens to about 2-5K per run.
  • Use lightContext: true. This limits bootstrap files to just HEARTBEAT.md.
  • Set a cheaper model. For example, ollama/llama3.2:1b is a good choice.
  • Keep your HEARTBEAT.md file small.
  • Use target: "none" if you only want internal state updates.

AI Setup Assistant

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.