Agent class internals
Last updated Aug 17, 2026
The core of the agents library is the Agent class. You extend it, override a few methods, and get state management, WebSockets, scheduling, RPC, and more for free. This page explains how Agent is built, layer by layer, so you understand what is happening under the hood.
The snippets shown here are illustrative and do not necessarily represent best practices. For the full API, refer to the API reference and the source code ↗︎.
What is the Agent?
The Agent class is an extension of DurableObject — agents are Durable Objects. If you are not familiar with Durable Objects, read What are Durable Objects first. At their core, Durable Objects are globally addressable (each instance has a unique ID), single-threaded compute instances with long-term storage (key-value and SQLite).
Agent does not extend DurableObject directly. It extends Server from the partyserver ↗︎ package, which extends DurableObject. Think of it as layers: DurableObject > Server > Agent.
Layer 0: Durable Object
Let's briefly consider which primitives are exposed by Durable Objects so we understand how the outer layers make use of them. The Durable Object class comes with:
constructor
constructor(ctx: DurableObjectState, env: Env) {}
The Workers runtime always calls the constructor to handle things internally. This means two things:
- While the constructor is called every time the Durable Object is initialized, the signature is fixed. Developers cannot add or update parameters from the constructor.
- Instead of instantiating the class manually, developers must use the binding APIs and do it through the DurableObjectNamespace.
RPC
By writing a Durable Object class which inherits from the built-in type DurableObject, public methods are exposed as RPC methods, which developers can call using a DurableObjectStub from a Worker.
// This instance could've been active, hibernated,
// not initialized or maybe had never even been created!
const stub = env.MY_DO.getByName("foo");
// We can call any public method on the class. The runtime
// ensures the constructor is called if the instance was not active.
await stub.bar();
fetch()
Durable Objects can take a Request from a Worker and send a Response back. This can only be done through the fetch method (which the developer must implement).
WebSockets
Durable Objects include first-class support for WebSockets. A Durable Object can accept a WebSocket it receives from a Request in fetch and forget about it. The base class provides methods that developers can implement that are called as callbacks. They effectively replace the need for event listeners.
The base class provides webSocketMessage(ws, message), webSocketClose(ws, code, reason, wasClean) and webSocketError(ws , error) (API).
export class MyDurableObject extends DurableObject {
async fetch(request) {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages.
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws, message) {
ws.send(message);
}
}
alarm()
HTTP and RPC requests are not the only entrypoints for a Durable Object. Alarms allow developers to schedule an event to trigger at a later time. Whenever the next alarm is due, the runtime will call the alarm() method, which is left to the developer to implement.
To schedule an alarm, you can use the this.ctx.storage.setAlarm() method. For more information, refer to Alarms.
this.ctx
The base DurableObject class sets the DurableObjectState into this.ctx. There are a lot of interesting methods and properties, but we will focus on this.ctx.storage.
this.ctx.storage
DurableObjectStorage is the main interface with the Durable Object's persistence mechanisms, which include both a KV and SQLITE synchronous APIs.
const sql = this.ctx.storage.sql;
// Synchronous SQL query
const rows = sql.exec("SELECT * FROM contacts WHERE country = ?", "US");
// Key-value storage
const token = this.ctx.storage.get("someToken");
this.ctx.env
Lastly, it is worth mentioning that the Durable Object also has the Worker Env in this.env. Learn more in Bindings.
Layer 1: Server (partyserver)
Now that you have seen what Durable Objects provide out of the box, the Server class from partyserver ↗︎ will make more sense. It is an opinionated DurableObject wrapper that replaces low-level primitives with developer-friendly callbacks.
Server does not add any storage operations of its own — it only wraps the Durable Object lifecycle.
Addressing
partyserver exposes helpers to address Durable Objects by name instead of going through bindings manually. This includes a URL routing scheme (<your-worker>/servers/:durableClass/:durableName) that the Agent layer builds on.
// Note the await here!
const stub = await getServerByName(env.MY_DO, "foo");
// We can still call RPC methods.
await stub.bar();
The URL scheme also enables a request router. In the Agent layer, this is re-exported as routeAgentRequest:
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const res = await routeAgentRequest(request, env);
if (res) return res;
return new Response("Not found", { status: 404 });
}
onStart
The addressing layer allows Server to expose an onStart callback that runs every time the Durable Object starts up (after eviction, hibernation, or first creation) and before any fetch or RPC call.
class MyServer extends Server {
onStart() {
// Some initialization logic that you wish
// to run every time the DO is started up.
const sql = this.ctx.storage.sql;
sql.exec(`...`);
}
}
onRequest and onConnect
Server already implements fetch for the underlying Durable Object and exposes two different callbacks that developers can make use of, onRequest and onConnect for HTTP requests and incoming WS connections, respectively (WebSocket connections are accepted by default).
class MyServer extends Server {
async onRequest(request: Request) {
const url = new URL(request.url);
return new Response(`Hello from ${url.origin}!`);
}
async onConnect(conn, ctx) {
const { request } = ctx;
const url = new URL(request.url);
// Connections are a WebSocket wrapper
conn.send(`Hello from ${url.origin}!`);
}
}
WebSockets
Just as onConnect is the callback for every new connection, Server also provides wrappers on top of the default callbacks from the DurableObject class: onMessage, onClose and onError.
There's also this.broadcast that sends a WS message to all connected clients (no magic, just a loop over this.getConnections()!).
this.name
It is hard to get a Durable Object's name from within it. partyserver tries to make it available in this.name but it is not a perfect solution. Learn more about it in this GitHub issue ↗︎.
Layer 2: Agent
Now finally, the Agent class. Agent extends Server and provides opinionated primitives for stateful, schedulable, and observable agents that can communicate via RPC, WebSockets, and (even!) email.
this.state and this.setState()
One of the core features of Agent is automatic state persistence. Developers define the shape of their state via the generic parameter and initialState (which is only used if no state exists in storage), and the Agent handles loading, saving, and broadcasting state changes (check Server's this.broadcast() above).
this.state is a getter that lazily loads state from storage (SQL). State is persisted across Durable Object evictions when it is updated with this.setState(), which automatically serializes the state and writes it back to storage.
There's also this.onStateChanged that you can override to react to state changes.
class MyAgent extends Agent<Env, { count: number }> {
initialState = { count: 0 };
increment() {
this.setState({ count: this.state.count + 1 });
}
onStateChanged(state, source) {
console.log("State updated:", state);
}
}
State is stored in the cf_agents_state SQL table. State messages are sent with type: "cf_agent_state" (both from the client and the server). Since agents provides JS and React clients, real-time state updates are available out of the box.
this.sql
The Agent provides a convenient sql template tag for executing queries against the Durable Object's SQL storage. It constructs parameterized queries and executes them. This uses the synchronous SQL API from this.ctx.storage.sql.
class MyAgent extends Agent {
onStart() {
this.sql`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT
)
`;
const userId = "1";
const userName = "Alice";
this.sql`INSERT INTO users (id, name) VALUES (${userId}, ${userName})`;
const users = this.sql<{ id: string; name: string }>`
SELECT * FROM users WHERE id = ${userId}
`;
console.log(users); // [{ id: "1", name: "Alice" }]
}
}
RPC and Callable Methods
agents takes Durable Objects RPC one step further by implementing RPC through WebSockets, so clients can call methods on the Agent directly. To make a method callable through WebSocket, use the @callable() decorator. Methods can return a serializable value or a stream (when using @callable({ streaming: true })).
class MyAgent extends Agent {
@callable({ description: "Add two numbers" })
async add(a: number, b: number) {
return a + b;
}
}
Clients can invoke this method by sending a WebSocket message:
{
"type": "rpc",
"id": "unique-request-id",
"method": "add",
"args": [2, 3]
}
For example, with the provided React client, it is as easy as:
const { stub } = useAgent({ name: "my-agent" });
const result = await stub.add(2, 3);
console.log(result); // 5
this.queue and friends
Agents include a built-in task queue for deferred execution. This is useful for offloading work or retrying operations. The available methods are this.queue, this.dequeue, this.dequeueAll, this.dequeueAllByCallback, this.getQueue, and this.getQueues.
```ts class MyAgent extends Agent { async onConnect() { // Queue a task to be executed later await this.queue("processTask", { userId: "123" }); }
