Skip to main content

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.
Extra validates the whole file before running anything — bad references, missing prompt files, or broken graph structure fail immediately with a clear error, not at request time. Every example below builds toward one running system: a customer support bot for an online store, with separate agents for orders, returns, and catalog questions.

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.
Set a fast, cheap model as the default (e.g. Haiku) and override to a stronger model only where it matters. Orchestrators that just route work well with a fast model — save the strong model for agents doing real reasoning.

Supported providers

For Bedrock, 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.
A node-level model is a full replacement, not a merge with defaults.model. If you override the model on an agent, specify every field — provider, name, and any others you need. Partial overrides won’t inherit missing fields from defaults.
Never put API keys in YAML. Extra reads provider credentials from environment variables at runtime.

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:
Nested fallbacks (a fallback model configured under another fallback model) are not allowed and will fail spec validation.

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.
The description is the model’s only signal for choosing a tool. Write it like you’re briefing a new team member — clear inputs, clear purpose. “Get order status by order ID” beats “order_status_getter” every time.
generate creates one stub file per tool (see Quickstart). You fill in the real implementation:
plugins/tools/get_order_status.py
Give an agent access by adding the tool id to its 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, use tool_tags to limit what a given connection discovers:
By default, tags are sent as an 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
The router sees this prompt plus every child’s 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:
The YAML only registers which hook runs whereplugin 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.
Set this once, at the project root, and forget about it.

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:
The YAML is a direct translation of that tree:
Orchestrators can nest inside other orchestrators, letting you build multi-level routing:
At runtime, 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 orchestrators or agents.
  • Orchestrators can have children; agents are leaves.
  • No cycles.

Access control

Mark any node protected: 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.
When any node in the spec is 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.

Complete example

An e-commerce support bot with a protected VIP tier: