Skip to content

Routing

Last updated View as MarkdownAgent setup

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()

Route requests from a Worker

routeAgentRequest() works with any Durable Object that installs the Lifecycle. Import it from agents/routing, or from agents:

src/index.jsjs
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 })
		);
	},
};
src/index.tsts
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:

  • null when the path does not start with the prefix, so your Worker can handle other routes.
  • A 400 response when the path matches the prefix but no Durable Object binding matches.
  • The object's response otherwise.

URL shape

The default URL shape is /agents/:binding/:name:

https://example.com/agents/my-agent/alice/settings
                           └──┬───┘ └─┬─┘└───┬──┘
                           binding  name   sub-path
  • :binding is the name of a Durable Object binding in env, in kebab case. The binding MyAgent and the binding MY_AGENT both become my-agent. ChatRoom becomes chat-room.
  • :name is the object's name. The router addresses the object with idFromName(name), and the object reads it as lifecycle.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.

Several agent classes

Export each class and bind each one. routeAgentRequest() finds the binding from the URL, so one call routes to all of them:

src/index.jsjs
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 })
		);
	},
};
src/index.tsts
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.

Name your agents

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.

Handle sub-paths

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 });
	}
}

Next steps

Was this helpful?