Sub-agents
Last updated Sep 15, 2026 Spawn child agents as co-located Durable Objects with their own isolated SQLite storage. The parent gets a typed RPC stub for calling methods on the child — every public method on the child class is callable as a remote procedure call with Promise-wrapped return types.
Use sub-agents when a single user or entity owns an open-ended set of long-lived agents, such as chats, documents, sessions, shards, or projects. Each sub-agent runs in parallel with its own state while the parent coordinates discovery, access control, and lifecycle.
If you want a parent chat agent to dispatch another chat-capable agent during a single turn and render that child's progress inline, use Agents as tools. Agents as tools are built on sub-agents, but add a parent-side run registry, streaming agent-tool-event frames, replay, cancellation, and cleanup.
Quick start
import { Agent } from "agents";
export class Orchestrator extends Agent {
async delegateWork() {
const researcher = await this.subAgent(Researcher, "research-1");
const findings = await researcher.search("cloudflare agents sdk");
return findings;
}
}
export class Researcher extends Agent {
async search(query) {
const results = await fetch(`https://api.example.com/search?q=${query}`);
return results.json();
}
}
import { Agent } from "agents";
export class Orchestrator extends Agent {
async delegateWork() {
const researcher = await this.subAgent(Researcher, "research-1");
const findings = await researcher.search("cloudflare agents sdk");
return findings;
}
}
export class Researcher extends Agent {
async search(query: string) {
const results = await fetch(`https://api.example.com/search?q=${query}`);
return results.json();
}
}
Both classes must be exported from the worker entry point. No separate Durable Object bindings are needed for child-only classes — child classes are discovered automatically via ctx.exports.
{
"$schema": "./node_modules/wrangler/config-schema.json",
// Set this to today's date
"compatibility_date": "2026-10-27",
"compatibility_flags": [
"nodejs_compat"
],
"durable_objects": {
"bindings": [
{
"class_name": "Orchestrator",
"name": "Orchestrator"
}
]
},
"migrations": [
{
"new_sqlite_classes": [
"Orchestrator"
],
"tag": "v1"
}
]
}
# Set this to today's date
compatibility_date = "2026-10-27"
compatibility_flags = ["nodejs_compat"]
[[durable_objects.bindings]]
class_name = "Orchestrator"
name = "Orchestrator"
[[migrations]]
new_sqlite_classes = ["Orchestrator"]
tag = "v1"
Only the top-level parent agent needs a Durable Object binding and migration. Child agents are created as facets of the parent — they share the same machine but have fully isolated SQLite storage.
subAgent
Get or create a named sub-agent. The first call for a given name triggers the child's onStart(). Subsequent calls return the existing instance.
class Agent {}
class Agent {
async subAgent<T extends Agent>(
cls: SubAgentClass<T>,
name: string,
): Promise<SubAgentStub<T>>;
}
| Parameter | Type | Description |
|---|---|---|
cls |
SubAgentClass<T> |
The Agent subclass. Must be exported from the worker entry point, and the export name must match the class name. |
name |
string |
Unique name for this child instance. The same name always returns the same child. |
Returns a SubAgentStub<T> — a typed RPC stub where every user-defined method on T is available as a Promise-returning remote call.
SubAgentStub
The stub exposes all public instance methods you define on the child class. Methods inherited from Agent (lifecycle hooks, setState, broadcast, sql, and so on) are excluded — only your custom methods appear on the stub.
Return types are automatically wrapped in Promise if they are not already:
class MyChild extends Agent {
greet(name) {
return `Hello, ${name}`;
}
async fetchData(url) {
return fetch(url).then((r) => r.json());
}
}
// On the stub:
// greet(name: string) => Promise<string> (sync → wrapped)
// fetchData(url: string) => Promise<unknown> (already async → unchanged)
class MyChild extends Agent {
greet(name: string): string {
return `Hello, ${name}`;
}
async fetchData(url: string): Promise<unknown> {
return fetch(url).then((r) => r.json());
}
}
// On the stub:
// greet(name: string) => Promise<string> (sync → wrapped)
// fetchData(url: string) => Promise<unknown> (already async → unchanged)
Requirements
- The child class must extend
Agent - The child class must be exported from the worker entry point (
export class MyChild extends Agent) - The export name must match the class name —
export { Foo as Bar }is not supported - The top-level parent class must be bound as a Durable Object namespace in
wrangler.jsonc - A facet-only child class does not need to be registered under
new_sqlite_classesunless the same class is also bound as a top-level Durable Object elsewhere - Nested facet parents do not need their own top-level Durable Object bindings; the runtime resolves nested children through the root parent namespace
- The child class name cannot be
Sub, because/sub/is reserved as the URL separator for nested routes
Notes for testing
Tests that use @cloudflare/vitest-plugin may need to list facet classes as test-only Durable Object bindings so ctx.exports provides a facet-compatible class value. Keep those facet classes out of new_sqlite_classes; the extra binding belongs only in test wrangler.jsonc files and is not a production Worker requirement.
abortSubAgent
Forcefully stop a running sub-agent. The child stops executing immediately and restarts on the next subAgent() call. Storage is preserved — only the running instance is killed.
class Agent {}
class Agent {
abortSubAgent(cls: SubAgentClass, name: string, reason?: unknown): void;
}
| Parameter | Type | Description |
|---|---|---|
cls |
SubAgentClass |
The Agent subclass used when creating the child |
name |
string |
Name of the child to abort |
reason |
unknown |
Error thrown to any pending or future RPC callers |
Abort is transitive — if the child has its own sub-agents, they are also aborted.
deleteSubAgent
Abort the child (if running) and permanently wipe its storage. The next subAgent() call creates a fresh instance with empty SQLite.
class Agent {}
class Agent {
deleteSubAgent(cls: SubAgentClass, name: string): void;
}
| Parameter | Type | Description |
|---|---|---|
cls |
SubAgentClass |
The Agent subclass used when creating the child |
name |
string |
Name of the child to delete |
Deletion is transitive — the child's own sub-agents are also deleted.
Introspection and access control
hasSubAgent
Check whether a child has been spawned and not deleted. This is backed by a framework-maintained SQLite registry.
if (!this.hasSubAgent(Chat, id)) {
return new Response("Not found", { status: 404 });
}
if (!this.hasSubAgent(Chat, id)) {
return new Response("Not found", { status: 404 });
}
listSubAgents
List spawned sub-agents, optionally filtered by class. Rows are returned in creation order.
const chats = this.listSubAgents(Chat);
// [{ className: "Chat", name: "chat-abc", createdAt: 1700000000000 }]
const chats = this.listSubAgents(Chat);
// [{ className: "Chat", name: "chat-abc", createdAt: 1700000000000 }]
onBeforeSubAgent
Override this middleware hook on the parent to gate, mutate, or short-circuit incoming /sub/ requests before the framework wakes the child. It mirrors onBeforeConnect and onBeforeRequest.
The hook can return:
| Return value | Effect |
|---|---|
void |
Forward the original request to the child |
Request |
Forward a modified request |
Response |
Short-circuit and do not wake the child |
export class Inbox extends Agent {
async onBeforeSubAgent(_request, { className, name }) {
// Strict registry gate: only allow clients to reach chats that were created.
if (!this.hasSubAgent(className, name)) {
return new Response(`${className} "${name}" not found`, {
status: 404,
});
}
}
}
export class Inbox extends Agent {
override async onBeforeSubAgent(_request, { className, name }) {
// Strict registry gate: only allow clients to reach chats that were created.
if (!this.hasSubAgent(className, name)) {
return new Response(`${className} "${name}" not found`, {
status: 404,
});
}
}
}
WebSocket upgrade requests flow through this hook the same way as plain HTTP requests. If you return a modified Request, preserve the original WebSocket upgrade headers.
Parent and child identity
Sub-agents know who their parent is through this.parentPath and this.selfPath.
// Inside a Chat spawned by Inbox:
this.parentPath;
// [{ className: "Inbox", name: "user-123" }]
this.selfPath;
// [
// { className: "Inbox", name: "user-123" },
// { className: "Chat", name: "chat-abc" }
// ]
// Inside a Chat spawned by Inbox:
this.parentPath;
// [{ className: "Inbox", name: "user-123" }]
this.selfPath;
// [
// { className: "Inbox", name: "user-123" },
// { className: "Chat", name: "chat-abc" }
// ]
parentPath is root-first, so the direct parent is always parentPath.at(-1). Top-level agents have parentPath === [].
Use parentAgent(Cls) from a sub-agent to get a typed RPC stub to its immediate parent:
const inbox = await this.parentAgent(Inbox);
await inbox.recordTurn(this.name, "...");
const inbox = await this.parentAgent(Inbox);
await inbox.recordTurn(this.name, "...");
parentAgent() resolves the direct parent even when that parent is itself a facet-only sub-agent, using a root-side RPC bridge under the hood. This gives you typed method calls to the immediate parent without requiring every nested parent class to be bound as a top-level Durable Object.
For grandparents and further ancestors, iterate this.parentPath and call getAgentByName() directly. If the binding name does not match the class name, call getAgentByName(env.MY_BINDING, this.parentPath.at(-1)!.name) instead of parentAgent().
Client routing
useAgent({ sub })
Extend any useAgent call with a sub chain to connect to a descendant facet:
const chat = useAgent({
agent: "Inbox",
name: userId,
sub: [{ agent: "Chat", name: chatId }],
});
const chat = useAgent({
agent: "Inbox",
name: userId,
sub: [{ agent: "Chat", name: chatId }],
});
The hook builds a URL like /agents/inbox/user-123/sub/chat/chat-abc and opens a direct WebSocket to the Chat child. Every other useAgent feature works as usual: state sync, stub calls, @callable RPC, and useAgentChat on top of the returned socket.
Direct HTTP and WebSocket URLs
Use buildAgentPath() to create a canonical pathname for an Agent identity. The same pathname supports HTTP requests and WebSocket connections.
import { buildAgentPath } from "agents";
const path = buildAgentPath(
[
{ className: "Inbox", name: userId },
{ className: "Chat", name: chatId },
],
{ leafPath: "/callbacks/job" },
);
// /agents/inbox/{userId}/sub/chat/{chatId}/callbacks/job
import { buildAgentPath } from "agents";
const path = buildAgentPath(
[
{ className: "Inbox", name: userId },
{ className: "Chat", name: chatId },
],
{ leafPath: "/callbacks/job" },
);
// /agents/inbox/{userId}/sub/chat/{chatId}/callbacks/job
Inside an Agent, pass this.selfPath directly. If the root Durable Object binding name differs from its class name, also pass rootBinding in the options. Use buildAgentUrl() to add a public origin for callbacks, webhooks, approvals, or asynchronous job completion.
import { buildAgentUrl } from "agents";
export class Chat extends Agent {
callbackUrl() {
return buildAgentUrl(this.env.PUBLIC_ORIGIN, this.selfPath, {
leafPath: "/callbacks/job",
});
}
async onRequest(request) {
if (new URL(request.url).pathname === "/callbacks/job") {
return this.handleJobCallback(request);
}
return new Response("Not found", { status: 404 });
}
}
import { buildAgentUrl } from "agents";
export class Chat extends Agent<Env> {
callbackUrl() {
return buildAgentUrl(this.env.PUBLIC_ORIGIN, this.selfPath, {
leafPath: "/callbacks/job",
});
}
override async onRequest(request: Request) {
if (new URL(request.url).pathname === "/callbacks/job") {
return this.handleJobCallback(request);
}
return new Response("Not found", { status: 404 });
}
}
Pass the incoming request to routeAgentRequest(). Each ancestor runs onBeforeSubAgent before the destination receives the request. For a sub-agent destination, routing removes the nested /sub/ segments, so its pathname is the leafPath suffix.
buildAgentUrl() accepts an HTTP(S) or WS(S) origin. The origin cannot contain credentials, a pathname, a query, or a fragment. Add callback query parameters through the returned URL searchParams property.
Root Agent names must already be valid pathname segments. The sub segment is reserved in routing prefixes, class and binding names, and root Agent names. The helper URL-encodes descendant names, including spaces, Unicode characters, /, and other URL-reserved characters.
Custom HTTP routing
For fetch handlers that do their own top-level URL parsing, use routeSubAgentRequest() to dispatch a request into a sub-agent from an already-resolved parent stub:
