packages/docs/websocket-events.mdx
Eliza uses WebSocket connections to deliver real-time updates from the agent runtime to connected clients, including the frontend UI, the Electrobun desktop app, and custom integrations.
ws://localhost:18789/ws
The WebSocket endpoint is available at /ws on the API server. In development, this is port 31337 (ELIZA_API_PORT); in production, the API shares port 2138 (ELIZA_PORT) with the dashboard. The Gateway (port 18789 by default) also proxies WebSocket connections.
When ELIZA_API_TOKEN is configured, the WebSocket connection requires authentication. The client connects to the /ws endpoint without credentials in the URL, then authenticates by sending an auth message immediately after the connection opens:
{ "type": "auth", "token": "YOUR_API_TOKEN" }
The server validates the token and responds with:
{ "type": "auth-ok" }
If the token is valid, the server activates the connection and sends the initial status event and event replay. If the token is missing or invalid, the server closes the connection with status code 1008 (Policy Violation).
You can also authenticate during the HTTP upgrade handshake by including the token in an Authorization: Bearer <token> header. If the handshake includes a valid token, the connection is activated immediately without needing to send an auth message.
When a WebSocket client connects and is authenticated (either via a handshake header or an auth message), the server immediately sends:
status event with the current agent state{
"type": "status",
"state": "running",
"agentName": "Eliza",
"model": "@elizaos/plugin-anthropic",
"startedAt": 1708000000000,
"uptime": 60000
}
The server broadcasts a status event to all connected clients every 5 seconds.
Clients can send a ping message; the server responds with a pong:
All WebSocket event types at a glance:
| Event Type | Direction | Buffered | Description |
|---|---|---|---|
auth | Client → Server | No | Authenticate the connection with an API token |
auth-ok | Server → Client | No | Confirms successful authentication |
ping | Client → Server | No | Keepalive ping |
pong | Server → Client | No | Keepalive response |
active-conversation | Client → Server | No | Set active conversation in the UI |
status | Server → Client | No | Agent state broadcast (every 5s) |
restart-required | Server → Client | No | Config change requires restart |
agent_event | Server → Client | Yes | Agent autonomy loop events |
heartbeat_event | Server → Client | Yes | Agent activity indicators |
training_event | Server → Client | Yes | Fine-tuning/training progress |
proactive-message | Server → Client | No | Agent-initiated conversation message |
conversation-updated | Server → Client | No | Conversation metadata changed |
install-progress | Server → Client | No | Plugin installation progress |
terminal-output | Server → Client | No | Shell command execution output |
emote | Server → Client | No | Avatar emote/animation trigger |
| Message Type | Payload | Description |
|---|---|---|
auth | { token: string } | Authenticate the connection; server responds with auth-ok on success |
ping | {} | Keepalive ping; server responds with pong |
active-conversation | { conversationId: string } | Inform the server which conversation is currently active in the UI |
When ELIZA_API_TOKEN is configured, the first message sent after connecting must be an auth message:
{
"type": "auth",
"token": "your-api-token"
}
On success, the server responds with { "type": "auth-ok" } and activates the connection (sending the initial status event and event replay). On failure, the server closes the connection with code 1008.
{
"type": "active-conversation",
"conversationId": "conv-abc-123"
}
The server stores the conversationId on the connection state. It uses this to decide whether to deliver proactive messages inline (active conversation) or mark them as unread (non-active conversation). If conversationId is not a string, the active conversation is cleared (null).
statusDirection: Server → Client | Buffered: No
Broadcast every 5 seconds and on state changes.
{
"type": "status",
"state": "running",
"agentName": "Eliza",
"model": "@elizaos/plugin-anthropic",
"startedAt": 1708000000000,
"pendingRestart": false,
"pendingRestartReasons": []
}
| Field | Type | Description |
|---|---|---|
state | string | One of: not_started, starting, running, paused, stopped, restarting, error |
agentName | string | Current agent display name |
model | string | undefined | Detected AI model provider plugin name |
startedAt | number | undefined | Epoch timestamp (ms) when the agent was started |
pendingRestart | boolean | Whether a restart is pending (periodic broadcast only) |
pendingRestartReasons | string[] | Human-readable reasons why a restart is needed (periodic broadcast only) |
restart-requiredDirection: Server → Client | Buffered: No
Sent when the server detects that a configuration change requires a restart. Triggered by operations like enabling shell access, changing plugin configuration, or modifying environment variables.
{
"type": "restart-required",
"reasons": ["Shell access enabled", "Plugin configuration changed"]
}
| Field | Type | Description |
|---|---|---|
reasons | string[] | Array of human-readable reasons describing what changed |
agent_eventDirection: Server → Client | Buffered: Yes
Envelope for agent autonomy loop events streamed from the AGENT_EVENT service. The server subscribes to the runtime's agent event service and wraps each event in a StreamEventEnvelope.
{
"type": "agent_event",
"version": 1,
"eventId": "evt-42",
"ts": 1708000000000,
"runId": "run-xyz",
"seq": 42,
"stream": "autonomy",
"sessionKey": "session-key",
"agentId": "agent-uuid",
"roomId": "room-uuid",
"payload": {}
}
| Field | Type | Description |
|---|---|---|
type | "agent_event" | Event type discriminator |
version | 1 | Envelope format version (always 1) |
eventId | string | Unique event identifier (format: evt-{N}, monotonically increasing) |
ts | number | Epoch timestamp (milliseconds) |
runId | string | undefined | Autonomy run identifier |
seq | number | undefined | Sequence number within the run |
stream | string | undefined | Event stream name (e.g., "autonomy", "assistant") |
sessionKey | string | undefined | Session key |
agentId | string | undefined | Agent UUID |
roomId | string | undefined | Room UUID |
payload | object | Inner event data from the agent runtime |
heartbeat_eventDirection: Server → Client | Buffered: Yes
Envelope for agent heartbeat events (activity indicators). The server subscribes to the runtime's heartbeat stream and wraps each event in a StreamEventEnvelope.
{
"type": "heartbeat_event",
"version": 1,
"eventId": "evt-43",
"ts": 1708000000000,
"payload": {
"ts": 1708000000000,
"status": "thinking",
"to": "discord",
"preview": "Composing reply...",
"durationMs": 1500,
"hasMedia": false,
"channel": "discord",
"indicatorType": "typing"
}
}
Envelope fields follow the same schema as agent_event (see above). The payload contains the heartbeat data:
| Payload Field | Type | Description |
|---|---|---|
ts | number | Heartbeat timestamp (milliseconds) |
status | string | Heartbeat status (e.g., "thinking", "replying") |
to | string | undefined | Target channel/destination |
preview | string | undefined | Short preview of what the agent is doing |
durationMs | number | undefined | Duration of the current operation (milliseconds) |
hasMedia | boolean | undefined | Whether the operation involves media |
reason | string | undefined | Reason for the heartbeat |
channel | string | undefined | Channel identifier |
silent | boolean | undefined | Whether this is a silent heartbeat (no UI indicator) |
indicatorType | string | undefined | UI indicator type (e.g., "typing") |
training_eventDirection: Server → Client | Buffered: Yes
Envelope for fine-tuning/training progress events. The server subscribes to the training service's event stream and wraps each event in a StreamEventEnvelope. The payload structure depends on the training service implementation.
{
"type": "training_event",
"version": 1,
"eventId": "evt-44",
"ts": 1708000000000,
"payload": {
"phase": "training",
"progress": 0.45,
"message": "Training epoch 5/10"
}
}
Envelope fields follow the same schema as agent_event (see above). The payload contains training-specific data determined by the active training service. If the training service emits a non-object value, it is wrapped as { value: <emitted_value> }.
proactive-messageDirection: Server → Client | Buffered: No
Sent when the agent autonomously generates a message in a conversation. This happens when the autonomy loop produces text output (via the "assistant" stream) that is routed to the user's active conversation. The message is persisted as a Memory in the conversation room before being broadcast.
{
"type": "proactive-message",
"conversationId": "conv-abc-123",
"message": {
"id": "msg-uuid",
"role": "assistant",
"text": "I noticed something interesting...",
"timestamp": 1708000000000,
"source": "autonomy"
}
}
| Field | Type | Description |
|---|---|---|
conversationId | string | The conversation this message belongs to |
| Message Field | Type | Description |
|---|---|---|
id | string | Message UUID (from the created Memory, or auto-{timestamp} fallback) |
role | "assistant" | Always "assistant" |
text | string | Message content |
timestamp | number | Epoch timestamp (milliseconds) |
source | string | Origin of the message (e.g., "autonomy") |
The frontend handles this by:
conversation-updatedDirection: Server → Client | Buffered: No
Sent when conversation metadata changes. Currently triggered when the server auto-generates a title from the first message in a conversation (replacing the default "New Chat" title).
{
"type": "conversation-updated",
"conversation": {
"id": "conv-abc-123",
"title": "Discussion about AI agents",
"roomId": "room-uuid",
"createdAt": "2024-02-15T10:00:00.000Z",
"updatedAt": "2024-02-15T10:30:00.000Z"
}
}
| Conversation Field | Type | Description |
|---|---|---|
id | string | Conversation identifier |
title | string | Display title of the conversation |
roomId | string | Associated room UUID in the runtime |
createdAt | string | ISO 8601 timestamp of conversation creation |
updatedAt | string | ISO 8601 timestamp of last update |
install-progressDirection: Server → Client | Buffered: No
Broadcast during plugin installation to report real-time progress. Sent from the POST /api/plugins/install handler as the plugin manager progresses through installation phases.
{
"type": "install-progress",
"pluginName": "@elizaos/plugin-discord",
"phase": "downloading",
"message": "Downloading @elizaos/[email protected]..."
}
| Field | Type | Description |
|---|---|---|
pluginName | string | undefined | Name of the plugin being installed |
phase | string | Current installation phase |
message | string | Human-readable progress message |
The phase field is one of the following values:
| Phase | Description |
|---|---|
resolving | Looking up the plugin in the registry |
downloading | Downloading the plugin package |
installing-deps | Installing npm dependencies |
validating | Validating the plugin structure |
configuring | Applying plugin configuration |
restarting | Restarting the runtime to load the plugin |
complete | Installation finished successfully |
error | Installation failed |
terminal-outputDirection: Server → Client | Buffered: No
Streams shell command execution output in real-time. Sent after a POST /api/terminal/run request when shell access is enabled. Each command run is identified by a unique runId. Multiple sub-events are emitted over the lifecycle of a single command.
Start event (emitted immediately when the command spawns):
{
"type": "terminal-output",
"runId": "run-1708000000000-a1b2c3",
"event": "start",
"command": "ls -la",
"maxDurationMs": 30000
}
stdout event (emitted as stdout data arrives):
{
"type": "terminal-output",
"runId": "run-1708000000000-a1b2c3",
"event": "stdout",
"data": "total 42\ndrwxr-xr-x 5 user staff 160 Feb 15 10:00 .\n"
}
stderr event (emitted as stderr data arrives):
{
"type": "terminal-output",
"runId": "run-1708000000000-a1b2c3",
"event": "stderr",
"data": "Warning: something happened\n"
}
exit event (emitted when the process exits normally):
{
"type": "terminal-output",
"runId": "run-1708000000000-a1b2c3",
"event": "exit",
"code": 0
}
timeout event (emitted when the process exceeds maxDurationMs):
{
"type": "terminal-output",
"runId": "run-1708000000000-a1b2c3",
"event": "timeout",
"maxDurationMs": 30000
}
error event (emitted when the process fails to spawn):
{
"type": "terminal-output",
"runId": "run-1708000000000-a1b2c3",
"event": "error",
"data": "spawn ENOENT"
}
| Field | Type | Description |
|---|---|---|
runId | string | Unique run identifier (format: run-{timestamp}-{random}) |
event | string | Sub-event type: start, stdout, stderr, exit, timeout, or error |
command | string | The shell command (only present on start) |
maxDurationMs | number | Maximum allowed execution time in ms (present on start and timeout) |
data | string | Output data (present on stdout, stderr, and error) |
code | number | Process exit code (present on exit; defaults to 1 if null) |
emoteDirection: Server → Client | Buffered: No
Broadcast when the POST /api/emote endpoint is called, triggering an avatar animation in the frontend. The emote must exist in the emote catalog (GET /api/emotes).
{
"type": "emote",
"emoteId": "wave",
"glbPath": "/emotes/wave.glb",
"duration": 2.0,
"loop": false
}
| Field | Type | Description |
|---|---|---|
emoteId | string | Unique emote identifier from the catalog |
glbPath | string | Path to the GLB animation file |
duration | number | Animation duration in seconds |
loop | boolean | Whether the animation should loop |
The /v1/messages endpoint supports streaming via "stream": true in the request body or an Accept: text/event-stream header. When streaming is enabled, the response uses SSE with the following event sequence:
message_start → content_block_start → content_block_delta* → content_block_stop → message_delta → message_stop
message_startSent once at the beginning of the response.
{
"type": "message_start",
"message": {
"id": "msg_a1b2c3d4e5f6",
"type": "message",
"role": "assistant",
"model": "Eliza",
"content": [],
"stop_reason": null,
"stop_sequence": null,
"usage": { "input_tokens": 0, "output_tokens": 0 }
}
}
content_block_startMarks the beginning of a content block.
{
"type": "content_block_start",
"index": 0,
"content_block": { "type": "text", "text": "" }
}
content_block_deltaDelivers incremental text chunks as the agent generates the response.
{
"type": "content_block_delta",
"index": 0,
"delta": { "type": "text_delta", "text": "Hello! " }
}
content_block_stopMarks the end of a content block.
{
"type": "content_block_stop",
"index": 0
}
message_deltaSent after all content blocks are complete, carrying the stop reason.
{
"type": "message_delta",
"delta": { "stop_reason": "end_turn", "stop_sequence": null },
"usage": { "output_tokens": 0 }
}
message_stopFinal event indicating the message is complete.
{
"type": "message_stop"
}
error (SSE)Sent if the runtime is unavailable or an error occurs during generation.
{
"type": "error",
"error": {
"type": "server_error",
"message": "Agent is not running"
}
}
| Error Type | Description |
|---|---|
service_unavailable | Agent runtime is not running |
server_error | Generation failed or an unexpected error occurred |
The frontend API client handles reconnection automatically. The reconnection strategy uses exponential backoff with these parameters:
| Parameter | Value | Description |
|---|---|---|
| Initial delay | 500ms | Delay before first reconnection attempt |
| Backoff factor | 1.5x | Multiplier applied after each failed attempt |
| Max delay | 10,000ms | Maximum delay between reconnection attempts |
| Max attempts | 15 | Fast-backoff reconnection attempts |
| Slow probe interval | 30,000ms | After max attempts are exhausted, the client continues probing every 30 seconds |
On successful reconnect, the backoffMs resets to 500ms. After the initial 15 fast-backoff attempts are exhausted, the client switches to a low-frequency 30-second probe interval so the UI can recover automatically without requiring a full page refresh.
The server maintains an in-memory event buffer for stream events. Key characteristics:
agent_event, heartbeat_event, and training_event types are buffered (via the StreamEventEnvelope wrapper)status, proactive-message, install-progress, terminal-output, emote, conversation-updated, restart-required) are broadcast live and not bufferedeventId (format: evt-{N})All buffered events share this envelope structure:
interface StreamEventEnvelope {
type: "agent_event" | "heartbeat_event" | "training_event";
version: 1;
eventId: string; // "evt-{N}", monotonically increasing
ts: number; // Epoch timestamp (milliseconds)
runId?: string; // Autonomy run identifier
seq?: number; // Sequence number within the run
stream?: string; // Event stream name
sessionKey?: string;
agentId?: string;
roomId?: string; // UUID
payload: object; // Inner event data
}
The frontend API client maintains a send queue for messages that cannot be delivered while the WebSocket is disconnected:
active-conversation messages are deduplicated (only the latest is kept)