WebSockets (2)
Last updated Jun 3, 2026 Agents support WebSocket connections for real-time, bi-directional communication. This page covers server-side WebSocket handling. For client-side connection, refer to the Client SDK.
Lifecycle hooks
Agents have several lifecycle hooks that fire at different points:
| Hook | When called |
|---|---|
onStart(props?) |
Once when the agent first starts (before any connections) |
onRequest(request) |
When an HTTP request is received (non-WebSocket) |
onConnect(connection, ctx) |
When a new WebSocket connection is established |
onMessage(connection, message) |
When a WebSocket message is received |
onClose(connection, code, reason, wasClean) |
When a WebSocket connection closes |
onError(connection, error) |
When a WebSocket error occurs on a connection |
onError(error) |
When a server-level error occurs (not tied to a specific connection) |
shouldSendProtocolMessages(connection, ctx) |
Whether to send protocol messages (identity, state, MCP) to this connection. Default: true |
onStart
onStart() is called once when the agent first starts, before any connections are established:
export class MyAgent extends Agent {
async onStart() {
// Initialize resources
console.log(`Agent ${this.name} starting...`);
// Load data from storage
const savedData = this.sql`SELECT * FROM cache`;
for (const row of savedData) {
// Rebuild in-memory state from persistent storage
}
}
onConnect(connection) {
// By the time connections arrive, onStart has completed
}
}
export class MyAgent extends Agent {
async onStart() {
// Initialize resources
console.log(`Agent ${this.name} starting...`);
// Load data from storage
const savedData = this.sql`SELECT * FROM cache`;
for (const row of savedData) {
// Rebuild in-memory state from persistent storage
}
}
onConnect(connection: Connection) {
// By the time connections arrive, onStart has completed
}
}
Handling connections
Define onConnect and onMessage methods on your Agent to accept WebSocket connections:
import { Agent, Connection, ConnectionContext, WSMessage } from "agents";
export class ChatAgent extends Agent {
async onConnect(connection, ctx) {
// Connections are automatically accepted
// Access the original request for auth, headers, cookies
const url = new URL(ctx.request.url);
const token = url.searchParams.get("token");
if (!token) {
connection.close(4001, "Unauthorized");
return;
}
// Store user info on this connection
connection.setState({ authenticated: true });
}
async onMessage(connection, message) {
if (typeof message === "string") {
// Handle text message
const data = JSON.parse(message);
connection.send(JSON.stringify({ received: data }));
}
}
}
import { Agent, Connection, ConnectionContext, WSMessage } from "agents";
export class ChatAgent extends Agent {
async onConnect(connection: Connection, ctx: ConnectionContext) {
// Connections are automatically accepted
// Access the original request for auth, headers, cookies
const url = new URL(ctx.request.url);
const token = url.searchParams.get("token");
if (!token) {
connection.close(4001, "Unauthorized");
return;
}
// Store user info on this connection
connection.setState({ authenticated: true });
}
async onMessage(connection: Connection, message: WSMessage) {
if (typeof message === "string") {
// Handle text message
const data = JSON.parse(message);
connection.send(JSON.stringify({ received: data }));
}
}
}
Connection object
Each connected client has a unique Connection object:
| Property/Method | Type | Description |
|---|---|---|
id |
string |
Unique identifier for this connection |
uri |
string \| null |
URL of the original WebSocket upgrade request. Persists across hibernation |
state |
State |
Per-connection state object |
setState(state) |
void |
Update connection state |
send(message) |
void |
Send message to this client |
close(code?, reason?) |
void |
Close the connection |
tags |
readonly string[] |
Tags assigned via getConnectionTags. Always includes the connection ID as the first tag |
server |
string |
The agent instance name (same as this.name on the Agent) |
Per-connection state
Store data specific to each connection (user info, preferences, etc.):
export class ChatAgent extends Agent {
async onConnect(connection, ctx) {
const userId = new URL(ctx.request.url).searchParams.get("userId");
connection.setState({
userId: userId || "anonymous",
role: "user",
joinedAt: Date.now(),
});
}
async onMessage(connection, message) {
// Access connection-specific state
console.log(`Message from ${connection.state.userId}`);
}
}
interface ConnectionState {
userId: string;
role: "admin" | "user";
joinedAt: number;
}
export class ChatAgent extends Agent {
async onConnect(
connection: Connection<ConnectionState>,
ctx: ConnectionContext,
) {
const userId = new URL(ctx.request.url).searchParams.get("userId");
connection.setState({
userId: userId || "anonymous",
role: "user",
joinedAt: Date.now(),
});
}
async onMessage(connection: Connection<ConnectionState>, message: WSMessage) {
// Access connection-specific state
console.log(`Message from ${connection.state.userId}`);
}
}
Broadcasting to all clients
Use this.broadcast() to send a message to all connected clients:
export class ChatAgent extends Agent {
async onMessage(connection, message) {
// Broadcast to all connected clients
this.broadcast(
JSON.stringify({
from: connection.id,
message: message,
timestamp: Date.now(),
}),
);
}
// Broadcast from any method
async notifyAll(event, data) {
this.broadcast(JSON.stringify({ event, data }));
}
}
export class ChatAgent extends Agent {
async onMessage(connection: Connection, message: WSMessage) {
// Broadcast to all connected clients
this.broadcast(
JSON.stringify({
from: connection.id,
message: message,
timestamp: Date.now(),
}),
);
}
// Broadcast from any method
async notifyAll(event: string, data: unknown) {
this.broadcast(JSON.stringify({ event, data }));
}
}
Excluding connections
Pass an array of connection IDs to exclude from the broadcast:
// Broadcast to everyone except the sender
this.broadcast(
JSON.stringify({ type: "user-typing", userId: "123" }),
[connection.id], // Do not send to the originator
);
// Broadcast to everyone except the sender
this.broadcast(
JSON.stringify({ type: "user-typing", userId: "123" }),
[connection.id], // Do not send to the originator
);
Connection tags
Tag connections for easy filtering. Override getConnectionTags() to assign tags when a connection is established:
export class ChatAgent extends Agent {
getConnectionTags(connection, ctx) {
const url = new URL(ctx.request.url);
const role = url.searchParams.get("role");
const tags = [];
if (role === "admin") tags.push("admin");
if (role === "moderator") tags.push("moderator");
return tags; // Up to 9 tags, max 256 chars each
}
// Later, broadcast only to admins
notifyAdmins(message) {
for (const conn of this.getConnections("admin")) {
conn.send(message);
}
}
}
export class ChatAgent extends Agent {
getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {
const url = new URL(ctx.request.url);
const role = url.searchParams.get("role");
const tags: string[] = [];
if (role === "admin") tags.push("admin");
if (role === "moderator") tags.push("moderator");
return tags; // Up to 9 tags, max 256 chars each
}
// Later, broadcast only to admins
notifyAdmins(message: string) {
for (const conn of this.getConnections("admin")) {
conn.send(message);
}
}
}
Connection management methods
| Method | Signature | Description |
|---|---|---|
getConnections |
(tag?: string) => Iterable<Connection> |
Get all connections, optionally by tag |
getConnection |
(id: string) => Connection \| undefined |
Get connection by ID |
getConnectionTags |
(connection, ctx) => string[] |
Override to tag connections |
broadcast |
(message, without?: string[]) => void |
Send to all connections |
isConnectionReadonly |
(connection) => boolean |
Check if a connection is readonly |
isConnectionProtocolEnabled |
(connection) => boolean |
Check if protocol messages are enabled for this connection |
Handling binary data
Messages can be strings or binary (ArrayBuffer / ArrayBufferView):
export class FileAgent extends Agent {
async onMessage(connection, message) {
if (message instanceof ArrayBuffer) {
// Handle binary upload
const bytes = new Uint8Array(message);
await this.processFile(bytes);
connection.send(
JSON.stringify({ status: "received", size: bytes.length }),
);
} else if (typeof message === "string") {
// Handle text command
const command = JSON.parse(message);
// ...
}
}
}
export class FileAgent extends Agent {
async onMessage(connection: Connection, message: WSMessage) {
if (message instanceof ArrayBuffer) {
// Handle binary upload
const bytes = new Uint8Array(message);
await this.processFile(bytes);
connection.send(
JSON.stringify({ status: "received", size: bytes.length }),
);
} else if (typeof message === "string") {
// Handle text command
const command = JSON.parse(message);
// ...
}
}
}
Note
Agents automatically send JSON text frames (identity, state, MCP servers) to every connection. If your client only handles binary data and cannot process these frames, use shouldSendProtocolMessages to suppress them.
Error and close handling
Handle connection errors and disconnections. The onError method has two overloads — one for WebSocket connection errors and one for server-level errors:
export class ChatAgent extends Agent {
// WebSocket connection error
// Server-level error (not tied to a specific connection)
onError(connectionOrError, error) {
if (error) {
console.error(`Connection ${connectionOrError.id} error:`, error);
} else {
console.error("Server error:", connectionOrError);
}
}
async onClose(connection, code, reason, wasClean) {
console.log(`Connection ${connection.id} closed: ${code} ${reason}`);
this.broadcast(
JSON.stringify({
event: "user-left",
userId: connection.state?.userId,
}),
);
}
}
export class ChatAgent extends Agent {
// WebSocket connection error
onError(connection: Connection, error: unknown): void;
// Server-level error (not tied to a specific connection)
onError(error: unknown): void;
onError(connectionOrError: Connection | unknown, error?: unknown) {
if (error) {
console.error(
`Connection ${(connectionOrError as Connection).id} error:`,
error,
);
} else {
console.error("Server error:", connectionOrError);
}
}
async onClose(
connection: Connection,
code: number,
reason: string,
wasClean: boolean,
) {
console.log(`Connection ${connection.id} closed: ${code} ${reason}`);
this.broadcast(
JSON.stringify({
event: "user-left",
userId: connection.state?.userId,
}),
);
}
}
