Single-agent loops are fine for short conversations. Once an agent has to research, plan, draft, and review, you want specialisation: separate agents for separate jobs, each with its own context window so deep work doesn’t pollute the parent’s.
agentlib ships three dispatch modes that cover the common patterns.
The three modes
Sync. “Do this one thing and tell me the result.” Parent blocks until the subagent finishes.
final result = await SubagentRunner.runOne(
parent: ctx,
def: researcherDef,
prompt: 'Research the latest on Vision Pro UX patterns.',
);
Parallel. “Do these N things at once; come back when all are done.” Each subagent runs in its own context, the parent gathers results once they’re all in.
final results = await SubagentRunner.runParallel(
parent: ctx,
defs: [researcherDef, drafterDef],
prompts: ['research X', 'draft article about X'],
);
Background. “Start this; I’ll come back later.” Useful for indexing, long-running ingestion, or any work that shouldn’t block the user’s current interaction.
final handle = SubagentRunner.dispatch(
parent: ctx,
def: indexerDef,
prompt: 'Index the user\'s notes folder.',
);
// ... user does other things ...
final result = await handle.await();
// or: handle.onComplete((result) { ... });
Why separate context windows
Two reasons. First, cost: subagent context doesn’t multiply into the parent. A research subagent can read 200 documents and the parent never sees them — only the subagent’s final structured summary. Second, clarity: the parent’s prompt stays focused on its job (“orchestrate the writing”), and each subagent’s prompt stays focused on its own.
Defining a subagent
A subagent is described by an AgentDefinition:
final researcherDef = AgentDefinition(
name: 'researcher',
description: 'Researches a topic deeply from the workspace.',
instructions: '''
You are a research assistant. Given a topic, search the workspace for relevant
notes, read the top 5, and emit a JSON summary with key points and citations.
''',
tools: ['fs.read', 'fs.grep', 'fs.glob'], // allow-list — researcher can only read
modelOverride: AnthropicProvider(apiKey: '<KEY>'), // use a stronger model for research
);
Key fields:
toolsis an allow-list. Researcher gets fs read access; not write.modelOverridelets you pick a different (often cheaper or stronger) model.instructionsis the subagent’s system prompt. Independent from the parent’s.permissionModecan beask,allow, ordeny— fine-grained capability gating.
Dispatch from a model turn
Most subagent calls happen via the Task tool that the orchestrator agent has access to:
Sh("task dispatch researcher --prompt 'Vision Pro UX best practices' --mode parallel")
The model emits this; agentlib looks up the researcher definition; spawns it; waits (or returns immediately if
mode is background); injects the result back as a ToolResult.
A research-and-draft example
final orchestrator = AgentSpec(
name: 'writer',
instructions: '''
You orchestrate writing. When a draft is requested:
1. Dispatch `researcher` in parallel with `outline`.
2. Wait for both to finish.
3. Dispatch `drafter` with the outline + research as input.
4. Return the draft.
''',
model: AnthropicProvider(apiKey: '<KEY>'),
subagents: [researcherDef, outlineDef, drafterDef],
);
The orchestrator’s system prompt explains the workflow. The subagent definitions live alongside.
Background work
Background dispatch is useful when:
- Indexing: walk the user’s documents, build an embedding index. Don’t block the chat.
- Periodic refresh: every hour, summarise new emails. The summary lives in
/memory; the chat agent reads from it instantly. - Speculative work: while the user is typing the next prompt, prefetch a likely response.
Background subagents survive parent suspension. If the app gets backgrounded while a subagent is running, the
subagent’s state is checkpointed; on OnResume, agentlib resumes it. If the OS kills the process entirely, the
subagent picks up from its last snapshot.
Hooks for subagents
Three hooks fire around subagents:
SubagentStart— before dispatch. Log it, deny it, mutate the prompt.SubagentMessage— each message the subagent emits. Useful for live progress UI in the parent.SubagentStop— when the subagent finishes. Log the duration; check for errors.
hooks.register(SubagentStart((event, ctx) async {
await analytics.log('subagent.start', { 'def': event.def.name });
return HookOutcome.carryOn();
}));
When subagents are wrong
Subagents are heavyweight. Each one is a full agent loop. If a piece of work is procedural (do this then this then this), a skill is lighter. If it’s a one-shot tool call, just call the tool directly.
Reach for a subagent when:
- The work needs its own model (cheaper, smaller, specialised).
- The work needs its own context (deep reading, lots of intermediate state).
- The work needs to run in the background.
- The work is parallel with other work the orchestrator is doing.
Read next
- Suspend, resume, push-resume — how background subagents survive the OS.
- Snapshots — checkpoint your orchestrator before a risky parallel dispatch.