Sub-agents and trees of work
How agents delegate to other agents in Flux, what durability gives the tree-of-work pattern, and when delegation actually pays off.
An agent can call another agent. That single move is the difference between a single LLM session and a tree of work — a parent agent that breaks a problem into pieces and hands each piece to a specialist child. The pattern shows up in every multi-agent framework: OpenAI Swarm calls it “handoff”, CrewAI calls it “delegation”, LangGraph models it as edges between nodes. Flux calls it agents=[...] and the resulting tool is delegate.
What Flux adds on top of the pattern is the same thing it adds to everything else: durability. Each sub-agent invocation is a recorded task, replayable from the event log, resumable across worker crashes. The tree doesn’t just exist as a runtime structure — it exists as a structure in the event log.
The pattern
A parent agent has a goal. It decides part of that goal is better handled by a different agent — one with a different system prompt, a different tool surface, maybe a different model. It delegates. The sub-agent runs, produces a result, returns. The parent reads the result, decides what to do next, possibly delegates again to a different child or to the same one with different input.
The line between “tool call” and “delegation” is mostly about who is doing the reasoning. A tool is a function the LLM calls with arguments to get a result; the LLM does all the thinking. A sub-agent is a whole conversation handed off to a different LLM session — the child does its own multi-turn reasoning, its own tool calls, its own planning if it has planning enabled. If the work needs its own reasoning loop, it’s a sub-agent. If it doesn’t, it’s a tool.
The Flux mechanism
flux/tasks/ai/delegation.py provides one primitive: build_delegate(agents: list) -> task. When you call agent(...) with a non-empty agents=[...] list, three things happen:
- Each sub-agent is validated. Names must be lowercase letters, digits, and single hyphens; under 64 characters; no consecutive hyphens; not starting or ending with one. Each sub-agent must be callable and have non-empty
nameanddescriptionattributes. Names within one list must be unique. Failures raiseAgentValidationErrorat construction time, before the workflow runs. - A
## Sub-Agentssection is appended to the parent’s system prompt, listing each child’s name and description. This is what the parent LLM sees to decide where to route work. - A
delegate@taskis built — a dict lookup over the registered children — and added to the parent’s tool list.
From the LLM’s point of view, delegate is just another tool. From Flux’s point of view, it’s just another task. Both views are right. The dispatch is a one-line dict lookup; the work happens inside whatever callable the child is.
Each child gets a blank context
This is the load-bearing detail. From the system-prompt preamble Flux injects into the parent (delegation.py::build_agents_preamble):
Each agent starts with a blank context. When chaining agents, consider passing relevant output from previous delegations so the next agent has the context it needs.
When the parent calls delegate(agent="researcher", instruction="...", input="..."), the child sees only instruction (with expected_output appended if provided) and context (parsed from input). It does not see the parent’s conversation history, prior tool outputs, system prompt, or working memory. If the child has its own working_memory or long_term_memory attached at construction, those are the child’s — separate from the parent’s.
The same applies in reverse. The parent sees a DelegationResult dict — {agent, status, output, execution_id?} — and nothing else. It doesn’t get to peek inside the child’s tool calls or intermediate reasoning. They’re recorded in the event log (so a human or another tool can inspect them), but the parent LLM only sees the final output.
This isolation is deliberate. It’s what makes specialization work: a child agent with a tight system prompt about, say, SQL query generation, isn’t polluted by the parent’s broader context. It also means you have to explicitly pass any context the child needs, inside instruction or input.
Four flavors of sub-agent
Look at what flux/agents/ and flux/tasks/ai/ actually expose, and there are four shapes:
In-workflow local sub-agent
The parent and child run in the same workflow execution. The child is a regular agent() task awaited from inside the parent’s tool loop. There’s no network hop, no separate execution; the child’s tool events nest inside the parent’s in the event log.
researcher = await agent(
"You are a research specialist.",
model="ollama/qwen3",
name="researcher",
description="Deep research using web sources.",
tools=[search_web],
)
manager = await agent(
"You are a project manager.",
model="ollama/qwen3",
agents=[researcher],
)
See examples/ai/sub_agents_local.py for the full example.
Workflow-backed sub-agent
A workflow_agent(name, description, workflow) is a factory that returns a task wrapping a remote Flux workflow. The parent invokes it via delegate; under the hood, it uses FluxClient to call run_workflow_sync (or resume_execution_sync if resuming) on the named workflow. The child runs as a separate workflow execution on a (possibly different) worker, with its own execution_id, its own event log, and its own checkpoint stream.
deployer = workflow_agent(
name="deployer",
description="Handles deployment pipelines.",
workflow="deploy_pipeline",
)
manager = await agent(
"You are a release manager.",
model="ollama/qwen3",
agents=[deployer],
)
See examples/ai/sub_agents_workflow.py.
Mixed list
Local agent() tasks and workflow_agent() instances can sit in the same agents=[...] list. The delegate tool dispatches by name and doesn’t care which kind it’s invoking. From the LLM’s perspective they’re identical — DelegationResult looks the same either way. See examples/ai/sub_agents_mixed.py.
The harness case
When you register an agent via flux agent create --file ...yaml (the YAML harness, flux/agents/template.py), and that YAML lists agents: [child], Flux wires those children up as workflow-backed sub-agents pointing at the shared agents/agent_chat workflow. So every harness sub-agent is a separate workflow execution. The Python in-workflow flavor is the one place you can avoid the network hop.
What durability gives this pattern
Every delegation is a delegate task call, which means it gets the full task contract:
- Recorded.
TASK_STARTEDandTASK_COMPLETEDevents bracket every delegation, with the sub-agent’s name, the instruction, and the result captured in the event log. - Replayable. If the parent crashes after the child has completed, replay returns the child’s recorded
DelegationResultwithout re-running the child. The parent picks up where it left off. - Resumable mid-flight. If a workflow-backed child pauses (e.g., for human approval), the parent receives
status="paused"with the child’sexecution_id. The parent LLM can resume by callingdelegateagain with thatexecution_id; Flux routes throughresume_execution_syncto pick the child back up at its own checkpoint, without re-running the parent’s prior delegations. - Independently durable. A workflow-backed child runs as its own execution. If the child’s worker dies mid-run, its work is checkpointed; the parent doesn’t even notice — the synchronous wait blocks until the child either completes, pauses, or fails on a different worker.
Frameworks without a persistent execution state can’t match this — CrewAI’s crew.kickoff() runs the tree in-process; Flux’s tree survives crashes by construction. If the CrewAI process dies, the tree is gone; in Flux every node is a checkpointable execution unit.
Failure semantics
build_delegate catches everything the sub-agent raises:
- A
PauseRequestedfrom the child becomesstatus="paused"with the child’soutputfield carrying the pause reason. - Any other exception becomes
status="failed"withoutput=str(e). - An unknown agent name returns
status="failed"with a message listing available agents.
The parent’s tool loop never sees a raised exception from delegate. It sees a JSON dict, parses status, and decides what to do — try a different child, retry with different input, give up and surface the error to the human, or ask for help. The parent’s own retry, fallback, and rollback options apply to delegate like any other task; if you want hard guarantees on the delegation call itself, configure them when you wire the sub-agent up.
The downside: a parent that doesn’t check status will happily proceed with a failed delegation’s error message as if it were valid output. Write the parent’s system prompt to inspect the status field, or constrain the parent to a structured output that forces it to acknowledge failures.
When trees beat single agents
Three reasons to delegate.
Specialization. A child can have a sharper system prompt and a tighter tool surface. The researcher’s prompt is about gathering information; the reviewer’s is about evaluating it. Mixing both into one agent dilutes both. Splitting them lets each be good at its job.
Parallelism. Multiple children can run in parallel branches of the parent’s reasoning. The parent can fan out to several researchers on different sub-topics, then synthesize. (The current delegate tool dispatches one call at a time per LLM turn, but the parent can issue several tool calls per turn — see Parallel tool execution.)
Cost. A cheap model for cheap sub-tasks; a strong model for orchestration. A gpt-4o-mini summarizer feeding a claude-sonnet decision-maker is a fraction of the cost of running the strong model end-to-end.
When trees aren’t worth it
Most “multi-agent” problems are actually single-agent problems with the wrong system prompt. If the work doesn’t need the child to do its own reasoning — if you’d be giving the child a one-shot prompt to produce a one-shot answer — it’s a tool, not an agent. A tool is cheaper, simpler, and equally durable.
The other anti-pattern is deep trees. In practice, most useful trees are shallow: a parent with two to five children, maybe one or two of those children with their own grandchildren. Deep, branching trees (ten levels, dozens of nodes) usually indicate that the orchestration itself should be a workflow with explicit control flow, not an LLM-driven delegation chain. Workflows are deterministic; delegation chains are probabilistic. The deeper the tree, the more places the dice get thrown.
Flux doesn’t enforce a depth limit on agent nesting — each level just runs its own tool loop bounded by its own max_tool_calls. The discipline is on you.
What to remember
- Delegation in Flux is
agents=[...]onagent(), which injects adelegatetool and a sub-agent preamble into the system prompt. The mechanism isflux/tasks/ai/delegation.py. - Each sub-agent gets a blank context. Pass what the child needs in
instructionorinput. Memory is not shared. - Four flavors: in-workflow local (
await agent(...)), workflow-backed (workflow_agent(...)), mixed in the same list, and the YAML harness (which uses workflow-backed children by default). - Durability applies per delegation. Failures, pauses, and resumes are first-class. The tree survives crashes because every node is a checkpointable unit.
- Use delegation when the child needs its own reasoning, when specialization or parallelism pays off, or when cost dictates a model mix. Don’t use it when a tool would do.
Where this shows up
- Agents as durable workflows — the broader mental model behind why agents and workflows share machinery.
- Memory architecture — why working memory and long-term memory live with each agent, not across the tree.
- Sub-agents — the how-to for the
agents=[...]API, including validation and nested hierarchies. - Composing workflows — the non-agent analogue: workflows calling workflows via
call().