Alloy has no sub-agent primitive, on purpose. An agent's "self" is just
Alloy.run/2 — a function call — so a sub-agent is a tool whose execute/2
runs another agent. The whole pattern is ~30 lines, and on the BEAM it comes
with properties most frameworks charge a runtime for: each delegation runs in
a supervised task, crashes are isolated to the tool call, and when the model
requests several delegations in one turn the executor fans them out in
parallel (concurrent?/0).
This mirrors how the leanest coding agents handle delegation: they spawn another copy of themselves via a shell command rather than shipping a sub-agent subsystem. Alloy's translation: spawn yourself via function call.
The module below is tested verbatim in
test/alloy/recipes/sub_agent_recipe_test.exs. If the API drifts, that test fails before this doc lies to you.
The tool
defmodule MyApp.Tools.Delegate do
@behaviour Alloy.Tool
@impl true
def name, do: "delegate"
@impl true
def description do
"Delegate a self-contained task to a focused sub-agent. " <>
"The sub-agent has read-only file access and returns its final " <>
"answer as text. Provide the full task description — the " <>
"sub-agent cannot see this conversation."
end
@impl true
def input_schema do
%{
type: "object",
properties: %{
task: %{
type: "string",
description: "Complete, self-contained task for the sub-agent"
}
},
required: ["task"]
}
end
# Sub-agents are independent — let the executor fan them out in
# parallel when the model requests several delegations in one turn.
@impl true
def concurrent?, do: true
@impl true
def execute(%{"task" => task}, context) do
case Alloy.run(task,
provider: Map.fetch!(context, :delegate_provider),
tools: [Alloy.Tool.Core.Read],
max_turns: 8,
timeout_ms: 60_000,
max_budget_cents: 25
) do
{:ok, result} ->
{:ok, result.text || ""}
{:error, result} ->
{:error, "Sub-agent stopped (#{result.status}): #{inspect(result.error)}"}
end
end
endUsing it
Pass the sub-agent's provider through context: — typically a cheaper or
faster model than the parent's:
{:ok, result} =
Alloy.run("Compare the error handling in lib/foo.ex and lib/bar.ex",
provider: {Alloy.Provider.Anthropic, api_key: key, model: "claude-opus-4-6"},
tools: [MyApp.Tools.Delegate, Alloy.Tool.Core.Read],
context: %{
delegate_provider:
{Alloy.Provider.Anthropic, api_key: key, model: "claude-haiku-4-5"}
}
)Design notes
Give the sub-agent a narrower toolset than the parent. The example grants
only Read. A sub-agent with Bash or Write multiplies your blast radius;
delegation should reduce capability, not forward it.
Cap everything explicitly. The child run knows nothing about its parent —
parent budget, deadline, and correlation IDs do not propagate. Treat
max_turns, timeout_ms, and max_budget_cents on the child as mandatory,
and remember the parent's own max_budget_cents cannot see tokens spent
inside the child. If you need combined accounting, return result.usage in a
structured tool result and sum in your application.
Recursion. If you give the sub-agent the delegate tool too, you have
unbounded recursion at the model's discretion. Don't — or thread a depth
counter through context: and refuse below zero.
Don't fake a team. One level of delegation with explicit caps covers most real uses (research, review, summarization of large inputs). If you find yourself wanting routing, shared state, and inter-agent messaging, you've outgrown this recipe — build that layer in your application where it belongs.