Clawist
🟡 Intermediate11 min readBy Lin6

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 agent
  • channels: 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.