> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extra-ai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# YAML Reference

> Every field you can declare in agents.yml, with examples across different domains.

## 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:

| Key                               | Required       | What it declares                                     |
| --------------------------------- | -------------- | ---------------------------------------------------- |
| [`system`](#system)               | Yes            | Name and metadata for this system                    |
| [`defaults`](#defaults)           | No             | The default model, used unless a node overrides it   |
| [`tools`](#tools)                 | No             | Python functions agents can call                     |
| [`mcps`](#mcps)                   | No             | Remote MCP servers agents can call                   |
| [`resolvers`](#resolvers)         | No             | Values auto-filled into prompts before an agent runs |
| [`orchestrators`](#orchestrators) | See note below | Routers that decide which agent handles a request    |
| [`agents`](#agents)               | See note below | Executors that run an LLM and can call tools         |
| [`hooks`](#hooks)                 | No             | Trusted code that runs at fixed points — auth, audit |
| [`plugins`](#plugins)             | No             | Where Extra finds your plugin code                   |
| [`graph`](#graph)                 | Yes            | How everything above connects — the routing topology |

<Note>
  `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.
</Note>

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.

```yaml theme={null}
system:
  name: "Acme Support Bot"
```

| Field  | Required | Description                            |
| ------ | -------- | -------------------------------------- |
| `name` | Yes      | Display name shown in logs and traces. |

***

## `defaults`

The model every agent and orchestrator uses unless it overrides its own.

```yaml theme={null}
defaults:
  model:
    provider: anthropic
    name: claude-sonnet-4-6
    temperature: 0.3
```

<Tip>
  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.
</Tip>

### Supported providers

<CodeGroup>
  ```yaml Anthropic theme={null}
  model:
    provider: anthropic
    name: claude-sonnet-4-6
    temperature: 0.3
  ```

  ```yaml Amazon Bedrock theme={null}
  model:
    provider: bedrock
    name: anthropic.claude-3-5-haiku-20241022-v1:0
    region: us-east-1
    temperature: 0.0
  ```
</CodeGroup>

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.

<Warning>
  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`.
</Warning>

<Warning>
  Never put API keys in YAML. Extra reads provider credentials from environment variables at runtime.
</Warning>

***

## `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.

```yaml theme={null}
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."
```

| Field         | Required | Description                                               |
| ------------- | -------- | --------------------------------------------------------- |
| `description` | Yes      | What the model reads to decide whether to call this tool. |

<Tip>
  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.
</Tip>

`generate` creates one stub file per tool (see [Quickstart](/docs/quickstart)). You fill in the real implementation:

```python plugins/tools/get_order_status.py theme={null}
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`](#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.

```yaml theme={null}
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"
```

| Field                | Required | Description                                |
| -------------------- | -------- | ------------------------------------------ |
| `url`                | Yes      | The MCP server's Streamable HTTP endpoint. |
| `tool_tags`          | No       | Only discover tools carrying this tag.     |
| `tool_tag_transport` | No       | How 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:

```yaml theme={null}
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:

| `type`        | Where the tag goes                                   | Required field                    |
| ------------- | ---------------------------------------------------- | --------------------------------- |
| `header`      | A custom request header, instead of `X-MCP-Tool-Tag` | `header_name` — the header to use |
| `query_param` | A query string parameter appended to the MCP `url`   | `param_name` — the parameter name |

<CodeGroup>
  ```yaml Custom header theme={null}
  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
  ```

  ```yaml Query parameter theme={null}
  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
  ```
</CodeGroup>

***

## `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).

```yaml theme={null}
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
```

| `scope`  | Runs on                   | Use it for                                                     |
| -------- | ------------------------- | -------------------------------------------------------------- |
| `shared` | Every agent that lists it | Context most agents need — date, user identity, locale         |
| `agent`  | Only the declaring agent  | Context specific to one agent — its own lookups, its own logic |

Implementation, scaffolded by `generate`:

```python plugins/resolvers/shared.py theme={null}
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.

|                      | Resolver                                     | Tool                                        |
| -------------------- | -------------------------------------------- | ------------------------------------------- |
| Runs                 | Before the agent starts                      | While the agent is running                  |
| Chosen by            | Extra, automatically                         | The model, if it decides to                 |
| Visible to the model | No                                           | Yes (name + description)                    |
| Token cost           | None                                         | Yes                                         |
| Typical use          | Fill `{{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.

```yaml theme={null}
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
```

| Field                  | Required | Description                                                      |
| ---------------------- | -------- | ---------------------------------------------------------------- |
| `name`                 | No       | Display name. Defaults to the key.                               |
| `description`          | No       | What a *parent* orchestrator reads to decide when to route here. |
| `model`                | No       | Overrides `defaults.model`. Full replacement.                    |
| `prompts.orchestrator` | **Yes**  | The routing instructions — see below.                            |
| `prompts.system`       | No       | Additional system context.                                       |
| `prompts.user`         | No       | User-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:

```markdown prompts/router/orchestrator.md theme={null}
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`](#tools), [`mcps`](#mcps), and [`resolvers`](#resolvers).

```yaml theme={null}
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]
```

| Field            | Required                                          | Description                                                        |
| ---------------- | ------------------------------------------------- | ------------------------------------------------------------------ |
| `name`           | No                                                | Display name.                                                      |
| `description`    | Required if this agent sits under an orchestrator | Tells the router when to send requests here.                       |
| `model`          | No                                                | Overrides `defaults.model`. Full replacement.                      |
| `prompts.system` | No                                                | Defines the agent's behavior and persona.                          |
| `prompts.user`   | No                                                | User-turn context injection.                                       |
| `resolvers`      | No                                                | Resolver ids to run before this agent executes.                    |
| `tools`          | No                                                | Tool ids this agent is allowed to call.                            |
| `mcps`           | No                                                | MCP server ids this agent has access to.                           |
| `protected`      | No                                                | `true` to gate this node behind [access control](#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

```markdown prompts/orders/system.md theme={null}
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](#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:

```yaml theme={null}
hooks:
  before_mcp_request:
    - plugin: mcp_auth
      method: inject_auth
```

The YAML only registers *which* hook runs *where* — `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](/docs/runtime-hooks).

***

## `plugins`

Tells Extra where to find your plugin code, so imports work regardless of which directory you run commands from.

```yaml theme={null}
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:

```text theme={null}
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:

```yaml theme={null}
graph:
  router:
    orders_agent:
    returns_agent:
    catalog_agent:
```

Orchestrators can nest inside other orchestrators, letting you build multi-level routing:

```yaml theme={null}
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.

***

<h2 id="access-control">
  Access control
</h2>

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.

```yaml theme={null}
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`:

```python plugins/access.py theme={null}
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:

```yaml theme={null}
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:
```
