Skip to main content

Events API

The OpenClaw Gateway broadcasts real-time events to connected WebSocket clients for agent responses, system health, device pairing, and more.

Event Format

All events follow this structure:
{
  "event": "event.name",
  "payload": {
    "key": "value"
  }
}
event
string
Event type identifier
payload
object
Event-specific data

Agent Events

Event: agent

Agent response chunks during execution. Assistant text stream:
{
  "event": "agent",
  "payload": {
    "runId": "run-123",
    "stream": "assistant",
    "data": {
      "type": "text",
      "text": "Hello! "
    }
  }
}
Tool invocation:
{
  "event": "agent",
  "payload": {
    "runId": "run-123",
    "stream": "tool",
    "data": {
      "phase": "start",
      "name": "web_search",
      "toolCallId": "call-456",
      "input": { "query": "OpenClaw" }
    }
  }
}
Lifecycle:
{
  "event": "agent",
  "payload": {
    "runId": "run-123",
    "stream": "lifecycle",
    "data": {
      "phase": "end"
    }
  }
}
See Agent Protocol for details.

Chat Events

Event: chat

WebChat session updates.
{
  "event": "chat",
  "payload": {
    "sessionId": "session-123",
    "type": "message",
    "message": {
      "role": "assistant",
      "content": "Hello!"
    }
  }
}

System Events

Event: presence

System presence changes (nodes connecting/disconnecting).
{
  "event": "presence",
  "payload": {
    "nodes": [
      {
        "id": "node-123",
        "name": "iPhone",
        "online": true,
        "lastSeen": 1234567890000
      }
    ],
    "version": 42
  }
}
nodes
array
Array of node presence objects
version
number
Presence version for change tracking

Event: health

Gateway health status updates.
{
  "event": "health",
  "payload": {
    "status": "healthy",
    "channels": [
      {
        "id": "signal",
        "status": "connected",
        "account": "+1234567890"
      },
      {
        "id": "telegram",
        "status": "disconnected",
        "error": "Not logged in"
      }
    ],
    "version": 10
  }
}
status
string
Overall health: "healthy", "degraded", "unhealthy"
channels
array
Channel health status
version
number
Health version for change tracking

Event: tick

Periodic heartbeat to keep connections alive.
{
  "event": "tick",
  "payload": {
    "timestamp": 1234567890000
  }
}

Event: heartbeat

Client heartbeat acknowledgment.
{
  "event": "heartbeat",
  "payload": {
    "timestamp": 1234567890000
  }
}

Event: shutdown

Gateway is shutting down.
{
  "event": "shutdown",
  "payload": {
    "reason": "Gateway restart requested"
  }
}
Clients should close connections and attempt reconnection.

Device Pairing Events

Event: node.pair.requested

New device pairing request.
{
  "event": "node.pair.requested",
  "payload": {
    "nodeId": "node-123",
    "name": "Alice's iPhone",
    "platform": "ios",
    "requestedAt": 1234567890000
  }
}
nodeId
string
Unique node identifier
name
string
Device name
platform
string
Platform: "ios", "android", "macos", "linux", "windows"

Event: node.pair.resolved

Device pairing resolved (approved/rejected).
{
  "event": "node.pair.resolved",
  "payload": {
    "nodeId": "node-123",
    "approved": true,
    "token": "device-token-abc123"
  }
}
approved
boolean
Whether pairing was approved
token
string
Device token (when approved)

Event: device.pair.requested

Device pairing request (alternative format).
{
  "event": "device.pair.requested",
  "payload": {
    "deviceId": "device-123",
    "name": "Desktop Client",
    "challenge": "123456"
  }
}

Event: device.pair.resolved

Device pairing resolved.
{
  "event": "device.pair.resolved",
  "payload": {
    "deviceId": "device-123",
    "approved": true
  }
}

Voice Events

Event: talk.mode

Voice mode changed.
{
  "event": "talk.mode",
  "payload": {
    "mode": "push-to-talk",
    "enabled": true
  }
}
mode
string
Voice mode: "push-to-talk", "voice-wake", "always-on"
enabled
boolean
Whether voice is enabled

Event: voicewake.changed

Voice wake triggers updated.
{
  "event": "voicewake.changed",
  "payload": {
    "triggers": ["hey openclaw", "hello openclaw"]
  }
}

Execution Approval Events

Event: exec.approval.requested

Execution approval requested for sensitive commands.
{
  "event": "exec.approval.requested",
  "payload": {
    "requestId": "req-123",
    "command": "rm -rf /dangerous/path",
    "requestedAt": 1234567890000,
    "requestedBy": "agent-run-456"
  }
}
requestId
string
Unique approval request ID
command
string
Command requiring approval

Event: exec.approval.resolved

Execution approval resolved.
{
  "event": "exec.approval.resolved",
  "payload": {
    "requestId": "req-123",
    "approved": false,
    "reason": "User rejected"
  }
}

Cron Events

Event: cron

Cron job status update.
{
  "event": "cron",
  "payload": {
    "id": "daily-report",
    "status": "running",
    "startedAt": 1234567890000
  }
}
id
string
Cron job identifier
status
string
Status: "running", "completed", "failed"

Update Events

Event: update.available

Software update available.
{
  "event": "update.available",
  "payload": {
    "version": "2026.3.1",
    "releaseNotes": "Bug fixes and improvements",
    "downloadUrl": "https://..."
  }
}

Connection Challenge

Event: connect.challenge

Sent during device pairing to display challenge code.
{
  "event": "connect.challenge",
  "payload": {
    "challenge": "123456",
    "expiresAt": 1234567890000
  }
}
challenge
string
6-digit challenge code to display to user
expiresAt
number
Expiration timestamp (milliseconds)

Event Subscriptions

Most events are automatically broadcast to all connected clients. For session-specific events, use subscriptions:

Subscribe to Session

{
  "jsonrpc": "2.0",
  "id": 40,
  "method": "node.subscribe",
  "params": {
    "sessionKey": "user:alice"
  }
}

Unsubscribe from Session

{
  "jsonrpc": "2.0",
  "id": 41,
  "method": "node.unsubscribe",
  "params": {
    "sessionKey": "user:alice"
  }
}

Example: Event Listener

const ws = new WebSocket('ws://localhost:18789');

const eventHandlers = {
  agent: (payload) => {
    if (payload.stream === 'assistant') {
      process.stdout.write(payload.data.text);
    } else if (payload.stream === 'tool') {
      console.log('Tool:', payload.data.name, payload.data.phase);
    }
  },
  
  health: (payload) => {
    console.log('Health:', payload.status);
    console.log('Channels:', payload.channels);
  },
  
  presence: (payload) => {
    console.log('Nodes:', payload.nodes.map(n => `${n.name} (${n.online ? 'online' : 'offline'})`));
  },
  
  'node.pair.requested': (payload) => {
    console.log(`Pairing request from ${payload.name} (${payload.nodeId})`);
    // Approve/reject via node.pair.approve / node.pair.reject
  },
  
  shutdown: (payload) => {
    console.log('Gateway shutting down:', payload.reason);
    ws.close();
  },
  
  tick: (payload) => {
    // Heartbeat - keep connection alive
  }
};

ws.on('message', (data) => {
  const frame = JSON.parse(data);
  
  if (frame.event) {
    const handler = eventHandlers[frame.event];
    if (handler) {
      handler(frame.payload);
    } else {
      console.log('Unhandled event:', frame.event);
    }
  } else {
    // RPC response
    console.log('Response:', frame);
  }
});

Event Filtering

Clients receive all events by default. To filter events client-side:
const interestedEvents = new Set(['agent', 'health', 'presence']);

ws.on('message', (data) => {
  const frame = JSON.parse(data);
  
  if (frame.event && !interestedEvents.has(frame.event)) {
    return; // Ignore
  }
  
  // Handle event
});

Event Ordering

Events are delivered in the order they occur, but:
  • Agent events for the same runId are strictly ordered
  • System events may be interleaved with agent events
  • Network delays may affect perceived ordering
Use event timestamps and version numbers for ordering guarantees.

Best Practices

Always handle start, end, and error phases for agent runs:
if (payload.stream === 'lifecycle') {
  if (payload.data.phase === 'start') {
    // Initialize
  } else if (payload.data.phase === 'end') {
    // Finalize
  } else if (payload.data.phase === 'error') {
    // Handle error
  }
}
Buffer assistant text chunks to display complete responses:
const responses = new Map();

if (payload.stream === 'assistant') {
  const current = responses.get(runId) || '';
  responses.set(runId, current + payload.data.text);
}
Gracefully close connections and attempt reconnection:
if (frame.event === 'shutdown') {
  ws.close();
  setTimeout(() => reconnect(), 5000);
}
Use version numbers to detect missed updates:
let lastPresenceVersion = 0;

if (frame.event === 'presence') {
  if (frame.payload.version > lastPresenceVersion + 1) {
    // Missed updates - fetch full state
  }
  lastPresenceVersion = frame.payload.version;
}

Next Steps

WebSocket Protocol

Learn WebSocket connection details

Agent Protocol

Invoke agents and handle responses

Device Pairing

Pair mobile and desktop devices