# 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

```ts
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](#concurrency-and-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](#observing-children)) |
| `onSpawn`, `onComplete` | none | Callbacks when a child starts and when it ends |
| `persist`, `shareReadState`, `shareHandle` | `false` | [Boundaries](#parent-and-child-boundaries). The `basic` preset's `spawn` uses `persist: true`. |
| `subagents` | none | [Type registry](#subagent-type-registry) |
| `models` | none | [Per-spawn model choice](#model-selection-per-spawn) |

## 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](https://zidane.si/docs/apps.md#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](#resume-a-child) 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.

```ts
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`.

```md title=".zidane/agents/reviewer.md"
---
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](https://zidane.si/docs/extensions.md).

## 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](https://zidane.si/docs/apps.md#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](https://zidane.si/docs/hooks.md#sub-agent-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](https://zidane.si/docs/agents.md#usage-and-cost).

In the apps, [time estimates](https://zidane.si/docs/run-estimates.md) give each running sub-agent its own time left, in its header.

## Reference

- [`zidane/tools`](https://zidane.si/docs/reference/tools.md)
- [`zidane/agent`](https://zidane.si/docs/reference/agent.md)
