Subagents
Subagents let a BeamWeaver agent delegate isolated work through the task tool. A subagent receives a fresh message context and runs as a normal BeamWeaver agent. It returns one final result to the parent; for structured specialists it can also capture the full output in graph state and return only a compact acknowledgement. This is useful when a task would otherwise fill the parent context with intermediate searches, file reads, tool outputs, or large specialist payloads.
Use subagents for multi-step work, specialized tool sets, or tasks that should use a different model or prompt. Do not use them for simple one-step lookups, or when the parent must inspect every intermediate message as part of the active conversation.
Composition Model
BeamWeaver has one agent abstraction. There is no separate DeepAgent type and no create_deep_agent constructor. An agent becomes "deep" only by composing the capabilities it needs:
-
TODO planning through
BeamWeaver.Agent.Middleware.TodoListorBeamWeaver.Tools.Todo. -
Virtual files through
filesystem,BeamWeaver.Agent.Middleware.Filesystem, orBeamWeaver.Tools.Filesystem. -
Delegation through
subagentsorBeamWeaver.Agent.Middleware.Subagents. -
Memory, compaction, overflow recovery, checkpointing, skills, and HITL through their normal agent fields or middleware entries.
Capabilities are positive declarations. If you do not include TODO middleware, there is no TODO behavior. If you do not include filesystem middleware or a filesystem agent field, there are no filesystem tools. If you do not configure subagents, there is no task tool.
Enable The Task Tool
Declare synchronous subagents in a subagents do block. BeamWeaver adds a model-visible task tool only when at least one synchronous subagent is configured.
defmodule MyApp.Agents.Researcher do
use BeamWeaver.Agent
name "researcher"
description "Conduct multi-step web research and return concise sourced notes."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.Tools.WebSearch
end
system_prompt """
You are a focused research assistant.
Use web_search when useful.
Return only essential findings and source URLs.
Keep the response under 500 words.
"""
end
defmodule MyApp.Agents.MainAgent do
use BeamWeaver.Agent
name "main_agent"
description "Coordinate answers and delegate deep research."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.Tools.WebSearch
end
subagents do
subagent MyApp.Agents.Researcher
end
system_prompt """
Coordinate the answer. Delegate deep research to the researcher when a
question needs multiple searches or source synthesis.
"""
end
{:ok, state} =
MyApp.Agents.MainAgent.invoke(%{
messages: [
BeamWeaver.Core.Message.user("Research recent changes in vector database pricing.")
]
})
The task tool accepts:
| Argument | Meaning |
|---|---|
description
| The full assignment for the subagent. Include all context the child needs. |
subagent_name or subagent_type
|
The configured subagent name, such as "researcher".
|
The child starts with description as its user message. The parent receives one tool result containing either the child's structured response encoded as JSON, or the last assistant message from the child.
Parent messages are not inherited by default. Set inherit_messages: true only when the child really needs the parent transcript; BeamWeaver filters parent tool protocol messages before passing inherited messages to the child.
Application-Owned Child Lifecycles
The default synchronous middleware runs an ephemeral child inside BeamWeaver. Applications that own durable child admission and recovery instead configure a BeamWeaver.Agent.Subagent.Host adapter:
middleware =
BeamWeaver.Agent.Middleware.Subagents.new(
host: %MyApp.ChildHost{},
child_mode: :background,
subagents: [
%BeamWeaver.Agent.Subagent.Spec{
name: "researcher",
description: "Collect source-backed facts"
}
]
)
Host mode passes one closed proposal to admit_child/3, then asks child_result/3 for either a typed terminal projection or the identical pending background handle. It does not compile or invoke a nested agent, copy parent state, install child-output/cache channels, or infer success from a missing result. Background hosts cannot return :needs_parent_input.
BeamWeaver deliberately does not define the host's database rows, permissions, budgets, leases, or recovery policy. Those remain application responsibilities.
Parent And Child Ownership
The parent owns delegation policy. The child owns its prompt, model, tools, middleware, and structured output:
defmodule MyApp.Agents.NarrativeCompressor do
use BeamWeaver.Agent
name "narrative_compressor"
description "Build the authoritative relationship narrative and timeline."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.Tools.RelatedDeals
end
middleware do
use BeamWeaver.Agent.Middleware.ToolCallNormalization
use BeamWeaver.Agent.Middleware.StructuredOutputRetry, max_retries: 2
end
system_prompt "Return only the requested structured narrative output."
response_schema MyApp.Schemas.NarrativeOutput, name: "narrative_output"
end
defmodule MyApp.Agents.SummarySupervisor do
use BeamWeaver.Agent
name "summary_supervisor"
description "Coordinate deal summary specialists."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
subagents do
subagent MyApp.Agents.NarrativeCompressor, capture_output: :narrative_output
end
system_prompt "Delegate narrative work to the narrative_compressor specialist."
end
Subagent Specs
Module-based subagents are the preferred public DSL. Use BeamWeaver.Agent.Subagent.Spec.new/1 only when the subagent list is generated at runtime from configuration. It accepts keyword lists and atom-keyed maps.
| Field | Type | Behavior |
|---|---|---|
:name
| string |
Required. Unique subagent name selected by the parent through task.
|
:description
| string | Required. Model-facing description used to choose the right subagent. |
:system_prompt
| string |
Recommended. Prepended to BeamWeaver's focused-subagent prompt. BeamWeaver accepts nil, but useful subagents should define their role and output shape.
|
:tools
| list |
Optional. Inherits parent tools when nil; use [] to give the subagent no parent tools. Any list replaces the inherited list.
|
:model
| model | Optional. Inherits the parent model when omitted. |
:middleware
| list | Optional. Subagent-specific middleware. Parent middleware is not inherited. |
:interrupt_on
| map or boolean |
Optional. Inherits parent HITL configuration when omitted. A child map overrides the parent policy; current generated subagents treat false like inheritance, so use a narrower map, remove tools, or use a compiled subagent to opt out. Requires checkpointing to resume.
|
:skills
| list |
Optional. Inherits parent skills when nil; use [] to opt out, or provide a list to replace the inherited skills.
|
:permissions
| list | Optional. Inherits parent filesystem permissions when omitted; a list replaces the inherited rules. |
:response_format
| structured output config |
Optional. Dynamic equivalent of response_schema/2 on a child module.
|
:capture_output
| atom or keyword |
Optional. Stores the child output in state.subagent_outputs[key] and returns a compact acknowledgement to the parent. Public Elixir config uses atom keys.
|
:execution_mode
| atom |
Optional. :agent_loop by default; use :structured_once or :research_then_generate for specialist patterns.
|
Dynamic generated child agents use the same capability rules as module agents: tools and middleware are present only when you declare them. They do not automatically receive TODO, filesystem, skills, summarization, or compaction middleware.
alias BeamWeaver.Agent.Subagent
Subagent.Spec.new(
name: "file_researcher",
description: "Researches files and returns concise findings.",
system_prompt: "Use files only when needed and keep the answer short.",
tools: [MyApp.Tools.SearchDocs],
middleware: [
{BeamWeaver.Agent.Middleware.TodoList, tool_name: "write_todos"},
BeamWeaver.Agent.Middleware.Filesystem
]
)
For path rule examples and the distinction between inherited, replaced, and unrestricted subagent permissions, see Filesystem Permissions .
Compiled Subagents
For complex workflows, provide a prebuilt BeamWeaver agent with BeamWeaver.Agent.Subagent.Compiled. The :agent must be a module-defined agent or a %BeamWeaver.Agent.Built{} returned by BeamWeaver.Agent.build/1.
alias BeamWeaver.Agent
alias BeamWeaver.Agent.Subagent
{:ok, analyzer} =
Agent.build(
name: "data-analyzer",
model: BeamWeaver.Models.init_chat_model!("openai:gpt-5.4"),
tools: [MyApp.Tools.QueryWarehouse],
system_prompt: "Analyze tabular data and return concise findings."
)
{:ok, supervisor} =
Agent.build(
name: "analysis-supervisor",
model: BeamWeaver.Models.init_chat_model!("openai:gpt-5.4"),
subagents: [
%Subagent.Compiled{
name: "data-analyzer",
description: "Analyzes warehouse data and reports trends.",
agent: analyzer
}
]
)
If you have a compiled graph rather than an agent, use
Subgraphs
or wrap the behavior in a BeamWeaver agent module. Compiled is intentionally an agent delegation API, not an arbitrary graph runnable slot.
General-Purpose Subagents
Official Python Deep Agents automatically adds a synchronous general-purpose subagent by default. BeamWeaver's normal BeamWeaver.Agent.build/1 and use BeamWeaver.Agent flow does not auto-inject that subagent. If you want a general-purpose delegate, declare it explicitly:
defmodule MyApp.Agents.GeneralPurpose do
use BeamWeaver.Agent
name "general-purpose"
description "Handles isolated multi-step work when no specialist fits."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
system_prompt """
You are a general-purpose delegated assistant.
Complete the task independently and return only the final answer.
"""
end
defmodule MyApp.Agents.Supervisor do
use BeamWeaver.Agent
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
subagents do
subagent MyApp.Agents.GeneralPurpose
end
end
To run without the task tool, omit subagents, pass subagents: [], or pass subagents: false. Async subagents use a separate tool surface and do not create the synchronous task tool by themselves.
Structured Output
Subagents can use the same structured-output strategies as top-level agents. When a child agent writes :structured_response, the task tool returns that response as JSON to the parent.
defmodule MyApp.Schemas.ResearchFindings do
use BeamWeaver.Schema
title "research_findings"
description "Concise sourced research findings."
strict true
field :summary, :string, required: true
field :confidence, :number, required: true
field :sources, {:array, :string}, required: true
end
defmodule MyApp.Agents.Researcher do
use BeamWeaver.Agent
name "researcher"
description "Researches a topic and returns structured findings."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.Tools.WebSearch
end
system_prompt "Return a concise structured answer with source URLs."
response_schema MyApp.Schemas.ResearchFindings, name: "research_findings", strategy: :tool
end
Without response_format, the parent receives the last assistant message text from the child.
Captured Specialist Outputs
Use capture_output when the parent should coordinate specialists without re-emitting their full structured payloads through the supervisor model. The child output is written to state.subagent_outputs[key], while the task tool message contains a short JSON acknowledgement.
defmodule MyApp.Schemas.NarrativeOutput do
use BeamWeaver.Schema
title "narrative_output"
strict true
field :summary, :string, required: true
field :timeline, {:array, :object}, required: true
end
defmodule MyApp.Agents.NarrativeCompressor do
use BeamWeaver.Agent
name "narrative_compressor"
description "Builds the authoritative relationship narrative and timeline."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
system_prompt "Return only the requested structured narrative output."
response_schema MyApp.Schemas.NarrativeOutput,
name: "narrative_output",
strategy: :provider
end
defmodule MyApp.Agents.SummarySupervisor do
use BeamWeaver.Agent
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
subagents do
subagent MyApp.Agents.NarrativeCompressor, capture_output: :narrative_output
end
end
After the parent calls task, the graph state includes:
state.subagent_outputs["narrative_output"]
# %{"summary" => "...", "timeline" => [...]}
The parent model sees only an acknowledgement similar to:
{
"status": "captured",
"subagent_name": "narrative_compressor",
"capture_key": "narrative_output",
"cache_hit": false,
"input_hash": "..."
}
Captured values are normalized to JSON-safe maps, lists, strings, numbers, booleans, or nil, and are persisted through checkpoints. Captured state is not inherited by child subagents, so specialist payloads do not recursively inflate child context.
Captured subagents are visible in native traces as child agent runs and compact parent tool messages. The parent-visible tool output is the same compact acknowledgement the parent model saw; the captured state.subagent_outputs payload stays internal and is not uploaded as tool message content.
Repeated captured calls are deduped by {subagent_name, input_hash} by default. BeamWeaver stores cache entries in state.subagent_cache; repeated calls with the same subagent and same task input return from cache without rerunning the child.
Override this only when needed:
subagents do
subagent MyApp.Agents.LiveMarketResearcher,
capture_output: [key: :market_snapshot, dedupe: false]
subagent MyApp.Agents.SmallClassifier,
capture_output: [key: :classification, parent_result: :full]
end
Use parent_result: :full only for small outputs that the supervisor genuinely needs to read directly. The default acknowledgement keeps the supervisor context small.
Named Specialist Tools
The generic task tool is convenient, but some applications want named application tool traces such as run_narrative_compressor or run_fact_extractor. In that case, expose normal application tools and have those tools run the specialist agent themselves. Add BeamWeaver.Agent.Middleware.SubagentOutputs to the supervisor so captured payloads have mergeable graph channels:
defmodule MyApp.SummaryAgent do
use BeamWeaver.Agent
alias BeamWeaver.Agent.Middleware
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.Tools.RunNarrativeCompressor
tool MyApp.Tools.RunFactExtractor
end
middleware do
use Middleware.TodoList, tool_name: "write_todos"
use Middleware.SubagentOutputs
end
end
The named tool should return a BeamWeaver.Graph.Command that merges the full JSON-safe output into :subagent_outputs and returns only a compact tool message to the parent model. This gives traces and logs meaningful tool names while keeping large specialist JSON out of the supervisor transcript.
Execution Modes
execution_mode controls how the child agent runs:
| Mode | Behavior |
|---|---|
:agent_loop
| Default behavior. The child can use tools and loop until it produces a final answer. |
:structured_once
| Runs one model call and enforces a one-call limit. Use this for specialists that should directly produce structured output without autonomous filesystem or TODO loops. |
:research_then_generate
| Runs a tool-enabled research pass, then a separate tool-free structured generation pass. Use this for tool-enabled structured specialists. |
Lean specialists need no negative flags. If the child module does not declare TODO, filesystem, or subagents, those capabilities do not exist:
defmodule MyApp.Agents.StageRecommender do
use BeamWeaver.Agent
name "stage_recommender"
description "Recommends the correct pipeline stage."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
system_prompt "Return the best pipeline stage."
response_schema MyApp.Schemas.StageRecommendation,
name: "stage_recommendation",
strategy: :provider
end
If a specialist needs tools before producing structured output, prefer :research_then_generate instead of combining provider-native structured output with active tools:
defmodule MyApp.Agents.DealFactExtractor do
use BeamWeaver.Agent
name "deal_fact_extractor"
description "Researches related records, then emits structured facts."
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
tools do
tool MyApp.Tools.RelatedDeals
end
middleware do
use BeamWeaver.Agent.Middleware.Filesystem
end
system_prompt "Research first, then return structured facts."
response_schema MyApp.Schemas.FactsOutput, name: "facts_output", strategy: :provider
end
The research pass can use the configured tools and middleware. The generation pass receives the original task plus concise research notes, does not inherit the parent transcript, and has no tools. This keeps structured-output parsing bounded and avoids provider tool/JSON interaction problems.
Runtime Context
Runtime context passed to the parent invocation propagates to synchronous subagents. Tools inside the child can read it with explicit injection:
alias BeamWeaver.Core.Tool
user_lookup =
Tool.from_function!(
name: "user_lookup",
description: "Look up data for the current user.",
input_schema: %{
"type" => "object",
"properties" => %{
"query" => %{"type" => "string"},
"context" => %{"type" => "object"}
},
"required" => ["query", "context"]
},
injected: [context: :context],
handler: fn input, _opts ->
context = input[:context] || input["context"] || %{}
user_id = context[:user_id] || context["user_id"]
query = input[:query] || input["query"]
"Data for #{user_id}: #{query}"
end
)
For subagent-specific configuration, either namespace fields in the context map or model them as separate keys:
Agent.invoke(agent, input,
context: %{
user_id: "user-123",
researcher_max_depth: 3,
fact_checker_strict_mode: true
}
)
When a shared tool needs to know which child called it, inject :tool_runtime and inspect runtime config:
Tool.from_function!(
name: "shared_lookup",
description: "Look up information with agent-specific behavior.",
input_schema: %{
"type" => "object",
"properties" => %{
"query" => %{"type" => "string"},
"tool_runtime" => %{"type" => "object"}
},
"required" => ["query", "tool_runtime"]
},
injected: [tool_runtime: :tool_runtime],
handler: fn input, _opts ->
runtime = input[:tool_runtime] || input["tool_runtime"]
config = runtime.config || %{}
name =
get_in(config, [:configurable, :subagent_name]) ||
get_in(config, ["configurable", "subagent_name"])
case name do
"fact-checker" -> MyApp.StrictLookup.search(input["query"] || input[:query])
_other -> MyApp.GeneralLookup.search(input["query"] || input[:query])
end
end
)
The parent-side tool message also carries metadata including :subagent_name and :subagent_type.
Streaming
Task subagents are launched from tool calls, so they do not appear as static subgraph nodes in graph topology. For UI or telemetry projections, collect or consume agent events and use BeamWeaver.Agent.Subagent.StreamTransformer:
alias BeamWeaver.Agent
alias BeamWeaver.Agent.Subagent.StreamTransformer
{:ok, events} = Agent.stream_events(agent, input)
transformer = StreamTransformer.new(subagent_names: ["researcher"])
{:ok, transformer, _new_handles} = StreamTransformer.process_many(transformer, events)
transformer = StreamTransformer.finalize(transformer)
for handle <- transformer.log do
IO.inspect(
%{
name: handle.graph_name,
status: handle.status,
task_input: handle.task_input,
output: BeamWeaver.Agent.Subagent.RunStream.output(handle)
},
label: "subagent"
)
end
For graph subgraphs and agents compiled as graph nodes, use BeamWeaver.Stream.Subgraphs instead.
For message, tool-call, lifecycle, nested subagent, and exact-arrival-order patterns, see Event Streaming .
Async Subagents
Use async_subagents or BeamWeaver.Agent.Subagent.AsyncSpec entries when the work should run on a remote Agent Protocol server and the parent should not block until completion.
async_subagents do
async_subagent "remote_research",
description: "Long-running background research worker.",
graph_id: "research_graph",
url: "https://agents.example.com"
end
Async subagents expose start_async_task, check_async_task, update_async_task, cancel_async_task, and list_async_tasks. They are separate from the synchronous task tool. See
Async Subagents
for transport, lifecycle, state, and custom client details.
Best Practices
-
Write descriptions that clearly tell the parent when to delegate.
-
Keep each subagent's tool set small. This improves focus and narrows the permission surface.
-
Put output limits in the subagent prompt. Ask for summaries, not raw tool dumps.
-
Use structured output when the parent needs to parse the result or pass it to another tool.
-
Use
capture_outputfor large structured specialist payloads that should be authoritative in state but not re-serialized through the supervisor model. -
Prefer
:research_then_generatefor specialists that need tools and must end with structured output. -
Have subagents write large artifacts to the filesystem and return file paths plus summaries.
-
Use subgraphs rather than task subagents when you need static graph introspection, state inspection, or graph-level persistence semantics.
Troubleshooting
| Problem | Fix |
|---|---|
| The parent does the work itself. |
Make the subagent description more specific and tell the parent prompt when to delegate.
|
The task tool is missing.
|
Configure at least one synchronous subagent. Async-only configuration exposes async tools, not task.
|
| Context is still bloated. | Instruct subagents to return concise summaries and write raw data to the filesystem. |
| Captured output is missing. |
Check state.subagent_outputs["your_key"]; the parent model only receives the acknowledgement unless parent_result: :full is configured.
|
| A captured subagent does not rerun. |
Repeated captured calls are cached by subagent name and task input. Use capture_output: [key: :your_key, dedupe: false] for volatile work.
|
| The wrong subagent is selected. | Make descriptions mutually exclusive, for example separate "quick facts" from "deep research". |
| A subagent cannot access an expected tool. |
Check whether :tools was set. A list replaces inherited tools; nil inherits parent tools.
|
A subagent cannot use write_todos or filesystem tools.
| Add the corresponding middleware or filesystem configuration to the child agent. These tools are not implicit. |
| A child cannot resume after HITL. | Ensure the parent has a checkpointer and the same thread configuration is used for resume. |