Sub-agents
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.
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 — child classes are discovered automatically via ctx.exports.
{ "$schema": "./node_modules/wrangler/config-schema.json", // Set this to today's date "compatibility_date": "2026-04-16", "compatibility_flags": [ "nodejs_compat", "experimental" ], "durable_objects": { "bindings": [ { "class_name": "Orchestrator", "name": "Orchestrator" } ] }, "migrations": [ { "new_sqlite_classes": [ "Orchestrator" ], "tag": "v1" } ]}# Set this to today's datecompatibility_date = "2026-04-16"compatibility_flags = ["nodejs_compat", "experimental"]
[[durable_objects.bindings]]class_name = "Orchestrator"name = "Orchestrator"
[[migrations]]new_sqlite_classes = ["Orchestrator"]tag = "v1"Only the 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.
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.
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)- 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
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.
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.
Each sub-agent has its own SQLite database, completely isolated from the parent and from other sub-agents. A parent writing to this.sql and a child writing to this.sql operate on different databases:
export class Parent extends Agent { async demonstrate() { this.sql`INSERT INTO parent_data (key, value) VALUES ('color', 'blue')`;
const child = await this.subAgent(Child, "child-1"); await child.increment("clicks");
// Parent's SQL and child's SQL are completely separate }}
export class Child extends Agent { async increment(key) { this .sql`CREATE TABLE IF NOT EXISTS counters (key TEXT PRIMARY KEY, value INTEGER DEFAULT 0)`; this .sql`INSERT INTO counters (key, value) VALUES (${key}, 1) ON CONFLICT(key) DO UPDATE SET value = value + 1`; const row = this.sql`SELECT value FROM counters WHERE key = ${key}`.one(); return row?.value ?? 0; }}export class Parent extends Agent { async demonstrate() { this.sql`INSERT INTO parent_data (key, value) VALUES ('color', 'blue')`;
const child = await this.subAgent(Child, "child-1"); await child.increment("clicks");
// Parent's SQL and child's SQL are completely separate }}
export class Child extends Agent { async increment(key: string): Promise<number> { this .sql`CREATE TABLE IF NOT EXISTS counters (key TEXT PRIMARY KEY, value INTEGER DEFAULT 0)`; this .sql`INSERT INTO counters (key, value) VALUES (${key}, 1) ON CONFLICT(key) DO UPDATE SET value = value + 1`; const row = this.sql<{ value: number; }>`SELECT value FROM counters WHERE key = ${key}`.one(); return row?.value ?? 0; }}Two different classes can share the same user-facing name — they are resolved independently. The internal key is a composite of class name and facet name:
const counter = await this.subAgent(Counter, "shared-name");const logger = await this.subAgent(Logger, "shared-name");// These are two separate sub-agents with separate storageconst counter = await this.subAgent(Counter, "shared-name");const logger = await this.subAgent(Logger, "shared-name");// These are two separate sub-agents with separate storageThe child's this.name property returns the facet name (not the parent's name):
export class Child extends Agent { getName() { return this.name; // Returns "shared-name", not the parent's ID }}export class Child extends Agent { getName(): string { return this.name; // Returns "shared-name", not the parent's ID }}Run multiple sub-agents concurrently:
export class Orchestrator extends Agent { async runAll(queries) { const results = await Promise.all( queries.map(async (query, i) => { const worker = await this.subAgent(Researcher, `research-${i}`); return worker.search(query); }), ); return results; }}export class Orchestrator extends Agent { async runAll(queries: string[]) { const results = await Promise.all( queries.map(async (query, i) => { const worker = await this.subAgent(Researcher, `research-${i}`); return worker.search(query); }), ); return results; }}Sub-agents can spawn their own sub-agents, forming a tree:
export class Manager extends Agent { async delegate(task) { const team = await this.subAgent(TeamLead, "team-a"); return team.assign(task); }}
export class TeamLead extends Agent { async assign(task) { const worker = await this.subAgent(Worker, "worker-1"); return worker.execute(task); }}
export class Worker extends Agent { async execute(task) { return { completed: task }; }}export class Manager extends Agent { async delegate(task: string) { const team = await this.subAgent(TeamLead, "team-a"); return team.assign(task); }}
export class TeamLead extends Agent { async assign(task: string) { const worker = await this.subAgent(Worker, "worker-1"); return worker.execute(task); }}
export class Worker extends Agent { async execute(task: string) { return { completed: task }; }}Pass an RpcTarget callback to stream results from a sub-agent back to the parent:
import { RpcTarget } from "cloudflare:workers";
class StreamCollector extends RpcTarget { chunks = []; onChunk(text) { this.chunks.push(text); }}
export class Parent extends Agent { async streamFromChild() { const child = await this.subAgent(Streamer, "streamer-1"); const collector = new StreamCollector(); await child.generate("Write a poem", collector); return collector.chunks; }}
export class Streamer extends Agent { async generate(prompt, callback) { const chunks = ["Once ", "upon ", "a ", "time..."]; for (const chunk of chunks) { callback.onChunk(chunk); } }}import { RpcTarget } from "cloudflare:workers";
class StreamCollector extends RpcTarget { chunks: string[] = []; onChunk(text: string) { this.chunks.push(text); }}
export class Parent extends Agent { async streamFromChild() { const child = await this.subAgent(Streamer, "streamer-1"); const collector = new StreamCollector(); await child.generate("Write a poem", collector); return collector.chunks; }}
export class Streamer extends Agent { async generate(prompt: string, callback: StreamCollector) { const chunks = ["Once ", "upon ", "a ", "time..."]; for (const chunk of chunks) { callback.onChunk(chunk); } }}Sub-agents run as facets of the parent Durable Object. Some Agent methods are not available in sub-agents:
| Method | Behavior in sub-agent |
|---|---|
schedule() | Throws "not supported in sub-agents" |
cancelSchedule() | Throws "not supported in sub-agents" |
keepAlive() | Throws "not supported in sub-agents" |
setState() | Works normally (writes to the child's own storage) |
this.sql | Works normally (child's own SQLite) |
subAgent() | Works — sub-agents can spawn their own children |
For scheduled work, have the parent schedule the task and delegate to the sub-agent when the schedule fires.
- Think —
chat()method for streaming AI turns through sub-agents - Long-running agents — sub-agent delegation in the context of multi-week agent lifetimes
- Callable methods — RPC via
@callableand service bindings - Chat agents —
ToolLoopAgentfor in-process AI SDK sub-calls