If you only talk to OpenClaw when you remember to, you are leaving a lot of leverage on the table. The heartbeat system flips the model: the agent checks in with you on a schedule, reminds you of loose threads, and offers to handle grunt work before it piles up. This guide shows exactly how to wire those heartbeats—cron, config, and message templates—so the pings feel like a helpful colleague, not a bot that forgot to log off.

Why bother with an OpenClaw heartbeat?

I started using heartbeats to keep personal side projects moving. The first week I got:

  • a morning DM with my calendar and a prompt to draft replies for yesterday’s unread email,
  • a lunchtime nudge that a GitHub Actions run was failing on a stale branch,
  • a 20:00 reminder to stand up from my chair because my Apple Watch ran out of battery.

All of that happened without me typing a single slash command. The lift was an hour of setup and a dozen lines of YAML.

Prerequisites

This article assumes:

  • OpenClaw v3.9.1+ (Node 22 or newer). Older versions lacked the heartbeat block.
  • You already have the gateway running (npm run gateway) and the daemon registered as a system service.
  • The agent is connected to at least one chat integration (Slack, Telegram, or WhatsApp). I use Slack in the examples.
  • Shell access to the box that runs the daemon. Cron examples are POSIX-ish; if you live in Windows land, use the Task Scheduler equivalent.

OpenClaw heartbeat anatomy

Heartbeat is a lightweight event generator inside the daemon. On every tick it emits heartbeat:tick. Any skill or tool can subscribe to that event and decide whether to speak up.

The moving parts:

  1. Daemon schedule. Configurable with heartbeat.interval (seconds) or heartbeat.cron.
  2. Message templates. Simple Mustache strings that can reference memory, calendars, or any tool output.
  3. Guard rails. Rate limiting and quiet hours so the agent does not morph into Clippy.

Configuring the daemon heartbeat interval

You can declare the schedule in .openclaw/daemon.yml. Two styles exist. Use only one.

Option A: Interval in seconds

Good for fast demos or IoT-style monitoring.

heartbeat: interval: 300 # every 5 minutes quietHours: start: "22:00" # local time end: "07:30"

Option B: Cron syntax

Better for human-friendly schedules, e.g. weekdays at 08:00.

heartbeat: cron: "0 8 * * 1-5" # 08:00 Monday–Friday timezone: "Europe/Berlin"

Internally OpenClaw uses cron-parser. Anything that library accepts will work. Remember that macOS’s crontab and Linux differ on the sixth field; stick to five fields unless you need seconds.

Wiring heartbeat check-ins with system cron

The daemon already keeps time, so why touch system cron? Two reasons:

  1. If the daemon crashes, cron will restart it and guarantee the next heartbeat.
  2. Containerised deployments (Docker, k8s) often rely on orchestrator-level schedules instead of in-process timers. I keep both to avoid surprises.

Minimal crontab entry:

# /etc/cron.d/openclaw-heartbeat # run every 10 minutes and send a SIGUSR1 to trigger an immediate tick */10 * * * * clawuser pkill -USR1 -f "node .*openclaw/daemon.js"

By default a SIGUSR1 forces a heartbeat:tick regardless of the configured interval. The daemon then back-offs if that would exceed rate limits.

Systemd timer alternative

On machines where cron is frowned upon:

# /etc/systemd/system/openclaw-heartbeat.timer [Unit] Description=OpenClaw Heartbeat Trigger [Timer] OnCalendar=*:0/10 Persistent=true [Install] WantedBy=timers.target

Enable with systemctl enable --now openclaw-heartbeat.timer.

Crafting helpful proactive messages

A heartbeat that only says “Ping!” is useless. The trick is to compose short, high-signal prompts. I rely on mustache plus a thin JavaScript helper.

Template directory layout

.openclaw/ ├─ templates/ │ ├─ morning.md │ └─ overdue.md └─ skills/ └─ heartbeat.js

morning.md

Good morning, {{user.firstName}} :coffee: You have {{inboxUnread}} unread emails and {{calendarEventsToday}} events today. Top one-liner reply suggestions: {{#replySuggestions}} • {{.}} {{/replySuggestions}} Anything you want me to handle?

heartbeat.js

import {render} from "mustache"; import fs from "node:fs/promises"; import {gmail, calendar} from "@openclaw/composio"; export async function onHeartbeat({agent, memory}) { const now = new Date(); if (now.getHours() !== 8) return; // only morning ping const inboxUnread = await gmail.countUnread(); const calendarEventsToday = await calendar.events.today(); const replySuggestions = await gmail.draftSmartReplies({limit: 3}); const template = await fs.readFile("templates/morning.md", "utf8"); const msg = render(template, { user: agent.owner, inboxUnread, calendarEventsToday, replySuggestions }); agent.sendMessage(msg); }

Save the file, restart the daemon, and tomorrow at 08:00 the agent will DM you on Slack with a tight summary and three AI-generated replies ready to send.

Rate limiting and quiet hours

OpenClaw ships with sane defaults: max 4 proactive messages per hour, silence between 22:00 and 07:00. Edit them in daemon.yml:

heartbeat: maxPerHour: 6 quietHours: start: "00:00" end: "07:00"

The guard is global. If multiple heartbeat skills fire inside the same hour, the excess will be dropped. I learnt this the hard way when an alias loop spawned 127 “Remember to hydrate” reminders.

Example: the surprising utility of a weekly retro

The check-in that paid for itself is the Friday retro. Every Friday 17:00 the agent asks two questions and appends relevant context:

  1. What felt like progress this week?
  2. Where did things drag?

Underneath, it pastes merged PRs, calendar events over 30 min, and any todo that stayed open all week. I answer in the chat thread, the agent stores the text in a weekly_retros vector store, and on Monday morning the “morning.md” heartbeat fetches the most recent retro to suggest first tasks.

The loop cost 20 minutes to script and now keeps my side projects from drifting.

Debugging heartbeat failures

  • No message arrives. Check logs/daemon.log for “rate-limit” or “quietHours” entries. Nine times out of ten that’s the culprit.
  • Daemon does not emit ticks. Run NODE_DEBUG=heartbeat npm run daemon. You should see [heartbeat] tick every interval.
  • Templates show blank values. Remember Mustache hides undefined fields. Log the object you pass to render to spot typos.
  • Message posted twice. You probably configured both interval and system cron. Pick one or add a lock file.

Security and privacy footnotes

The heartbeat system accesses the same tools the agent already has. Still, proactive mode magnifies surprise. A few best practices:

  • Store sensitive templates outside the repo (templates/ in a private S3 bucket) if you are committing to GitHub.
  • Set heartbeat.quietHours aggressively to avoid midnight leaks of confidential summaries.
  • Use Slack app scopes narrowly. The agent only needs chat:write for proactive DMs.

Next steps

Heartbeat is just a timed event dispatcher. The magic comes from the small scripts you hang off it. My suggestion: pick one friction you hit every day—maybe inbox zero, maybe hydration—and make the agent nudge you before you forget. Once that works, layer another. In a month you will have an invisible PA that never sleeps and costs $5 of AWS credits a month.

If you build something clever, open a discussion on GitHub Discussions. I stole half my tricks from there and I am happy to steal yours too.