Sub-agents·续

import { getAgentByName, routeSubAgentRequest } from "agents";

export default {
    async fetch(request, env) {
        const url = new URL(request.url);
        const match = url.pathname.match(/^\/api\/u\/([^/]+)(\/.*)$/);
        if (!match) return new Response("Not found", { status: 404 });

const [, userId, rest] = match;
        const parent = await getAgentByName(env.Inbox, userId);
        return routeSubAgentRequest(request, parent, { fromPath: rest });
    },
};
import { getAgentByName, routeSubAgentRequest } from "agents";

export default {
    async fetch(request: Request, env: Env) {
        const url = new URL(request.url);
        const match = url.pathname.match(/^\/api\/u\/([^/]+)(\/.*)$/);
        if (!match) return new Response("Not found", { status: 404 });

const [, userId, rest] = match;
        const parent = await getAgentByName(env.Inbox, userId);
        return routeSubAgentRequest(request, parent, { fromPath: rest });
    },
};

fromPath takes any pathname that contains a sub-agent tail, such as /sub/chat/chat-abc. You can pass the result of buildAgentPath() directly. The helper parses it, runs the parent onBeforeSubAgent hook, and forwards the request into the facet.

External typed RPC

From inside the parent Durable Object, this.subAgent(Cls, name) returns a typed stub. From outside the parent, use getSubAgentByName():

import { getAgentByName, getSubAgentByName } from "agents";

const inbox = await getAgentByName(env.Inbox, userId);
const chat = await getSubAgentByName(inbox, Chat, chatId);

await chat.addMessage({ role: "user", content: "hello" });
import { getAgentByName, getSubAgentByName } from "agents";

const inbox = await getAgentByName(env.Inbox, userId);
const chat = await getSubAgentByName(inbox, Chat, chatId);

await chat.addMessage({ role: "user", content: "hello" });

getSubAgentByName() returns an RPC-only proxy. Method calls work, but .fetch() throws. Use routeSubAgentRequest() for HTTP and WebSocket forwarding.

Storage isolation

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

Naming and identity

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 storage
const counter = await this.subAgent(Counter, "shared-name");
const logger = await this.subAgent(Logger, "shared-name");
// These are two separate sub-agents with separate storage

The 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
    }
}

Patterns

Parallel sub-agents

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

Nested sub-agents

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

Callback streaming

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

Scheduling and durable work

Sub-agents can schedule their own callbacks and run durable fibers:

Method Behavior in sub-agent
schedule() / scheduleEvery() Work normally and run callbacks inside the sub-agent
cancelSchedule() Works for schedules owned by the calling sub-agent
getScheduleById() / listSchedules() Work and return schedules scoped to the calling sub-agent
keepAlive() / keepAliveWhile() Work by delegating the heartbeat to the top-level parent
runFiber() Works, with fiber rows and snapshots stored in the child's SQLite database
setState() Works normally and writes to the child's own storage
this.sql Works normally and points at the child's own SQLite database
subAgent() Works, so sub-agents can spawn their own children

The top-level parent still owns the physical Durable Object alarm because facets do not have independent alarm slots. The Agents SDK records which child owns each scheduled callback or recovery check, wakes the parent, and routes the work back into the child. The callback still runs with the sub-agent as this, so it uses the child's state, SQLite storage, and getCurrentAgent() context.

The older synchronous getSchedule() and getSchedules() APIs throw inside sub-agents because scheduled rows are stored on the top-level parent. Use getScheduleById() and listSchedules() instead.

Calling this.destroy() inside a sub-agent delegates cleanup to the parent. The parent cancels that sub-agent's schedules, removes recovery metadata for the sub-agent and its descendants, removes the registry entry, and asks the runtime to wipe the child storage. Treat this.destroy() as fire-and-forget because deleting the sub-agent can abort its isolate before the method returns cleanly.

Workflows from sub-agents

Sub-agents can also start Workflows with this.runWorkflow(). Workflow tracking is local to the sub-agent's SQLite database, and AgentWorkflow.agent routes RPC, callbacks, state updates, and broadcasts back to the originating sub-agent. Parent agents do not automatically list or control child-started workflows.

Because SubAgentStub<T> only exposes user-defined child methods, add child wrapper methods for controls such as getWorkflow(), approveWorkflow(), or terminateWorkflow(), then call those wrappers through await this.subAgent(Child, name). If you pass runWorkflow(..., { agentBinding }) from a sub-agent, use the root Agent binding name, not a child binding name.

For sub-agent workflow origins, AgentWorkflow.agent is RPC-only. Use it to call Agent methods, but use routeSubAgentRequest() or the nested /agents/{parent}/{name}/sub/{child}/{name} URL shape for external HTTP or WebSocket routing instead of this.agent.fetch().

Example

Multi-session chat example

Build an inbox where each chat is an AIChatAgent sub-agent with isolated state and direct client routing.

  • 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 @callable and service bindings
  • Agents as tools — run Think or AIChatAgent sub-agents as retained, streaming tools
  • Schedule tasks — scheduling primitives for top-level agents and sub-agents

Was this helpful?

YesNo

On this page

Sub-agents·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/execution/sub-agents/#page","headline":"Sub-agents","description":"Spawn child agents with isolated storage and typed RPC using subAgent(), abortSubAgent(), and deleteSubAgent().","url":"https://developers.cloudflare.com/agents/runtime/execution/sub-agents/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/runtime/execution/sub-agents/og.png?v=1d540fe8d5fdfbbf","dateModified":"2026-09-15","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}


description: Explore how the Agent class extends Durable Objects to provide state, WebSockets, scheduling, and RPC. title: Agent class internals image: https://developers.cloudflare.com/agents/runtime/lifecycle/agent-class/og.png?v=a31f520b4b74a2c7


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.