Cereal docs
Configure agents

Sub-agents

Handing a bounded piece of work to a cheaper, narrower version of the agent.

With the TypeScript SDK, pass an agent returned by defineAgent directly in subagents: [researcher]. See multiple agents for reusable references, shared routing, and deployment.

subagents is a roster on the definition, so it is versioned, diffable and auditable exactly as tools is. An agent with no roster cannot spawn sub-agents.

export default defineAgent({
  agent: {
    slug: "coding-agent",
    model: { provider: "gateway", model: "anthropic/claude-sonnet-4.6" },
    system: "You are a careful engineer.",

    subagents: [
      // Relational: the parent's setup, narrowed. Every omitted override means
      // *inherit*, never "default".
      {
        kind: "derived",
        name: "explore",
        description: "Read-only codebase search. Give it a question, not a plan.",
        extends: "parent",
        overrides: {
          model: { model: "anthropic/claude-haiku-4.6" },
          tools: { subset: ["read", "grep", "glob"] },
          system: { mode: "append", text: "You cannot write. Answer with findings." },
          limits: { maxTurns: 12 }
        }
      },
      // By reference: an agent that exists in its own right.
      { kind: "agent", name: "reviewer", description: "Second-opinion review.", agent: "code-reviewer" }
    ],

    subagentPolicy: { maxDepth: 2, maxConcurrent: 4, maxTotalPerSession: 16 }
  },
  tools: [/* … */]
})

Write a description that tells the model when to use the sub-agent and what result to expect.

tools.subset can only narrow. It is intersected with what the base can already reach, so explore's inability to write is a property of the resolver rather than of its prompt. A name the parent cannot reach is refused by sync before anything is sent.

extends: "parent" inherits the spawning session's effective setup, its own overrides already applied. So a session created with a per-tenant paragraph appended to its prompt spawns children that know which tenant they are working for.

The tools it gets

With a non-empty roster and below the depth limit, the agent gets five: sys_spawn_subagent, sys_message_subagent, sys_wait_subagent, sys_stop_subagent, sys_list_subagents. A sub-agent additionally gets sys_report, the channel back to whoever delegated.

A spawn returns immediately with a handle, two words such as brisk-otter, unique per parent, and the child works in the background. Its report arrives in the parent's log as a subagent.completed, waking a turn if the parent is idle and folding into the next step if it is not, so the parent keeps working rather than polling. Every tool that names a child takes the handle. A raw session id still resolves, and an unknown name is a tool error listing the handles that exist.

wait: true, and sys_wait_subagent over several handles, block for at most a bounded window, sixty seconds by default, and then return with the settled children in results and the rest in pending. The child's report still arrives when it finishes.

A child's status is reported in the MCP Tasks vocabulary, working, input_required, completed, failed and cancelled, with a statusMessage where the word does not explain itself. Cereal's own idle is not used here, because a model reads it as "not started".

A child is a real session

Its own transcript, its own trace, its own usage, its own id. You reach it the way you reach any other:

const children = await client.sessions.children({ id: sessionId })
for await (const event of client.sessions.stream(children[0].id)) { /* … */ }

The parent's log carries only the child's outputs: subagent.spawned, subagent.status, subagent.message and subagent.completed, never its working, so a busy child cannot fill its parent's context. Subscribe to those four in hooks.onSessionEvent for a "a sub-agent finished" webhook.

Children are created through the spawn tool. sessions.create does not accept a parentSessionId. You can send messages to, interrupt, and stream an existing child directly; those direct actions do not appear as parent-agent messages.

Caps

maxDepth: 2, maxConcurrent: 4, maxTotalPerSession: 16, selfDelegation off. The depth ceiling is the root session's, so a child cannot raise it by declaring a bigger one. Usage rolls up: a session's descendantUsage is everything its tree spent, and limits.maxInputTokensPerSession is checked against the total on the root, so a budget cannot be evaded by delegating.

A refused spawn is a tool error, not a failed turn: the model can wait for a slot, pick a different sub-agent, or answer without one.

Policies and machines are shared

A sub-agent inherits its parent's policies, so a child's tool call is gated by the same onToolCall hook, arriving with session.parentSessionId set, which is how you tell delegated work from the conversation the user is having. The sub-agent tools themselves are exempt: gating sys_spawn_subagent would ask a person to approve the question and then approve every answer.

If the agent has a sandbox, the whole tree shares one machine, same workspace and same processes, which is what makes a review-lens child possible at all. Narrow what a child may do to it with overrides.sandbox.tools.

On this page