Autonomous Behavior
How the agent acts without being explicitly called — heartbeats, cron jobs, webhooks, and proactive triggers.
Overview
OpenClaw is not just a request-response chatbot. In daemon/gateway mode, the agent operates continuously and can act autonomously through several coordinated mechanisms:
┌──────────────────────────────────────────────────────────────┐
│ Autonomous Triggers │
│ │
│ Timer-Based Event-Based System │
│ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Cron │ │ Inbound │ │ Health │ │
│ │ Jobs │ │ Messages │ │ Monitor │ │
│ ├──────────┤ ├──────────┤ ├─────────┤ │
│ │Heartbeat │ │ Webhooks │ │ Config │ │
│ │ Interval │ │ /Hooks │ │ Reload │ │
│ ├──────────┤ ├──────────┤ ├─────────┤ │
│ │Maintenance│ │ Exec │ │ Session │ │
│ │ Timers │ │ Events │ │ Reaper │ │
│ └──────────┘ └──────────┘ └─────────┘ │
│ │ │ │ │
│ └───────────┬───────────┘ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ Agent Runner │ │ │
│ │ (isolated or │ │ │
│ │ main session)│ │ │
│ └──────┬───────┘ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ Delivery │ │ │
│ │ (announce, │ │ │
│ │ webhook, │ │ │
│ │ direct msg) │ │ │
│ └──────────────┘ │ │
└──────────────────────────────────────────────────────────────┘
Cron System
The cron system is the primary mechanism for scheduled autonomous execution.
Architecture (src/cron/)
Gateway startup
→ cron.start() arms timer
→ Timer fires when nextRunAtMs reached
→ onTimer() finds due jobs
→ executeJobCore() runs isolated agent
→ applyJobResult() updates state + schedules next
→ armTimer() reschedules
Schedule Types
| Kind | Description | Example |
|---|---|---|
"at" |
One-shot execution at absolute timestamp | { kind: "at", at: 1708000000000 } |
"every" |
Recurring interval (milliseconds) | { kind: "every", everyMs: 3600000 } |
"cron" |
Cron expression with timezone | { kind: "cron", expr: "0 9 * * MON", tz: "America/New_York" } |
Job Structure (src/cron/types.ts)
type CronJob = {
id: string;
agentId?: string; // Override agent (default: main)
sessionKey?: string; // Origin session for delivery
name: string;
enabled: boolean;
schedule: CronSchedule;
sessionTarget: "main" | "isolated";
wakeMode: "now" | "next-heartbeat";
payload: CronPayload; // "systemEvent" or "agentTurn"
delivery?: CronDelivery; // none | announce | webhook
state: {
nextRunAtMs?: number;
runningAtMs?: number;
lastRunAtMs?: number;
lastStatus?: "ok" | "error" | "skipped";
consecutiveErrors?: number;
};
};Execution Modes
sessionTarget |
Behavior |
|---|---|
"isolated" |
Creates ephemeral session (cron:<jobId>:run:<runId>). Agent runs with its own context, no cross-contamination with main session. |
"main" |
Enqueues a system event into the main session. The next heartbeat or user message picks it up. |
wakeMode |
Behavior |
|---|---|
"now" |
Immediately triggers requestHeartbeatNow() to process the event |
"next-heartbeat" |
Waits for the next scheduled heartbeat interval |
Error Handling & Backoff
When jobs fail, exponential backoff kicks in:
| Consecutive Errors | Backoff |
|---|---|
| 1 | 30 seconds |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5+ | 60 minutes |
One-shot jobs ("at" schedule) are disabled after any terminal status.
Timer Mechanics (src/cron/service/timer.ts)
- Max timer delay: 60 seconds (prevents Node.js timer overflow)
- Minimum refire gap: 2 seconds (prevents spin-loops)
- Session reaper sweep: Piggybacks on cron timer every 5 minutes to clean orphaned cron sessions
- Job execution timeout: 10 minutes (default)
Isolated Agent Execution (src/cron/isolated-agent/run.ts)
runCronIsolatedAgentTurn()
1. Resolve agent config (may use per-job agent override)
2. Create ephemeral session key: cron:<jobId>
3. Ensure agent workspace exists
4. Select model (with fallback chain)
5. Run agent:
- runEmbeddedPiAgent() for Claude models
- runCliAgent() for CLI providers
6. Handle messaging tool auto-delivery
7. Return: { outputText, delivered, model, provider, usage }Heartbeat System
The heartbeat is an interval-based wake mechanism that allows the agent to perform periodic tasks.
How It Works
startHeartbeatRunner() creates scheduler
→ Scans enabled agents from config
→ Computes nextDueMs for each agent
→ setInterval checks schedule
→ When due: requestHeartbeatNow() queues wake
→ Coalesce timer batches requests (250ms window)
→ heartbeatWakeHandler() fires
→ runHeartbeatOnce() executes:
1. Check quiet hours (isWithinActiveHours())
2. Skip if main queue has pending user requests
3. Read HEARTBEAT.md from workspace
4. Build prompt with task context
5. Execute LLM
6. Process response
Configuration
| Config Key | Default | Purpose |
|---|---|---|
agents.defaults.heartbeat |
"30m" |
Default interval for all agents |
agents.list[*].heartbeat |
— | Per-agent override |
Source: src/infra/heartbeat-runner.ts
HEARTBEAT.md Convention
The agent reads HEARTBEAT.md from its workspace root on each heartbeat. This file contains tasks the agent should perform proactively:
- If
HEARTBEAT.mdis empty or missing → heartbeat is skipped (no API call) - If content exists → agent executes with the file content as context
- The default prompt tells the agent to check this file
HEARTBEAT_OK Token
When the agent has nothing to report, it responds with HEARTBEAT_OK. This special token:
- Is stripped from the response (not delivered to any channel)
- Causes the heartbeat turn to be pruned from the transcript (both user and assistant messages)
- Prevents silent heartbeats from accumulating in session history
Wake Priority System (src/infra/heartbeat-wake.ts)
Wake requests have priorities that determine processing order:
| Priority | Reason | Description |
|---|---|---|
| 0 | RETRY |
Failed retries |
| 1 | INTERVAL |
Scheduled interval ticks |
| 2 | DEFAULT |
Manual requests |
| 3 | ACTION |
Hooks, exec events, manual triggers |
Requests are coalesced within a 250ms window — multiple triggers batch into a single handler call.
Special Prompt Overrides
The heartbeat doesn't always use the default prompt:
| Context | Prompt Used |
|---|---|
| Normal heartbeat | Default (reads HEARTBEAT.md) |
| Exec event completed | EXEC_EVENT_PROMPT — relays async command results |
| Cron event | buildCronEventPrompt() — includes cron job context |
Duplicate Suppression
The heartbeat suppresses duplicate messages within a 24-hour window. If the agent produces the same response as a recent heartbeat, it's not re-delivered.
System Events (src/infra/system-events.ts)
System events are an in-memory queue that bridges autonomous triggers to the agent:
enqueueSystemEvent(text, { sessionKey, contextKey })
drainSystemEventEntries() // Pull all + clear
peekSystemEventEntries() // View without consuming
hasSystemEvents() // Check if queue non-empty- Session-scoped (per session key)
- Max 20 events per queue
- Not persisted (in-memory only)
- Context key tracks event type for deduplication
Events are prefixed to the next agent prompt — whether triggered by heartbeat, user message, or cron job.
Channel Health Monitoring (src/gateway/channel-health-monitor.ts)
Channels are monitored continuously and restarted automatically:
| Parameter | Value |
|---|---|
| Check interval | 5 minutes |
| Startup grace period | 60 seconds |
| Cooldown between restarts | 2 check cycles |
| Max restarts per hour | 3 |
Restart triggers:
- Channel not running
- Channel disconnected
- Channel configured and enabled but not connected
Gateway Maintenance Timers (src/gateway/server-maintenance.ts)
Three timers run continuously in the gateway:
Tick (every ~5s)
- Broadcasts keepalive
"tick"event to all WebSocket clients - Clients use this to detect disconnection
Health Refresh (every ~30s)
- Probes channel health status
- Updates cached health snapshot
- Broadcasts health updates to clients
Dedupe Cleanup (every 60s)
- Cleans message deduplication cache
- Prunes expired chat abort controllers
- Clears completed session state
- Maintains agent run sequence size limits
Webhook / Hook Triggers
External events can trigger agent execution via HTTP webhooks.
Hook Endpoint (src/gateway/server/hooks.ts)
POST /hooks?path=<hook-path>
Hook Processing
HTTP POST arrives
→ applyHookMappings() matches path to mapping
→ renderTemplate() substitutes {{payload.*}}, {{headers.*}}, {{query.*}}, {{now}}
→ Optional transform module modifies payload
→ Dispatch:
├─ dispatchWakeHook():
│ → enqueueSystemEvent(text)
│ → requestHeartbeatNow({ reason: "hook:wake" })
│
└─ dispatchAgentHook():
→ Creates ephemeral cron job
→ runCronIsolatedAgentTurn() immediately
→ enqueueSystemEvent(result)
→ requestHeartbeatNow({ reason: "hook:..." })
Preset Mappings
Built-in mappings include Gmail (email → agent), with custom JavaScript transform modules supported for other services.
Node Events (src/gateway/server-node-events.ts)
Peripheral devices (mobile apps, paired devices) can trigger agent behavior:
| Event Type | Description |
|---|---|
voice.transcript |
Voice input transcribed from mobile |
agent.request |
Deep link invocation from device |
exec.started |
Async command began executing |
exec.finished |
Async command completed |
exec.denied |
Command execution denied |
Flow:
Device sends event → handleNodeEvent()
→ enqueueSystemEvent() for context
→ requestHeartbeatNow({ reason: "exec-event" })
→ Heartbeat picks up event with special prompt
→ Result delivered to originating device/channel
Exec event output is compacted to 180 characters for the system event summary.
Inbound Message Handling (src/web/inbound/monitor.ts)
Even "normal" message handling involves autonomous infrastructure:
Debouncing
Rapid consecutive messages are batched:
| Config Key | Default | Purpose |
|---|---|---|
messages.inbound.debounceMs |
0ms | Base delay |
messages.inbound.byChannel[id] |
— | Per-channel override |
Deduplication
isRecentInboundMessage() filters messages that have already been processed within a recent time window.
Callback Chain
User sends message → Channel SDK event
→ extractText/Media/Context
→ Debouncer.enqueue()
→ debounceMs timeout (or manual flush)
→ onMessage callback
→ Route resolution → Agent execution
Daemon Mode (src/daemon/service.ts)
The daemon keeps all autonomous behavior running persistently:
Platform-Specific Installation
| Platform | Service Manager | Location |
|---|---|---|
| macOS | LaunchAgent | ~/Library/LaunchAgents/ |
| Linux | systemd user service | ~/.config/systemd/user/ |
| Windows | Task Scheduler | System task store |
What the Daemon Runs
openclaw daemon start
→ Spawns detached process
→ Starts gateway server (HTTP + WebSocket)
→ Starts channel monitors (WhatsApp, Telegram, etc.)
→ Starts cron timer
→ Starts heartbeat runner
→ Starts health monitor
→ Starts maintenance timers
→ Logs to syslog/journald
→ Auto-restarts on crash
Complete Autonomous Behavior Map
Autonomous Triggers
├── Timer-Based
│ ├── Cron Jobs
│ │ └── onTimer() → findDueJobs() → executeJobCore()
│ ├── Heartbeat Interval
│ │ └── startHeartbeatRunner() → runHeartbeatOnce()
│ ├── Gateway Ticks (5s)
│ │ └── Keepalive broadcasts
│ ├── Health Check (30s)
│ │ └── Channel probes + restart
│ └── Maintenance (60s)
│ └── Cache cleanup + session reaping
│
├── Event-Based Wakeup
│ ├── Inbound Messages
│ │ └── Channel SDK → debounce → route → agent
│ ├── Webhooks / Hooks
│ │ └── HTTP POST → template → dispatch → agent
│ ├── Exec Events
│ │ └── Command completion → system event → heartbeat
│ └── Voice Transcripts
│ └── Mobile audio → system event → agent
│
├── Automatic Delivery
│ ├── Cron announce → Direct channel post
│ ├── Heartbeat deliver → If showAlerts + delivery.to
│ ├── Messaging tool → Matched target auto-delivery
│ └── Webhook callback → HTTP POST to delivery.to
│
└── System-Level
├── Session Reaper → Cleans orphaned cron sessions (5m)
├── Channel Restart → Auto-restart dead channels (5m)
├── Config Reload → Watch file + hot-reload
└── Daemon → Auto-restart on crash
Key Insight: The Heartbeat as a Universal Wake Mechanism
The heartbeat is not just a periodic timer — it's the universal delivery mechanism for all autonomous behavior. Cron jobs, webhooks, exec events, and node events all ultimately funnel through requestHeartbeatNow() to wake the agent. The heartbeat runner:
- Checks if there are pending system events
- Builds the appropriate prompt (heartbeat, exec, cron, or hook context)
- Runs the agent
- Delivers the response
This design means a single code path handles all autonomous agent invocations, regardless of trigger source. The priority system ensures urgent triggers (exec events, hooks) take precedence over routine interval ticks.