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:
KeyRequiredWhat it declares
systemYesName and metadata for this system
defaultsNoThe default model, used unless a node overrides it
toolsNoPython functions agents can call
mcpsNoRemote MCP servers agents can call
resolversNoValues auto-filled into prompts before an agent runs
orchestratorsSee note belowRouters that decide which agent handles a request
agentsSee note belowExecutors that run an LLM and can call tools
hooksNoTrusted code that runs at fixed points — auth, audit
pluginsNoWhere Extra finds your plugin code
graphYesHow everything above connects — the routing topology
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.
system:
  name: "Acme Support Bot"
FieldRequiredDescription
nameYesDisplay name shown in logs and traces.

defaults

The model every agent and orchestrator uses unless it overrides its own.
defaults:
  model:
    provider: anthropic
    name: claude-sonnet-4-6
    temperature: 0.3
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

model:
  provider: anthropic
  name: claude-sonnet-4-6
  temperature: 0.3
model:
  provider: bedrock
  name: anthropic.claude-3-5-haiku-20241022-v1:0
  region: us-east-1
  temperature: 0.0
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.
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.

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.
tools:
  get_order_status:
    description: "Get the current status and tracking info for an order by order ID."

  cancel_order:
    description: "Cancel an order that hasn't shipped. Requires order ID and reason."

  create_return_request:
    description: "Open a return request for a delivered order. Requires order ID and reason."
FieldRequiredDescription
descriptionYesWhat the model reads to decide whether to call this tool.
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
def get_order_status(order_id: str) -> dict:
    """Get the current status and tracking info for an order by order ID."""
    return {
        "status": "shipped",
        "tracking_number": "1Z999AA10123456784",
        "estimated_delivery": "2026-07-05",
    }
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.
mcps:
  orders_mcp:
    url: "https://api.acme.com/mcp"

  catalog_mcp:
    url: "https://catalog.acme.com/mcp"
    tool_tags: ["public"]        # only discover tools tagged "public"
FieldRequiredDescription
urlYesThe MCP server’s Streamable HTTP endpoint.
tool_tagsNoOnly discover tools carrying this tag.
tool_tag_transportNoHow the tag is sent — see below.
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:
mcps:
  crm_read_only:
    url: "https://crm.acme.com/mcp"
    tool_tags: ["read-only"]     # orders_agent only sees read tools

  crm_admin:
    url: "https://crm.acme.com/mcp"
    tool_tags: ["admin"]         # admin_agent sees write tools
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:
typeWhere the tag goesRequired field
headerA custom request header, instead of X-MCP-Tool-Tagheader_name — the header to use
query_paramA query string parameter appended to the MCP urlparam_name — the parameter name
mcps:
  internal_api:
    url: "https://internal.acme.com/mcp"
    tool_tags: ["v2"]
    tool_tag_transport:
      type: header
      header_name: "X-Internal-Tag"
    # sends: X-Internal-Tag: v2
mcps:
  legacy_api:
    url: "https://legacy.acme.com/mcp"
    tool_tags: ["v2"]
    tool_tag_transport:
      type: query_param
      param_name: "tag"
    # requests: https://legacy.acme.com/mcp?tag=v2

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).
resolvers:
  current_date:
    scope: shared      # runs once, every agent that opts in gets it

  customer_name:
    scope: shared

  loyalty_tier:
    scope: agent        # runs only for agents that declare it
scopeRuns onUse it for
sharedEvery agent that lists itContext most agents need — date, user identity, locale
agentOnly the declaring agentContext specific to one agent — its own lookups, its own logic
Implementation, scaffolded by generate:
plugins/resolvers/shared.py
class SharedResolver:
    def current_date(self, ctx) -> str:
        return datetime.now().strftime("%Y-%m-%d")

    def customer_name(self, ctx) -> str:
        return ctx.headers.get("X-Customer-Name", "Customer")

Resolvers vs. tools

Both point at Python code — the difference is when they run and who decides to run them.
ResolverTool
RunsBefore the agent startsWhile the agent is running
Chosen byExtra, automaticallyThe model, if it decides to
Visible to the modelNoYes (name + description)
Token costNoneYes
Typical useFill {{current_date}}, {{customer_name}}”Look up this order”, “cancel this booking”

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.
orchestrators:
  router:
    name: "Support Router"
    description: "Routes customer requests to the right department."
    model:
      provider: anthropic
      name: claude-haiku-4-5    # routing only — a fast model is enough
      temperature: 0.0
    prompts:
      orchestrator: "prompts/router/orchestrator.md"
      system: "prompts/router/system.md"   # optional
FieldRequiredDescription
nameNoDisplay name. Defaults to the key.
descriptionNoWhat a parent orchestrator reads to decide when to route here.
modelNoOverrides defaults.model. Full replacement.
prompts.orchestratorYesThe routing instructions — see below.
prompts.systemNoAdditional system context.
prompts.userNoUser-turn context injection.

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
You are a customer support router for an online store.
Route the user to the most relevant department based on their message.
If it's unclear which department applies, ask one clarifying question before routing.
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.
agents:
  orders_agent:
    name: "Orders"
    description: "Handles order status, tracking, and delivery questions."
    model:
      provider: anthropic
      name: claude-sonnet-4-6
    prompts:
      system: "prompts/orders/system.md"
      user: "prompts/orders/user.md"       # optional
    resolvers: [current_date, customer_name]
    tools: [get_order_status, cancel_order]
    mcps: [orders_mcp]

  returns_agent:
    name: "Returns"
    description: "Handles return requests, refund status, and return policy questions."
    prompts:
      system: "prompts/returns/system.md"
    tools: [create_return_request]
    mcps: [orders_mcp]

  catalog_agent:
    name: "Catalog"
    description: "Answers questions about products, availability, and specifications."
    prompts:
      system: "prompts/catalog/system.md"
    mcps: [catalog_mcp]
FieldRequiredDescription
nameNoDisplay name.
descriptionRequired if this agent sits under an orchestratorTells the router when to send requests here.
modelNoOverrides defaults.model. Full replacement.
prompts.systemNoDefines the agent’s behavior and persona.
prompts.userNoUser-turn context injection.
resolversNoResolver ids to run before this agent executes.
toolsNoTool ids this agent is allowed to call.
mcpsNoMCP server ids this agent has access to.
protectedNotrue to gate this node behind access control.
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
You are an orders specialist for Acme Store.
Today's date is {{current_date}}.
You are helping {{customer_name}}.

You can:
- Check order status and tracking
- Cancel orders that haven't shipped yet
- Escalate to a human agent for complex issues

You cannot process refunds — direct those questions to the returns team.
{{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:
hooks:
  before_mcp_request:
    - plugin: mcp_auth
      method: inject_auth
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.
plugins:
  import_roots: ["."]
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:
User message


  router                  orchestrator — decides where to send the request
  ├── orders_agent        handles order status, tracking
  ├── returns_agent       handles refunds and returns
  └── catalog_agent       handles product questions
The YAML is a direct translation of that tree:
graph:
  router:
    orders_agent:
    returns_agent:
    catalog_agent:
Orchestrators can nest inside other orchestrators, letting you build multi-level routing:
graph:
  main_router:
    store_router:          # sub-orchestrator — only sees store-related children
      orders_agent:
      returns_agent:
      catalog_agent:
    account_agent:          # direct child of main_router
    vip_agent:               # protected — only reachable by VIP customers
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.
agents:
  vip_agent:
    description: "Exclusive offers and services for VIP members."
    protected: true
    prompts:
      system: "prompts/vip/system.md"
When any node in the spec is protected: true, Extra requires plugins/access.py:
plugins/access.py
class AccessResolver:
    def can_access(self, ctx, node_id: str) -> bool:
        return ctx.headers.get("X-Customer-Tier") == "vip"
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:
system:
  name: "Acme Customer Support"

defaults:
  model:
    provider: anthropic
    name: claude-haiku-4-5
    temperature: 0.0

mcps:
  orders_mcp:
    url: "https://api.acme.com/mcp"
    tool_tags: ["support"]
  catalog_mcp:
    url: "https://catalog.acme.com/mcp"

tools:
  create_return:
    description: "Open a return request for a delivered order by order ID and reason."

resolvers:
  current_date:
    scope: shared
  customer_name:
    scope: shared

orchestrators:
  router:
    description: "Routes customer requests to the right department."
    model:
      provider: anthropic
      name: claude-haiku-4-5
      temperature: 0.0
    prompts:
      orchestrator: "prompts/router/orchestrator.md"

agents:
  orders_agent:
    description: "Order status, tracking, and delivery questions."
    prompts:
      system: "prompts/orders/system.md"
    resolvers: [current_date, customer_name]
    mcps: [orders_mcp]

  returns_agent:
    description: "Return requests and refund questions."
    prompts:
      system: "prompts/returns/system.md"
    resolvers: [current_date, customer_name]
    tools: [create_return]
    mcps: [orders_mcp]

  catalog_agent:
    description: "Product questions, availability, and specifications."
    prompts:
      system: "prompts/catalog/system.md"
    mcps: [catalog_mcp]

  vip_agent:
    description: "Exclusive deals and priority support for VIP members."
    protected: true
    prompts:
      system: "prompts/vip/system.md"
    resolvers: [customer_name]
    mcps: [orders_mcp]

hooks:
  before_mcp_request:
    - plugin: mcp_auth
      method: inject_auth

plugins:
  import_roots: ["."]

graph:
  router:
    orders_agent:
    returns_agent:
    catalog_agent:
    vip_agent: