Heartbeats & Hooks

Ambient awareness vs precise scheduling. Event-driven automation. From basic to advanced.

TL;DR: A heartbeat is your AI checking in every 30 minutes — quietly monitoring things and only speaking up when something needs attention. A hook is a script that fires automatically when something happens inside OpenClaw (a session starts, a command runs, a message arrives). Together they make your AI proactive without being noisy.

Part 1: Heartbeats

What is a heartbeat?

Every 30 minutes (by default), OpenClaw sends your AI a pulse — a periodic check-in prompt. Your AI reads a file called HEARTBEAT.md and acts on whatever's in it. If nothing needs attention, it stays silent. If something does, it messages you.

Think of it like a doctor checking your pulse: most of the time nothing's wrong, they say nothing. Occasionally they say "actually, something's up."

Heartbeat vs Cron — the real difference

Heartbeat Cron Job
Runs in your main session Runs in an isolated session
Approximate timing (~30 min) Exact timing ("9:00 AM sharp")
Has full conversation context Starts fresh each time
Batches multiple checks together One task per job
Best for: ambient monitoring Best for: precise scheduled tasks
Silent unless something to report Always delivers output to channel

The decision flowchart

Setting up HEARTBEAT.md

Create this file in your workspace. Keep it short — it's loaded every poll cycle.

cat > ~/.openclaw/workspace/HEARTBEAT.md << 'EOF' # HEARTBEAT.md ## Active Checks - Check APPROVAL_QUEUE.md — flag anything pending for more than 2 hours - Check memory/$(date +%Y-%m-%d).md — any open items from today? - If an urgent email has arrived since last check, flag it ## Quiet Hours - No proactive messages 23:00–08:00 unless genuinely urgent - Do not message about routine items during quiet hours ## Cooldown - Don't repeat the same alert within 2 hours EOF

Configuring heartbeat timing

The heartbeat interval is set in your openclaw.json:

"heartbeat": { "enabled": true, "interval": "30m", "channel": "telegram" }
Best practice: Use heartbeat for 2–4 combined checks per cycle. Create separate cron jobs only when exact timing matters.

Tracking heartbeat state

To prevent repeat alerts, your AI can track what it last checked:

# ~/.openclaw/workspace/memory/heartbeat-state.json { "lastChecks": { "email": 1773785000, "calendar": 1773785000, "approvals": 1773785000 }, "lastAlerts": { "pending-approval-x": 1773785000 } }

Your AI reads this file, compares timestamps, and only checks things that are due. It writes back after each check. No repeat alerts, no noise.

---

Part 2: Hooks

What is a hook?

A hook is a small script that fires automatically when something happens inside OpenClaw. Not on a schedule — on an event. Session starts: hook fires. Command runs: hook fires. Message arrives: hook fires.

Hooks run inside the Gateway, not in the agent session. They're fast, lightweight, and run in the background.

The two types

Internal Hooks External Webhooks
Run inside OpenClaw on agent events External HTTP endpoints that trigger OpenClaw
Examples: boot-md, session-memory, command-logger Examples: GitHub webhook → Jinn reviews PR
Config: hooks.internal Config: hooks.enabled + token
Bundled with OpenClaw You set them up in external services

The bundled hooks you should enable

"hooks": { "internal": { "enabled": true, "entries": { "boot-md": { "enabled": true }, "bootstrap-extra-files": { "enabled": true }, "session-memory": { "enabled": true }, "command-logger": { "enabled": true } } } }

What each one does:

boot-md is the most important hook. Without it, your AI loads without SOUL.md, MEMORY.md, or AGENTS.md — it has no identity, no context, and no rules. Enable it on day one.

External webhooks

You can expose a webhook endpoint from your Gateway so external services can trigger your AI:

"hooks": { "enabled": true, "token": "your-secret-token", "path": "/hooks", "allowedAgentIds": ["main"] }

This exposes three endpoints on your Gateway:

Endpoint What it does
POST /hooks/wake Wakes the main agent with a message
POST /hooks/agent Routes a message to a specific agent
POST /hooks/<name> Fires a named mapped hook

Example: GitHub PR triggers Jinn to review it

# 1. Get your Gateway's public URL (via Cloudflare tunnel or similar) openclaw gateway tunnel # 2. In GitHub: Settings → Webhooks → Add webhook # URL: https://your-tunnel.trycloudflare.com/hooks/wake # Secret: your-secret-token # Events: Pull requests # 3. The webhook body tells Jinn what to do: # POST /hooks/wake # { "message": "New PR opened: {{PR title}}. Please review it." }

Creating custom hooks (advanced)

You can write your own hooks in TypeScript. Each hook lives in a directory with two files:

~/.openclaw/hooks/ └── my-custom-hook/ ├── HOOK.md # Metadata: name, events, description └── handler.ts # TypeScript: what to do when event fires

The HOOK.md format:

# my-custom-hook ## Events - session:start - command:after ## Description Logs session starts and post-command states to a local file. ## Enabled true

The handler receives an event context object with:

Hook event types

Category Events
Session session:start, session:end, session:reset
Command command:before, command:after
Agent agent:turn:start, agent:turn:end
Message message:inbound, message:outbound
Gateway gateway:start, gateway:stop

Managing hooks via CLI

# List all discovered hooks openclaw hooks list # Check hook details openclaw hooks info --name boot-md # Enable/disable a hook openclaw hooks enable --name my-custom-hook openclaw hooks disable --name my-custom-hook # Check why a hook isn't running openclaw hooks check --name my-custom-hook

Best Practices

Keep hook handlers fast. Hooks run synchronously in the Gateway. A slow hook delays everything else. If you need to do heavy work, spawn it async or use a cron job instead.
Filter events early. Check event.type immediately in your handler and return early if it's not the event you care about. Don't do expensive operations then discard.
Never put secrets in HOOK.md. HOOK.md is discoverable by anyone with filesystem access. Put secrets in ~/.openclaw/.env and read them via process.env.
Webhooks require a public URL. Your Gateway runs on localhost. To receive external webhooks (from GitHub, Stripe, etc.), you need a tunnel. Use the Cloudflare quick tunnel: cloudflared tunnel --url http://localhost:18789