Building Proactive AI Assistants with OpenClaw's Heartbeat System
Most AI assistants are reactive—they wait for you to ask before doing anything. But what if your assistant could check your email for urgent messages, remind you of upcoming meetings, or alert you to system issues before you notice? That's proactive AI, and OpenClaw's heartbeat system makes it possible.
In this guide, you'll learn how to configure heartbeat monitoring, design smart check routines, and build an AI assistant that anticipates your needs.
What is Heartbeat Monitoring?
A heartbeat is a periodic poll sent to your AI assistant. Instead of waiting for user input, OpenClaw can ping your agent at regular intervals (every 30 minutes, hourly, etc.) to ask: "Anything that needs attention?"
The agent can then:
- Check email for urgent messages
- Look at calendar for upcoming events
- Monitor system status
- Review notifications
- Run background maintenance tasks
If something needs attention, the agent reaches out proactively. If not, it replies HEARTBEAT_OK and stays quiet.
Setting Up Heartbeat
1. Enable Heartbeat in Config
Edit ~/.openclaw/config/gateway.json:
{
"heartbeat": {
"enabled": true,
"intervalMinutes": 30,
"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.",
"channels": ["discord"]
}
}
Parameters:
intervalMinutes: How often to poll (15-60 recommended)prompt: Instructions sent to agentchannels: Where to send heartbeat (e.g., Discord channel ID or "main" for main session)
2. Create HEARTBEAT.md
In your workspace (~/.openclaw/workspace/HEARTBEAT.md):
# HEARTBEAT.md - Proactive Monitoring Rules
## What to Check (Rotate Through These)
### Email (Check every 4 hours)
- Look for unread emails with "urgent", "important", or "ASAP"
- Check for calendar invites requiring response
- Flag messages from VIPs
### Calendar (Check twice daily: 8am, 6pm)
- Upcoming events in next 4 hours
- Events requiring preparation
- Conflicts or overlapping meetings
### System Status (Every 2 hours)
- Check disk usage: `df -h /`
- Check memory: `free -h`
- Review recent error logs: `journalctl -p err --since "30 minutes ago"`
### GitHub (Every 3 hours)
- Check for PR reviews requested
- Look for critical issues assigned to me
### Weather (Once daily: 7am)
- Check forecast for the day
- Alert if rain/snow/extreme temps expected
## State Tracking
Keep track of last checks in `memory/heartbeat-state.json`:
```json
{
"lastChecks": {
"email": 1708975200,
"calendar": 1708970000,
"system": 1708978800,
"github": 1708972400,
"weather": 1708945200
}
}
When to Reach Out
Do notify:
- Urgent email arrived
- Meeting in < 2 hours
- Disk usage > 85%
- Critical error in logs
- PR review requested
Don't notify:
- Late night (11pm-7am) unless truly urgent
- Same notification already sent today
- Low-priority routine stuff
Response Format
If something needs attention:
📧 Urgent email from John: "Need quarterly report by EOD"
📅 Meeting in 90 minutes: "Team standup at 2pm"
If nothing needs attention:
HEARTBEAT_OK
### 3. Restart OpenClaw
```bash
openclaw restart
Your agent will now receive heartbeat prompts every 30 minutes.
Designing Smart Check Routines
Principle 1: Rotate Checks to Reduce Costs
Don't check everything every time. Rotate through different checks:
const checks = ['email', 'calendar', 'system', 'github'];
let currentCheckIndex = 0;
function getNextCheck() {
const check = checks[currentCheckIndex];
currentCheckIndex = (currentCheckIndex + 1) % checks.length;
return check;
}
Heartbeat 1: Check email
Heartbeat 2: Check calendar
Heartbeat 3: Check system
Heartbeat 4: Check GitHub
Heartbeat 5: Back to email
Principle 2: Time-Based Checks
Some checks are only relevant at certain times:
function shouldCheck(checkType) {
const hour = new Date().getHours();
if (checkType === 'weather' && hour !== 7) {
return false; // Only check weather at 7am
}
if (checkType === 'calendar' && hour < 8) {
return false; // Don't check calendar before 8am
}
return true;
}
Principle 3: Frequency Limits
Don't check the same thing too often:
function needsCheck(checkType) {
const state = loadState();
const lastCheck = state.lastChecks[checkType] || 0;
const now = Date.now();
const intervals = {
email: 4 * 60 * 60 * 1000, // 4 hours
calendar: 12 * 60 * 60 * 1000, // 12 hours
system: 2 * 60 * 60 * 1000, // 2 hours
github: 3 * 60 * 60 * 1000 // 3 hours
};
return (now - lastCheck) > intervals[checkType];
}
Email Monitoring Example
Gmail via CLI
# Install gmail-cli or use API
npm install -g gmail-cli
# Check unread emails
gmail unread --label INBOX --max 10
Email Filtering Logic
async function checkEmail() {
const unread = await exec("gmail unread --label INBOX --max 20");
const emails = parseEmails(unread);
const urgent = emails.filter(email => {
const subject = email.subject.toLowerCase();
const sender = email.from.toLowerCase();
// Flag urgent keywords
if (subject.includes('urgent') || subject.includes('asap')) {
return true;
}
// Flag VIP senders
const vips = ['boss@company.com', 'client@important.com'];
if (vips.some(vip => sender.includes(vip))) {
return true;
}
return false;
});
if (urgent.length > 0) {
return formatEmailAlert(urgent);
}
return null; // No urgent emails
}
function formatEmailAlert(emails) {
return emails.map(email =>
`📧 ${email.from}: "${email.subject}"`
).join('\n');
}
Calendar Integration
Google Calendar via CLI
# Install gcalcli
pip install gcalcli
# Check upcoming events
gcalcli agenda --calendar "Personal" --tsv
Calendar Check Logic
async function checkCalendar() {
const now = new Date();
const fourHoursFromNow = new Date(now.getTime() + 4 * 60 * 60 * 1000);
const events = await exec(`gcalcli agenda --tsv --start "${now.toISOString()}" --end "${fourHoursFromNow.toISOString()}"`);
const parsed = parseCalendarEvents(events);
const upcoming = parsed.filter(event => {
const timeUntil = event.start - now;
const hoursUntil = timeUntil / (1000 * 60 * 60);
// Alert for events in next 2 hours
return hoursUntil <= 2 && hoursUntil > 0;
});
if (upcoming.length > 0) {
return formatCalendarAlert(upcoming);
}
return null;
}
function formatCalendarAlert(events) {
return events.map(event => {
const timeUntil = Math.round((event.start - Date.now()) / (1000 * 60));
return `📅 In ${timeUntil} minutes: "${event.title}"`;
}).join('\n');
}
System Monitoring
Disk Usage
df -h / | awk 'NR==2 {print $5}' | sed 's/%//'
Memory Usage
free | awk 'NR==2 {printf "%.0f", $3/$2 * 100}'
Error Logs
journalctl -p err --since "1 hour ago" --no-pager | tail -20
System Check Logic
async function checkSystem() {
const alerts = [];
// Check disk usage
const diskUsage = await exec("df -h / | awk 'NR==2 {print $5}' | sed 's/%//'");
const diskPercent = parseInt(diskUsage.trim());
if (diskPercent > 85) {
alerts.push(`💾 Disk usage at ${diskPercent}% - consider cleanup`);
}
// Check memory
const memUsage = await exec("free | awk 'NR==2 {printf \"%.0f\", $3/$2 * 100}'");
const memPercent = parseInt(memUsage.trim());
if (memPercent > 90) {
alerts.push(`🧠 Memory usage at ${memPercent}% - may need restart`);
}
// Check error logs
const errors = await exec("journalctl -p err --since '1 hour ago' --no-pager | tail -20");
const errorCount = errors.trim().split('\n').length;
if (errorCount > 10) {
alerts.push(`⚠️ ${errorCount} errors in last hour - check logs`);
}
return alerts.length > 0 ? alerts.join('\n') : null;
}
GitHub Integration
Using GitHub CLI
# Install gh CLI
gh pr list --assignee @me
gh issue list --assignee @me
GitHub Check Logic
async function checkGitHub() {
const alerts = [];
// Check for PR reviews requested
const prs = await exec("gh pr list --search 'review-requested:@me' --json number,title");
const prList = JSON.parse(prs);
if (prList.length > 0) {
alerts.push(`🔍 ${prList.length} PR(s) awaiting your review`);
}
// Check for assigned issues
const issues = await exec("gh issue list --assignee @me --state open --json number,title");
const issueList = JSON.parse(issues);
const criticalIssues = issueList.filter(issue =>
issue.title.toLowerCase().includes('critical') ||
issue.title.toLowerCase().includes('urgent')
);
if (criticalIssues.length > 0) {
alerts.push(`🚨 ${criticalIssues.length} critical issue(s) assigned`);
}
return alerts.length > 0 ? alerts.join('\n') : null;
}
Advanced Patterns
Conditional Notifications
Only notify during waking hours:
function shouldNotify() {
const hour = new Date().getHours();
// Quiet hours: 11pm - 7am
if (hour >= 23 || hour < 7) {
return false;
}
return true;
}
Notification Deduplication
Don't send the same alert multiple times:
const sentAlerts = new Set();
function sendAlert(message) {
const hash = hashString(message);
if (sentAlerts.has(hash)) {
console.log("Duplicate alert, skipping");
return;
}
sentAlerts.add(hash);
// Clear hash after 24 hours
setTimeout(() => sentAlerts.delete(hash), 24 * 60 * 60 * 1000);
// Send notification
notifyUser(message);
}
Priority Levels
Classify alerts by urgency:
const PRIORITY = {
LOW: 1,
MEDIUM: 2,
HIGH: 3,
CRITICAL: 4
};
function classifyAlert(alert) {
if (alert.includes('critical') || alert.includes('urgent')) {
return PRIORITY.CRITICAL;
}
if (alert.includes('important') || alert.includes('soon')) {
return PRIORITY.HIGH;
}
// ... etc
}
function shouldNotifyImmediately(priority) {
// During quiet hours, only notify for critical
if (!isWakingHours() && priority < PRIORITY.CRITICAL) {
return false;
}
return true;
}
Proactive Maintenance Tasks
Heartbeat isn't just for notifications—use it for background tasks:
Auto-Cleanup
async function performMaintenance() {
// Clean old log files
await exec("find ~/.openclaw/logs -type f -mtime +7 -delete");
// Archive old memory files
await exec("tar -czf memory-archive-$(date +%Y%m).tar.gz memory/*.md");
// Update dependencies
if (Math.random() < 0.1) { // 10% of heartbeats
await exec("npm update -g openclaw");
}
}
Git Auto-Commit
async function autoCommit() {
const status = await exec("git status --porcelain");
if (status.trim().length > 0) {
await exec("git add memory/");
await exec("git commit -m 'Auto-commit: memory update'");
console.log("Auto-committed memory changes");
}
}
Cost Optimization for Heartbeat
Heartbeat can get expensive if not optimized:
Use Cheap Model
{
"heartbeat": {
"model": "claude-haiku-3.5" // Cheapest model
}
}
Keep Context Minimal
In HEARTBEAT.md, explicitly minimize context:
## Context Rules for Heartbeat
- Load SOUL.md only (identity)
- DO NOT load MEMORY.md (too large)
- DO NOT load conversation history
- Keep heartbeat logic under 1000 tokens
Batch Checks
Instead of checking one thing per heartbeat:
## Batch Check Schedule
Every 2 hours:
- Email + Calendar + System (all in one heartbeat)
Every 6 hours:
- GitHub + Weather
Monitoring Heartbeat Health
Log Heartbeat Activity
# Check heartbeat logs
grep "HEARTBEAT" ~/.openclaw/logs/gateway.log
Track Response Times
function logHeartbeat(durationMs, result) {
const entry = {
timestamp: new Date().toISOString(),
durationMs,
result: result === "HEARTBEAT_OK" ? "ok" : "alert",
cost: estimateCost(result)
};
appendToFile("heartbeat-stats.jsonl", JSON.stringify(entry) + "\n");
}
Real-World Example
Here's a complete heartbeat workflow from a production OpenClaw setup:
# HEARTBEAT.md
## Schedule
- Email: Every 4 hours (9am, 1pm, 5pm)
- Calendar: Twice daily (8am, 6pm)
- System: Every 2 hours
- GitHub: Every 3 hours during work hours
## Logic
1. Load state from memory/heartbeat-state.json
2. Determine which check is due
3. Run check
4. If alert found:
- Format message
- Check if already sent today
- Send if new
5. Update state with lastCheck timestamp
6. Return HEARTBEAT_OK if nothing to report
## Example Output
Typical heartbeat cycle:
- 90% of time: HEARTBEAT_OK
- 8% of time: Single alert
- 2% of time: Multiple alerts
Cost: ~$2/month (using Haiku for heartbeat)
Conclusion
Heartbeat monitoring transforms your AI assistant from reactive to proactive. By checking email, calendar, system status, and other sources at regular intervals, your agent can alert you to issues before you discover them yourself.
Key principles:
- Rotate checks to reduce costs
- Use time-based and frequency-based logic
- Keep context minimal (use Haiku model)
- Implement notification deduplication
- Respect quiet hours
- Track state to avoid redundant checks
With thoughtful configuration, heartbeat adds tremendous value without significant cost.
Next steps: Explore Cost Optimization for Self-Hosted AI Assistants and OpenClaw Automation Cron Jobs Guide to build efficient automated systems.
More Articles
The Ultimate OpenClaw AWS Setup Guide

The definitive guide to setting up OpenClaw on AWS. Includes spot instance configuration, cost optimization, and step-by-step instructions.
Building AI Workflows with Tool Chaining in OpenClaw
Master the art of chaining tools and function calls to build powerful multi-step AI automation workflows—from data extraction to content generation and deployment.
Cost Optimization Guide for Self-Hosted AI Assistants: Run Claude on a Budget
Practical strategies to reduce API costs for self-hosted AI assistants—smart model routing, caching, batching, and OpenClaw-specific optimizations to run Claude affordably.