Each agent is a named Durable Object. Routing is how a request in your Worker reaches the right one. routeAgentRequest() reads the Durable Object binding and the object's name from the URL, and forwards the request to that object. Inside the object, the Lifecycle takes over and offers the request to its capabilities, then to your onRequest() hook.
routeAgentRequest(request, env) Worker
└─ stub.fetch(request) picks the object from the URL
└─ Lifecycle Durable Object
├─ startup, on a cold object
├─ capabilities, in registration order
└─ host onRequest()routeAgentRequest() works with any Durable Object that installs the Lifecycle. Import it from agents/routing, or from agents:
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { routeAgentRequest } from "agents/routing";
export class MyAgent extends DurableObject {
lifecycle = Lifecycle.install(this);
onRequest(request) {
return new Response(`Hello from ${this.lifecycle.name}`);
}
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { routeAgentRequest } from "agents/routing";
export class MyAgent extends DurableObject<Env> {
readonly lifecycle = Lifecycle.install(this);
onRequest(request: Request): Response {
return new Response(`Hello from ${this.lifecycle.name}`);
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;Bind the class as a SQLite-backed Durable Object:
{
"durable_objects": {
"bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }],
}[[durable_objects.bindings]]
name = "MyAgent"
class_name = "MyAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "MyAgent" ]routeAgentRequest() handles both HTTP requests and WebSocket upgrades. It returns:
nullwhen the path does not start with the prefix, so your Worker can handle other routes.- A
400response when the path matches the prefix but no Durable Object binding matches. - The object's response otherwise.
The default URL shape is /agents/:binding/:name:
https://example.com/agents/my-agent/alice/settings
└──┬───┘ └─┬─┘└───┬──┘
binding name sub-path:bindingis the name of a Durable Object binding inenv, in kebab case. The bindingMyAgentand the bindingMY_AGENTboth becomemy-agent.ChatRoombecomeschat-room.:nameis the object's name. The router addresses the object withidFromName(name), and the object reads it aslifecycle.name.- The sub-path is anything after the name. The object receives the full URL, so
onRequest()can route on/settings.
To change the agents segment, pass the prefix option.
Export each class and bind each one. routeAgentRequest() finds the binding from the URL, so one call routes to all of them:
import { routeAgentRequest } from "agents/routing";
export { ChatRoom } from "./chat-room";
export { UserProfile } from "./user-profile";
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
};import { routeAgentRequest } from "agents/routing";
export { ChatRoom } from "./chat-room";
export { UserProfile } from "./user-profile";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;{
"durable_objects": {
"bindings": [
{ "name": "ChatRoom", "class_name": "ChatRoom" },
{ "name": "UserProfile", "class_name": "UserProfile" },
],
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["ChatRoom", "UserProfile"] },
],
}[[durable_objects.bindings]]
name = "ChatRoom"
class_name = "ChatRoom"
[[durable_objects.bindings]]
name = "UserProfile"
class_name = "UserProfile"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "ChatRoom", "UserProfile" ]Requests to /agents/chat-room/... reach ChatRoom, and requests to /agents/user-profile/... reach UserProfile.
The name decides which object handles a request. Each name is its own object, with its own storage, alarm, and connections. Pick names that match how the data is shared:
| Pattern | Name | Example URL |
|---|---|---|
| One per user | The user ID | /agents/user-profile/user-abc123 |
| One per resource | A room or document ID | /agents/chat-room/general |
| One per task | A generated ID | /agents/report/report-2026-10-29 |
| A singleton | A fixed name | /agents/app-config/default |
Use a name that clients cannot guess when the object holds private data, and check who is calling before you route. For more information, refer to Authentication.
The Lifecycle requires named objects. Startup fails for an object created with newUniqueId() or idFromString(), or with a name longer than 1,024 bytes.
The object receives the full URL. Match the end of the path in onRequest():
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
export class UserProfile extends DurableObject {
lifecycle = Lifecycle.install(this);
async onRequest(request) {
const url = new URL(request.url);
if (request.method === "GET" && url.pathname.endsWith("/settings")) {
return Response.json((await this.ctx.storage.get("settings")) ?? {});
}
if (request.method === "PUT" && url.pathname.endsWith("/settings")) {
await this.ctx.storage.put("settings", await request.json());
return new Response(null, { status: 204 });
}
return new Response("Not found", { status: 404 });
}
}import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
export class UserProfile extends DurableObject<Env> {
readonly lifecycle = Lifecycle.install(this);
async onRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
if (request.method === "GET" && url.pathname.endsWith("/settings")) {
return Response.json((await this.ctx.storage.get("settings")) ?? {});
}
if (request.method === "PUT" && url.pathname.endsWith("/settings")) {
await this.ctx.storage.put("settings", await request.json());
return new Response(null, { status: 204 });
}
return new Response("Not found", { status: 404 });
}
}- Routing options covers CORS, placement, startup props, and retries.
- Authentication checks callers in the Worker, before a request reaches the object.
- Custom routing reaches objects without the default URL shape, including over RPC.
- HTTP and Server-Sent Events handles requests in
onRequest()and streams responses. - Request dispatch describes what happens to the request inside the object.