Sub-agents
Delegate work to child agents with the spawn tool.
spawn hands a task to a child agent. By default the child inherits the tools, system prompt, aliases, MCP servers, skills and behavior of the parent. The events of the child bubble up to the hooks of the parent.
Spawn a child agent#
import { basicTools, createSpawnTool, definePreset } from 'zidane'
definePreset({
name: 'orchestrator',
tools: {
...basicTools,
spawn: createSpawnTool({ maxConcurrent: 5, model: 'claude-haiku-4-5-20251001', thinking: 'low' }),
},
})
createSpawnTool({ preset: { tools: {} } }) // preset replaces inherited fields: children get no tools| Option | Default | Effect |
|---|---|---|
maxConcurrent | 3 | Concurrent children, Infinity for no cap (queuing) |
maxDepth | 3 | Nesting limit. 0 turns spawning off, 1 forbids grandchildren. |
model | parent's | Child model |
system, thinking | unset | Child system prompt and thinking level. A per-call input.system wins. |
preset | none | Fields that replace the inherited ones |
timeoutMs | none | Per-child timeout. When it expires, zidane destroys the child and fires spawn:error. |
forwardHooks | true | Re-fire child events on the parent as child:* (observing children) |
onSpawn, onComplete | none | Callbacks when a child starts and when it ends |
persist, shareReadState, shareHandle | false | Boundaries. The basic preset's spawn uses persist: true. |
subagents | none | Type registry |
models | none | Per-spawn model choice |
Concurrency and queuing#
Past maxConcurrent, the spawn calls of a top-level agent wait for a slot in arrival order and fire spawn:queued. The calls of a sub-agent past the cap are refused, because a wait on a slot that an ancestor holds can deadlock.
One turn runs at most behavior.maxConcurrentTools tool calls at once (10 by default), spawns included. If you set maxConcurrent above 10, raise maxConcurrentTools too.
In the apps, the setting Max parallel subagents (maxParallelSubagents: 1, 2, 3, 5, 10 or Unlimited, default 3) sets the cap of each session, nested subagents included. It also raises maxConcurrentTools of the session to match. A change applies from the next run (settings).
A refused call (concurrency cap, maxDepth, unknown model, bad resume) returns a tool error to the model. The run of the parent continues.
Parent and child boundaries#
Flag (default false) | Shares |
|---|---|
persist | The Session of the parent, plus read state. Child turns append to session.turns under the runId of the child. Its SessionRun carries parentRunId, so you can rebuild the tree. A child resume needs it. |
shareReadState | Only the read-state map. Child read_file calls count for the read tracking of the parent, and child turns stay in memory. Use it when the requireReadBeforeEdit guard of the parent must honor child reads without storing child turns. |
shareHandle | The ExecutionHandle of the parent. The child skips execution.spawn() and never destroys the handle, so remote contexts avoid a new sandbox per spawn. |
The child always gets the AgentClock and AgentBehavior of the parent.
Resume a child#
The spawn input takes resume: a child label such as child-2, or a run id. The child continues with its stored turns, and task holds the next instruction. Resume needs persist: true and a parent session. The tool refuses a child that is still running or that has no stored turns.
Subagent type registry#
subagents adds a subagent_type enum to the spawn schema. Use the Claude Code SDK names (Explore, Plan, Verification, general-purpose) so that trained models pick them. The enum always includes general-purpose. An unregistered type runs as general-purpose, with no error.
createSpawnTool({
subagents: {
Explore: { description: 'read-only research', readonly: true },
Plan: { description: 'design changes', system: 'You are the planning subagent. Do not edit.' },
Verification: { description: 'run tests', tools: ['shell', 'read_file'] },
},
})| Field | Effect |
|---|---|
description | Shown to the model to pick a type |
system, model | Type system prompt and model. A per-call input.system or input.model wins. |
thinking | Type thinking level. Wins over createSpawnTool({ thinking }). |
tools | Allow-list of canonical names, a subset of the tools of the parent. It can list mcp_<server>_<tool> names. A list without MCP names gives the child no MCP tools. Wins over readonly. |
readonly: true | Shorthand for ['read_file', 'grep', 'glob', 'list_files']. The child gets no MCP tools. |
Subagents from Markdown files#
In the apps, each Markdown file in an agents/ folder is a subagent type. The first file for a name wins. zidane reads <project>/.agents/agents, <project>/.zidane/agents, ~/.agents/agents and ~/.zidane/agents. With the setting Import configurations from other agents on, it also reads .claude/agents.
---
name: reviewer
description: Reviews diffs for bugs
tools: Read, Grep, Bash(git:*)
model: sonnet
---
You review diffs. Report bugs with file:line.| Key | Effect |
|---|---|
name | Type name, case-sensitive. Defaults to the file name. |
description | Required. zidane skips a file without one. |
tools | Allowed tools, as Claude Code or zidane names. Without it, the type gets every tool of the parent. Bash(git:*) grants all of shell, because patterns do not restrict arguments. |
disallowedTools | Tools to remove. Without tools, the type keeps the native tools minus this list, and gets no MCP or extension tools. |
readonly | true limits the agent to read tools |
permissionMode | plan is the same as readonly: true. zidane ignores other values. |
model | Model for this type |
The body is the system prompt of the type. zidane skips a file with an empty body. A file with an unreadable tools, disallowedTools or readonly value is skipped, so a typo cannot widen the tool set. A file with name: Explore in your user folder replaces the built-in Explore. The agent profiles in the picker of the apps (Build, Plan) are separate. They are built in or come from extensions.
Model selection per spawn#
models adds a model field to the spawn schema, so the calling model picks the child model. Each entry routes to the provider that serves it. The tool refuses an unknown id and lists the closest matches. Up to 8 entries, the field description lists the models. Above 8, the field points to a separate model_search tool, which you register with createModelSearchTool.
Precedence: input.model, the type's model, createSpawnTool({ model }), then the parent's model.
In the apps, the setting Allow subagents to use different models (subagentModels, on by default) adds the model field and model_search when your signed-in providers have a model catalog. When it is off, children stay on the model of the parent (settings).
Observing children#
spawn:queued, spawn:before, spawn:complete and spawn:error fire on the parent. With forwardHooks on, child events re-fire as child:* with childId and depth (bubble events). A child:spawn:* event at depth 2 or more also carries parentChildId, the sub-agent that called spawn.
child:tool:gate, child:mcp:tool:gate and child:tool:transform share the context that the loop of the child awaits. A parent handler can block, substitute or rewrite a tool call of the child. Usage totals across parent and children are on agents.
In the apps, time estimates give each running sub-agent its own time left, in its header.