Skip to content

Routing options

Last updated View as MarkdownAgent setup

Pass options to routeAgentRequest() as the third argument. Their type is AgentOptions, exported from agents/routing:

Option Type Description
prefix string URL segments before the binding and name. Defaults to agents.
cors boolean | HeadersInit Adds CORS headers to matched responses and answers matched OPTIONS preflight requests.
jurisdiction DurableObjectJurisdiction Creates objects in a jurisdiction, such as "eu".
locationHint DurableObjectLocationHint Hints where to create objects, such as "enam".
props object JSON-serializable values passed to onStart(props) when the request starts the object.
routingRetry false | RoutingRetryOptions Retries transient Durable Object errors. Enabled by default.
onBeforeConnect (request, route) => Response | Request | void Runs in the Worker before a WebSocket upgrade is forwarded.
onBeforeRequest (request, route) => Response | Request | void Runs in the Worker before an HTTP request is forwarded.

onBeforeConnect and onBeforeRequest are covered in Authentication.

Prefix

prefix replaces the agents segment. It can contain several segments:

import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request, env) {
		// Matches /api/v1/my-agent/alice
		return (
			(await routeAgentRequest(request, env, { prefix: "api/v1" })) ??
			new Response("Not found", { status: 404 })
		);
	},
};
import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// Matches /api/v1/my-agent/alice
		return (
			(await routeAgentRequest(request, env, { prefix: "api/v1" })) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

useAgent and AgentClient build /agents/... paths. With another prefix, pass the full path as their basePath option, such as basePath: "api/v1/my-agent/alice".

CORS

Set cors: true when your frontend runs on another origin. It adds these headers to every matched HTTP response:

  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS
  • Access-Control-Allow-Headers: *
  • Access-Control-Max-Age: 86400

For credentialed requests, pass your own headers with a specific origin:

import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env, {
				cors: {
					"Access-Control-Allow-Origin": "https://app.example.com",
					"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
					"Access-Control-Allow-Headers": "Content-Type, Authorization",
					"Access-Control-Allow-Credentials": "true",
				},
			})) ?? new Response("Not found", { status: 404 })
		);
	},
};
import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		return (
			(await routeAgentRequest(request, env, {
				cors: {
					"Access-Control-Allow-Origin": "https://app.example.com",
					"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
					"Access-Control-Allow-Headers": "Content-Type, Authorization",
					"Access-Control-Allow-Credentials": "true",
				},
			})) ?? new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Matched OPTIONS requests get the CORS headers without reaching the object. Responses that onBeforeRequest returns also get them. WebSocket upgrade responses never do.

Placement

jurisdiction and locationHint apply to every object the call routes to:

import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env, {
				jurisdiction: "eu",
				locationHint: "weur",
			})) ?? new Response("Not found", { status: 404 })
		);
	},
};
import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		return (
			(await routeAgentRequest(request, env, {
				jurisdiction: "eu",
				locationHint: "weur",
			})) ?? new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

An object in a jurisdiction has a different ID from an object with the same name outside it. Use the same jurisdiction every time you route to an object. The location hint only applies when the object is first created. For the available values, refer to Data location.

Props

props passes values from the Worker to the object's startup. Each capability's onStart() hook and the host's onStart(props) receive them. Type them with the second type argument of Lifecycle.install():

import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { routeAgentRequest } from "agents/routing";

export class MyAgent extends DurableObject {
	lifecycle = Lifecycle.install(this);
	#plan = "free";

	onStart(props) {
		this.#plan = props?.plan ?? "free";
	}

	onRequest() {
		return Response.json({ plan: this.#plan });
	}
}

export default {
	async fetch(request, env) {
		const userId = request.headers.get("x-user-id") ?? "anonymous";
		return (
			(await routeAgentRequest(request, env, {
				props: { userId, plan: "pro" },
			})) ?? new Response("Not found", { status: 404 })
		);
	},
};
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { routeAgentRequest } from "agents/routing";

type Props = { userId: string; plan: "free" | "pro" };

export class MyAgent extends DurableObject<Env> {
	readonly lifecycle = Lifecycle.install<Env, Props>(this);
	#plan: Props["plan"] = "free";

	onStart(props?: Props): void {
		this.#plan = props?.plan ?? "free";
	}

	onRequest(): Response {
		return Response.json({ plan: this.#plan });
	}
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const userId = request.headers.get("x-user-id") ?? "anonymous";
		return (
			(await routeAgentRequest(request, env, {
				props: { userId, plan: "pro" },
			})) ?? new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Props only reach a cold object. A warm object has already run startup and ignores them. Do not keep anything in props that you need on every request. Store it, or send it with each request.

The same props go to whichever object the URL matches. To start an object over RPC with props, call lifecycle.start(props). For more information, refer to Call an object over RPC.

Retries

routingRetry retries Durable Object errors that the platform marks as retryable and not caused by overload. It backs off exponentially, with jitter. Its type is RoutingRetryOptions:

Field Default Description
maxAttempts 3 Attempts, including the first
baseDelayMs 100 Base delay for the backoff, in milliseconds
maxDelayMs 800 Maximum delay between attempts, in milliseconds
onRetry None Called before each retry with the error, attempt number, and delay
import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env, {
				routingRetry: {
					maxAttempts: 5,
					onRetry: ({ attempt, delayMs, name }) => {
						console.warn(
							`Retrying ${name}, attempt ${attempt} in ${delayMs} ms`,
						);
					},
				},
			})) ?? new Response("Not found", { status: 404 })
		);
	},
};
import { routeAgentRequest } from "agents/routing";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		return (
			(await routeAgentRequest(request, env, {
				routingRetry: {
					maxAttempts: 5,
					onRetry: ({ attempt, delayMs, name }) => {
						console.warn(
							`Retrying ${name}, attempt ${attempt} in ${delayMs} ms`,
						);
					},
				},
			})) ?? new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Pass routingRetry: false to turn retries off.

Was this helpful?