OpenClaw heartbeat: what your agent does every 30 minutes, and what it costs
OpenClaw's heartbeat wakes your agent twice an hour and carries your whole conversation each time: 28k to 107k tokens a run, measured. The cost, and how to cut it.
The heartbeat is a system-owned automation that wakes your agent for a full turn every 30 minutes by default (every hour when it signs in to Anthropic with OAuth or a token), asks whether anything needs attention, and expects NO_REPLY unless something does. Since OpenClaw 2 its alerts go to the owner’s direct message. Each turn runs in your main conversation on the agent’s model, so it carries that whole conversation and grows with it: about 28k tokens a run on a fresh install, 77k on average on an agent eleven days into everyday use, where 197 silent heartbeats in four days were half of every token it sent to a model. Measured on GPT-5.6 Luna, idle heartbeats cost about 2.5¢ each when the prompt cache missed, about $1.20 a day at the default pace. Under agents.defaults.heartbeat, isolatedSession: true runs each one in a fresh session and every: "0m" stops the schedule.
What a heartbeat turn actually does
It is not a ping. The heartbeat docs describe it as running “periodic agent turns in the main session so the model can surface anything that needs attention without spamming you,” and each turn is a real one: the agent gets the same system prompt as an ordinary message, with its workspace files and its tools, plus this verbatim instruction:
Follow the heartbeat monitor scratch context when provided. Recurring
tasks are automations; create or change their schedules with the
automations tool, not heartbeat scratch. Do not infer or repeat old tasks
from prior chats. If nothing needs attention, reply NO_REPLY.CopiedSo the agent is being asked, twice an hour, to look around and decide whether to speak. Most of the time it replies NO_REPLY and you never see the turn. When it judges something worth raising, the reply is delivered as an alert. The heartbeat depends on automations being enabled: with cron.enabled: false there are no scheduled heartbeats at all. Recurring work you actually want done on a schedule belongs in an automation, which is what the prompt itself tells the agent.
Why OpenClaw 2 made it visible
The heartbeat existed in the 2026.7 line too, on by default at 30 minutes, but its delivery target was none: the turns ran and nobody heard from them. OpenClaw 2 changed the default to the owner. The current docs put it as alerts going to “the operator’s direct message by default,” resolved from commands.ownerAllowFrom or an explicit channel allowFrom. Quiet acknowledgements are suppressed; only alerts get through, and the first one explains the periodic checks and how to turn the messages off. If you upgraded and your agent has started sending you unprompted Telegram messages at odd hours, this is the mechanism, not a haunting.
What it costs: it grows with the conversation
A heartbeat’s input is whatever its session holds, and by default that is the main session: the system prompt, the tool schemas, the workspace files and the conversation so far. Per OpenClaw’s session docs, all your direct messages share that session by default, and it is never reset automatically: it keeps growing until you start a new one with /new or /reset, and compaction summarizes it only as it nears the model’s context limit. So a heartbeat carries least on the day you install OpenClaw, and more the longer you talk to the agent without starting a new session.
Measured on a fresh install (OpenClaw 2026.9.1, September 4, 2026), a heartbeat carried about 28k tokens, almost all of it the system prompt and the tool schemas. Measured on an agent eleven days into everyday use (OpenClaw 2026.9.4), over its last four days, September 20 to 24: 197 heartbeats, one every 30 minutes, every one answered NO_REPLY. They carried 77k tokens a run on average, and the latest carried 107k with none of it served from cache. Between them they sent 15.2 million tokens to the model and got back 2,472, so the cost is almost all input. That was half of every token the agent sent to a model in those four days (15.2 million of 30.1 million); its owner’s 19 turns made up most of the rest.
| Measured, 2026 | Agent and model | Tokens a run | Cost a run |
|---|---|---|---|
Sep 4 | Fresh install; GPT-5.6 Luna, paid per token, cache applying | About 28k | About $0.0012 |
Sep 18 | Idle agent; GPT-5.6 Luna, paid per token, cache missing | About 100k to 120k, from the price | About $0.025 |
Sep 20 to 24 | Eleven days into everyday use; GPT-5.6 Sol on a ChatGPT subscription | 77k average, 107k latest | Not billed per token |
The September 18 agent sat idle for about twelve hours and spent $0.61, about 25 heartbeats at roughly 2.5¢ each. At GPT-5.6 Luna’s list price of $0.20 per million input tokens (its price then and on October 1), that is the whole context billed at full rate: from the $0.02 cached price, each poll would have had to carry over a million tokens. At that rate, 48 polls come to about $1.20 a day with nobody talking to the agent. The heartbeat also runs on whatever model the agent uses: a single heartbeat on an agent running Claude Opus 5.5 cost $0.27 on September 23.
For your own agent the arithmetic is the context times 48 a day. At 100k tokens that is 4.8 million input tokens a day. On list prices of October 1, 2026, from OpenAI’s price list and Anthropic’s, that is $0.48 a day on GPT-6 Luna ($0.10 per million input tokens), $19.20 on Claude Opus 5.5 ($4), and $48 on GPT-6 Astra ($10), which is the model a fresh OpenClaw setup with an OpenAI API key selects. Read from the prompt cache, the same tokens cost a tenth of that, a twentieth on Opus 5.5; written to the cache on a miss, a quarter more. These are arithmetic on list prices, not bills: where an agent lands depends on how big its conversation has grown and on the cache.
The cache decides which price you pay
OpenAI’s prompt caching guide says that on GPT-5.6 and later a cached prefix stays eligible for at least 30 minutes after it was last written or read, the same length as the heartbeat’s default interval. Anthropic’s default cache lasts five minutes, or an hour if you pay twice the input price to write it. On an agent nobody is talking to, then, each heartbeat arrives at the edge of OpenAI’s window, or long after Anthropic’s default one has closed. That comes from the documentation, and it fits what was measured: the idle Luna polls were billed at full rate, and on the subscription agent the share of heartbeat input served from cache was 88% on September 21 and 25% on September 24.
To see your own numbers, send /status in the chat: per the token-use docs, it shows the session’s context usage, which is roughly what the next heartbeat will carry, and /usage cost summarizes cost from the session logs. The model guide covers how to tell from your provider’s activity log whether the cache is applying.
The levers
Everything lives under agents.defaults.heartbeat (or per agent under agents.entries.<id>.heartbeat); the docs are explicit that you edit this block, not the automation job it creates.
heartbeat.isolatedSession # true: a fresh session each run, no conversation history
heartbeat.lightContext # true: skip the workspace bootstrap files as well
heartbeat.model # a different model for heartbeat runs only
heartbeat.every # "30m" default, "1h" on Anthropic OAuth; "0m" stops the schedule
heartbeat.activeHours # { start: "09:00", end: "22:00" } skips the night
heartbeat.target # "owner" default; "none" keeps alerts internal
heartbeat.session # which session it runs in; the main one by defaultCopiedThe session setting is the one that answers the growth. With isolatedSession: true each heartbeat starts from a fresh session with no conversation history, so what it carries stops growing with your chat; the docs’ own figure is a fall from around 100K tokens to 2 to 5K a run. Adding lightContext: true trims the workspace files too. The heartbeat keeps its schedule, its prompt and its alerts, which still go to the same conversation. What it loses is its view of the recent chat, and the default prompt already tells it not to take tasks from prior chats.
model moves heartbeat turns onto another model, which matters most when the agent runs on a flagship. In the main session the heartbeat model still reads the whole conversation, and the docs warn that one with a small context window can leave the next ordinary turn overflowing, so pair a small heartbeat model with isolatedSession. If your agent runs on a ChatGPT subscription, a pay-per-token heartbeat model moves those turns from the plan’s usage limits onto a bill.
every and activeHours scale the count: an hour halves the 48 runs a day, and a 09:00 to 22:00 window cuts them to 26, with the runs outside it skipped rather than queued. The token-use docs also use the interval the other way: with Anthropic’s one-hour cache (cacheRetention: "long"), a heartbeat every 55 minutes can keep the cache warm, so an idle conversation is not written to it in full again. target changes who hears an alert, not whether the turn runs: "none" still pays for every one.
To stop it, set every to "0m". That removes the recurring schedule but keeps event-driven wakes: a background command finishing can still run one turn, and tools.exec.notifyOnExit: false stops those as well. On a running gateway, the system commands pause and resume heartbeats:
openclaw system heartbeat disable
openclaw system heartbeat enableCopiedHEARTBEAT.md, if you have one
Older installs kept a HEARTBEAT.md in the workspace listing things to check on each beat. The runtime no longer reads it. After an upgrade, openclaw doctor --fix moves the file into the heartbeat’s monitor scratch, a short checklist appended to every heartbeat prompt, turns any valid tasks: entries into independently scheduled automations, archives the original and removes the file. What doctor does covers the rest of that migration. Because the scratch rides along on every run, keep it to a few lines; print or replace it with the commands below, taking the job id from openclaw cron list --all.
openclaw cron scratch <jobId>
openclaw cron scratch <jobId> --set "..."Copied