Events API
The OpenClaw Gateway broadcasts real-time events to connected WebSocket clients for agent responses, system health, device pairing, and more.
All events follow this structure:
{
"event": "event.name",
"payload": {
"key": "value"
}
}
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
}
}
Array of node presence objects
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
}
}
Overall health: "healthy", "degraded", "unhealthy"
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
}
}
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"
}
}
Whether pairing was approved
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
}
}
Voice mode: "push-to-talk", "voice-wake", "always-on"
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"
}
}
Unique approval request ID
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
}
}
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
}
}
6-digit challenge code to display to user
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
Handle all lifecycle phases
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);
}
Respond to shutdown events
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