CodeDocs Vault

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)

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:

HEARTBEAT_OK Token

When the agent has nothing to report, it responds with HEARTBEAT_OK. This special token:

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

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:

Gateway Maintenance Timers (src/gateway/server-maintenance.ts)

Three timers run continuously in the gateway:

Tick (every ~5s)

Health Refresh (every ~30s)

Dedupe Cleanup (every 60s)

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:

  1. Checks if there are pending system events
  2. Builds the appropriate prompt (heartbeat, exec, cron, or hook context)
  3. Runs the agent
  4. 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.