Orchestration is deciding which agents run, in what order, and how they decide what happens next. There are two main approaches, freely mixable:
An agent equipped with tools, handoffs and clear instructions can plan autonomously: research with tools, delegate specialist work, write results somewhere. This is the most flexible pattern. Tactics that pay off (same as Python):
research := &agents.Agent{Name: "Researcher", Tools: []agents.Tool{webLookup}}
writer := &agents.Agent{Name: "Writer", Instructions: agents.StaticInstructions("Write the final report.")}
planner := &agents.Agent{
Name: "Planner",
Instructions: agents.StaticInstructions("Plan the work, research as needed, then hand off to the writer."),
Tools: []agents.Tool{webLookup},
Handoffs: []agents.Handoff{agents.HandoffTo(writer)},
}
Where a handoff transfers the conversation, AsTool keeps the orchestrator in charge: the sub-agent runs on just the input the orchestrator passes, returns its final output as the tool result, and the orchestrator continues.
spanish := &agents.Agent{Name: "spanish_agent", Instructions: agents.StaticInstructions("Translate the message to Spanish.")}
french := &agents.Agent{Name: "french_agent", Instructions: agents.StaticInstructions("Translate the message to French.")}
orchestrator := &agents.Agent{
Name: "orchestrator",
Instructions: agents.StaticInstructions(
"You are a translation agent. Use the tools to translate; for multiple languages, call the relevant tools."),
Tools: []agents.Tool{
spanish.AsTool(agents.AgentToolConfig{Name: "translate_to_spanish", Description: "Translate the user's message to Spanish"}),
french.AsTool(agents.AgentToolConfig{Name: "translate_to_french", Description: "Translate the user's message to French"}),
},
}
AgentToolConfig options:
| Field | Purpose |
|---|---|
Name / Description |
What the calling model sees (name defaults to the sanitized agent name) |
MaxTurns |
Turn budget for the nested run (0 = default) |
CustomOutputExtractor |
Derive the tool’s string result from the nested *RunResult |
The nested run inherits the parent’s model provider, model override, model settings and tracer through the run context, so sub-agents need no provider of their own. Its spans join the parent’s trace; its usage is tracked separately. If the model calls several agent-tools in one turn they run concurrently — like any other function tools.
Plain Go is often the clearest orchestrator — deterministic, testable, cheap:
// Chain: outline -> approve -> write
outline, err := agents.Run(ctx, outliner, topic, opts)
if err != nil { return err }
check, err := agents.Run(ctx, reviewer, outline.FinalOutputString(), opts)
if err != nil { return err }
if verdict, _ := agents.FinalOutputAs[Verdict](check); !verdict.Good {
return fmt.Errorf("outline rejected: %s", verdict.Reason)
}
story, err := agents.Run(ctx, writer, outline.FinalOutputString(), opts)
Patterns that map directly from the Python docs:
OutputType to get a typed verdict you can branch on.agents.Run calls in goroutines (e.g. with errgroup) and join the results.g, gctx := errgroup.WithContext(ctx)
results := make([]*agents.RunResult, len(questions))
for i, q := range questions {
g.Go(func() error {
res, err := agents.Run(gctx, analyst, q, opts)
results[i] = res
return err
})
}
if err := g.Wait(); err != nil { /* … */ }