OpenClaw can do more than wait for you to type /todo. Its heartbeat system lets an agent wake itself up on a schedule, check context, and push a suggestion before you ask. This guide shows exactly how to wire those heartbeats with cron jobs so your bot feels less reactive and more like a competent assistant.
Why Heartbeats Matter for Proactive Suggestions
Most chat-based agents stay silent until they see a human message. Heartbeats change the model: OpenClaw runs a scheduled check-in inside the daemon, evaluates triggers, then decides whether to post. Users report higher retention when the agent reaches out first, especially for routine nudge scenarios—stand-up reminders, end-of-day wrap-ups, dinner planning, hydration pings, you name it.
Under the hood every heartbeat is just a cron expression stored in the agent config. At runtime the daemon maps those expressions to Node 22 timers and executes your handler. So if you understand cron, you understand heartbeats.
Cron 101 in the OpenClaw Context
OpenClaw sticks with standard cron(5) semantics—minute, hour, day-of-month, month, day-of-week—plus one nice quality-of-life: it supports @ nicknames (@hourly, @daily, @weekly, @monthly, @yearly).
Quick refresher:
- Fields:
*/5 * * * *runs every five minutes. - Lists:
0 9,13,17 * * 1-5fires at 09:00, 13:00, 17:00 on weekdays. - Step values:
0 */3 * * *triggers every three hours. - Timezone: OpenClaw assumes UTC unless you set
timezoneinconfig/agent.yaml. Each heartbeat can override it, but keep one source of truth if you can.
If you are on ClawCloud, the container is locked to UTC for deterministic scaling. Set timezone: "America/Los_Angeles" in your project settings and the platform injects TZ into the container, so cron respects it.
Defining a Heartbeat in agent.yaml
The minimal syntax looks like this:
heartbeats:
dinner-reminder:
schedule: "0 17 * * *" # every day at 17:00
handler: "./heartbeats/dinner.js"
Place that YAML next to package.json and restart the daemon. OpenClaw will read the section, spin up one Node worker per heartbeat, and call your exported function at the right time.
The handler signature is straight JavaScript/TypeScript:
// heartbeats/dinner.js
module.exports = async ({ agent, memory, tools, log }) => {
const planned = await memory.get("dinner_planned_today");
if (planned) {
log.info("Dinner already planned. No ping.");
return;
}
const suggestion = "It's 5 PM and you haven't planned dinner yet. Should I order Thai or open a recipe?";
await agent.say(suggestion);
};
A couple of notes that tripped me up:
agent.say()picks the primary channel you configured when you named your bot on ClawCloud. If you need channel-specific pings, useagent.send(channelId, message).memoryis persisted in Dynamo under the hood when you’re on ClawCloud, or SQLite when you self-host. Either way the API is the same.- Errors bubble up to the daemon log, not the chat. Good. Your users don’t need stack traces.
Using Context for Smarter Suggestions
A heartbeat without context is just a timer. Ideally you want to check inputs—calendar, location, weather, anything—to decide whether to speak up. OpenClaw makes this easy with its 800-plus Composio connectors. You import the tool once and use it like any other npm lib.
// heartbeats/dinner.js (context aware)
const openai = require("openai"); // optional for LLM reasoning
module.exports = async ({ agent, memory, tools, log }) => {
const cal = tools.googleCalendar();
const events = await cal.events.list({
timeMin: new Date().toISOString(),
timeMax: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
});
const cookingWindowFree = events.data.items.every(e => !e.start.dateTime);
const weather = await tools.openWeatherMap().current();
const isRaining = weather.weather[0].main === "Rain";
const planned = await memory.get("dinner_planned_today");
if (planned || !cookingWindowFree) {
log.info("Skip: either dinner already planned or time blocked.");
return;
}
const modelSuggestion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: "You are a concise assistant" },
{ role: "user", content: `It's ${isRaining ? "raining" : "clear"}. Suggest a dinner idea.` }
],
});
await agent.say(`Dinner time incoming: ${modelSuggestion.choices[0].message.content}`);
};
Now the agent pings only when you actually have a free window and haven’t set a plan. It even personalizes based on weather. Small addition, big jump in perceived intelligence.
Testing Heartbeats Locally
You don’t want to wait until 17:00 everyday to debug. The daemon exposes --fast-forward and --run-once flags so you can trigger heartbeats instantly.
# install & run OpenClaw 3.7.2 locally
npm i -g openclaw@3.7.2
# start the gateway in dev mode
openclaw dev --fast-forward "dinner-reminder"
It skips cron validation and runs the handler immediately, then exits. When happy, push to ClawCloud:
# commit and deploy (ClawCloud CLI v0.42.)
clawcloud deploy
The platform container boots, schedules the cron job, and wires logs to the dashboard. Use clawcloud logs -f to tail in real time.
Advanced Patterns: Dynamic Cron via Memory
Sometimes a hard-coded cron line is too rigid. Example: you want the agent to ping 30 minutes before the user’s next calendar event, whatever time that is. Since cron needs an explicit number, the trick is to regenerate the schedule after every run.
- Create a single “scheduler” heartbeat that runs every hour.
- Inside the handler, calculate the next event, then update (or write) a secondary heartbeat with
agent.heartbeats.upsert(id, schedule, handlerPath). - The daemon hot-reloads without restart. When the secondary heartbeat fires, it pings the user and deletes itself.
Pseudo-code:
// heartbeats/scheduler.js
module.exports = async ({ tools, agent }) => {
const nextMeeting = await tools.googleCalendar().nextEvent();
if (!nextMeeting) return;
const trigger = new Date(new Date(nextMeeting.start) - 30 * 60 * 1000);
const cronExpr = `${trigger.getUTCMinutes()} ${trigger.getUTCHours()} ${trigger.getUTCDate()} ${trigger.getUTCMonth()+1} *`;
await agent.heartbeats.upsert("meeting-reminder", cronExpr, "./heartbeats/meeting.js");
};
This pattern lets you stack arbitrary schedules without filling agent.yaml with brittle lines.
Common Pitfalls and Debugging Tips
- Misaligned timezones: If your reminder posts an hour late, 99% of the time
TZis wrong. Logprocess.env.TZat startup and fix the project setting. - Overlapping runs: Heartbeats are not re-entrant by default. If a handler is still running when the next tick comes, OpenClaw queues it. Long tasks can snowball. Set
timeout: "2m"under the heartbeat to fail fast. - Memory leaks: Handlers share nothing but imported libs. Be wary of global caches (e.g.,
openaiinstance) if you mutate them. ClawCloud restarts the container daily, but leaks can still burn RAM in the meantime. - Spam: Nothing will get your bot muted faster than unsolicited walls of text. Keep suggestions short, add an opt-out flag in memory (
memory.set("dinner_nags", false)), and respect it.
Example Walkthrough: 17:00 Dinner Nudge End-to-End
- Create project:
clawcloud init dinnerbot && cd dinnerbot. - Add heartbeat in
agent.yaml:heartbeats: dinner-reminder: schedule: "0 17 * * *" handler: "./heartbeats/dinner.js" timezone: "Europe/Berlin" timeout: "1m" - Write handler (use the context-aware example above).
- Install tools:
npm i openai @composio/google-calendar @composio/open-weather-map. - Link credentials: in the ClawCloud dashboard, add Google Calendar OAuth and OpenWeatherMap API key; they appear in the container as
GOOGLE_CALENDAR_TOKENandOPENWEATHER_API_KEY. - Local dry run:
openclaw dev --fast-forward "dinner-reminder". Confirm the suggestion posts to your test Telegram channel. - Deploy:
clawcloud deploy. The agent is live in ~45 seconds. At 17:00 tomorrow you should see a neatly contextual dinner suggestion.
Total time: 10–15 minutes if you already have the API keys.
Takeaway & Next Step
Heartbeats turn OpenClaw from a passive chat widget into an initiative-taking assistant. Wire one or two cron-backed handlers, respect user context, test with --fast-forward, and you’ll wonder why you ever waited for users to type first. Once you have the dinner nudge working, replicate the pattern for stand-ups, inbox zeros, or hydration reminders. Your future self—and your users—will thank you.