What this file does
agents.yml defines your entire agent system: every agent, what it can do, and how requests route between them. Extra validates it and compiles it into a running system.
You still write the prompt files it references and implement any tools or resolvers you declare — the YAML alone isn’t the whole system.
Every top-level key, in the order you’d typically write them:
orchestrators and agents are each individually optional, but graph must have a root, and that root has to be declared in one of them. In practice: a single-agent system needs only agents; anything routing between multiple agents needs at least one orchestrator too.system
Identifies the system. Shows up in logs, traces, and the CLI — this is how you tell your systems apart once you’re running more than one.
defaults
The model every agent and orchestrator uses unless it overrides its own.
Supported providers
region can be omitted if AWS_REGION or AWS_DEFAULT_REGION is set. Credentials follow the standard AWS chain — profile, environment variables, or an IAM role.
For Gemini, set GEMINI_API_KEY in your environment (export GEMINI_API_KEY="..."). Choose any Gemini model your key can access via name. Secrets must never be stored in YAML.
For OpenAI, set OPENAI_API_KEY in your environment (export OPENAI_API_KEY="..."). Choose any OpenAI model your key can access via name. See the OpenAI API docs for model names. Secrets must never be stored in YAML.
Fallback models
To make agents resilient to LLM provider outages, rate limits, or transient network errors, you can configure a fallback backup model directly under any model configuration block:tools
A tool is a Python function an agent can choose to call. You describe what it does; Extra generates the stub; you write the logic.
generate creates one stub file per tool (see Quickstart). You fill in the real implementation:
plugins/tools/get_order_status.py
tools list — see agents below.
mcps
MCP servers are remote tool providers you connect by URL. Extra discovers their tools automatically at startup — you never write MCP client code.
Grant an agent access the same way as any tool — by id, in the agent’s
mcps list.
Tool tags
If your MCP server groups tools into categories, usetool_tags to limit what a given connection discovers:
X-MCP-Tool-Tag header. If your server expects something else, set tool_tag_transport to one of two types:
resolvers
A resolver fills a {{variable}} in a prompt — always run before the agent starts, never chosen by the model, and free (no token cost).
Implementation, scaffolded by
generate:
plugins/resolvers/shared.py
Resolvers vs. tools
Both point at Python code — the difference is when they run and who decides to run them.orchestrators
An orchestrator is a router. It reads the incoming message, looks at its children’s description fields, and decides which child should handle it. It never calls tools or MCP servers itself — routing is its only job.
Writing the orchestrator prompt
prompts.orchestrator points to a Markdown file containing the actual instructions the router follows. This is the file’s full content — nothing more is needed:
prompts/router/orchestrator.md
description. It picks the best match — it doesn’t need to know how each child works internally.
agents
An agent is an executor. Once the router sends it a request, it runs an LLM with its own prompt and can call whatever tools or MCP servers it’s been given access to — declared above under tools, mcps, and resolvers.
An agent only ever sees the tools, MCP servers, and resolvers it explicitly lists — nothing is shared by default.
Writing the system prompt
prompts/orders/system.md
{{current_date}} and {{customer_name}} are filled in automatically before the agent runs — see resolvers above.
hooks
A hook is trusted code that runs automatically at a fixed point in the request lifecycle — never exposed to the model, never chosen by it. The most common use is injecting auth headers before an MCP call:
plugin resolves to a class through plugins/plugins.toml, and method names the method to call. It carries no import path, no config block, and no secret. The hook code reads its token from the environment and attaches the Authorization header at request time, so the token never touches a prompt, a log line, or the YAML file itself.
Full list of lifecycle points, audit hooks, and result-transform examples: Runtime Hooks.
plugins
Tells Extra where to find your plugin code, so imports work regardless of which directory you run commands from.
graph
graph is where routing topology is declared. Indentation defines parent/child relationships. There is exactly one root — the entry point every request hits first.
Here’s the running example we’ve been building — one orchestrator routing to three agents:
main_router first decides between store_router, account_agent, and vip_agent. If it picks store_router, that orchestrator then makes its own routing decision among its three children. Each orchestrator only ever sees its own direct children — never the whole tree.
Rules:
- Exactly one root.
- Every key must already be declared under
orchestratorsoragents. - Orchestrators can have children; agents are leaves.
- No cycles.
Access control
Mark any nodeprotected: true to gate it behind your own access logic. A protected node is invisible to the router until your access plugin approves it — the model never even knows it exists.
protected: true, Extra requires plugins/access.py:
plugins/access.py
ctx carries the request headers — use whatever your auth system already provides. A denied node is removed from the router’s options entirely.