What is heartbeat in OpenClaw and how to build it elegantly
In my previous post about building an OpenClaw-style runtime with Mastra, I mentioned heartbeat as a useful background mechanism. At that time, heartbeat was still close to a timer job.
Now I have upgraded it into something much closer to human behavior: it can observe recent conversation context, decide when follow-up is needed, and proactively reach out in the right channel.
This post explains what heartbeat should be in OpenClaw systems, why naive cron implementations fail, and the architecture I used to make it elegant.
What heartbeat really means in OpenClaw
Heartbeat is not “run every N minutes and do something.”
A good heartbeat is a lightweight background mind that asks:
- Which conversations are likely unfinished?
- Which users might need a nudge or a wrap-up?
- Is now an appropriate moment to reach out?
- Should I stay silent?
In other words, heartbeat is about continuity of care, not just scheduling.
The old version: heartbeat as simple cron
My first version was basically cron-driven behavior living inside ChannelManager. It worked for basic automation, but it had structural problems:
- heartbeat logic was mixed with channel orchestration responsibilities
- it was harder to reason about behavior and safety boundaries
- heartbeat context could leak into user-facing memory threads
- proactive outreach was limited because heartbeat lacked a global recent-activity view
This version was functional, but not elegant.
The upgrade roadmap: from timer to behavior engine
I moved heartbeat through four concrete architecture upgrades.
1) Extract heartbeat into its own scheduler
I created a dedicated HeartbeatScheduler at /src/mastra/schedulers/heartbeat-scheduler.ts.
That removed heartbeat logic from ChannelManager so each part has one job:
ChannelManagermanages channels and conversationsHeartbeatSchedulerruns background follow-up behavior
This separation made reasoning, testing, and future iteration much cleaner.
2) Give heartbeat scoped tools, not broad power
I added heartbeat-specific tools in /src/mastra/tools/channel-tools.ts:
getRecentConversationssendMessageToChannel
These tools are only exposed during heartbeat runs. That keeps power narrowly scoped and avoids accidental misuse in normal interactive flows.
3) Isolate heartbeat memory from user threads
Heartbeat now runs in its own dedicated memory thread:
- resource:
heartbeat - thread:
heartbeat
This is critical. Heartbeat can reason over system-level follow-up decisions without polluting any user’s conversation memory.
4) Wire real activity signals through the event bus
In /src/mastra/index.ts, heartbeat is instantiated alongside ChannelManager, and event subscriptions track conversation activity.
The result: heartbeat no longer runs blind. It has a recent activity picture and can make context-aware decisions.
Why this feels more human now
With this architecture, heartbeat behaves less like a robot timer and more like a careful assistant:
- Observe recent conversations
- Decide whether follow-up is necessary
- Reach out in the right channel only when useful
- Stay silent when no intervention is needed
That “decide to do nothing” step is where human-like quality starts.
Design principles that made it elegant
The biggest lessons from this refactor:
- Separation of concerns: scheduling behavior should not live inside channel plumbing.
- Capability scoping: give background agents the minimum tools they need.
- Memory hygiene: isolate operational cognition from user conversation state.
- Signal-driven behavior: use real activity data, not fixed assumptions.
- Silence as a feature: proactive systems must also know when not to speak.
Where this roadmap goes next
The current version already supports proactive follow-up with clean boundaries. The next evolution is better policy intelligence:
- urgency scoring for unfinished threads
- richer “follow up vs wait” heuristics
- stronger tone adaptation by channel and user preference
- explicit audit traces for why heartbeat acted
That is the direction from automation toward trustworthy, human-like runtime behavior.
If you are building OpenClaw-style agents, my strongest recommendation is this: treat heartbeat as a first-class behavioral subsystem, not a cron afterthought.