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

# Agent squads: route inbound to the right specialist

> Compose a classifier agent plus specialist members behind a single entry point. Build intent-based routing with fallback handling, cost caps, and a visual canvas.

# Agent squads

A squad composes several specialist agents behind one entry point. Every inbound message first hits the squad's **classifier** agent, which assigns the message an intent label. Orbit then routes the turn to the specialist member that owns the matching label, or to an optional **fallback** agent when nothing matches.

## What a squad is

A squad has four parts:

* **Classifier agent** — the front door. On every inbound turn Orbit asks it a single, constrained question: "Which of these intent labels best fits this message?" The classifier answers with exactly one label, and that label decides where the turn goes.
* **Member agents (2–6)** — the specialists. Each member is declared with an agent ID, one or more intent labels (up to 10), and a short description that tells the classifier what kinds of messages the label covers.
* **Fallback agent (optional)** — receives turns the classifier maps to no member, and turns where the classifier itself is unreachable (for example a provider timeout).
* **Daily cost cap (optional)** — `max_cost_per_day_cents` bounds the squad's LLM spend per UTC day. When the cap is reached, routing stops with a `429` instead of firing further model calls.

Squads are tenant-scoped: every agent the squad references must exist in your tenant, and validation rejects the routing loops that would otherwise spin (a classifier listed as a member, a fallback equal to the classifier, or a label claimed by two members). The API rejects those shapes with `422` and a `squad_loop_detected` reason.

## When to use a squad

Most routing setups fall into one of two shapes:

* **Handoff targets** (the non-squad equivalent): one router agent converses with the user and hands off with a tool call. Good when the router should have a personality, answer simple questions itself, or hand off mid-conversation. See [Agent handoff targets](/agents/handoff-targets).
* **Squad** (this page): a classifier labels the message, usually on its first turn, and a specialist owns the conversation from there. Good when you want strict multi-persona separation — sales vs support vs returns — where each specialist carries its own model, knowledge bases, and guardrails, and where membership lives in one config object instead of spread across every peer's allowlist.

If one agent should handle everything and only occasionally escalate, use handoff targets. If routing belongs at the front door with a deterministic label, use a squad.

## Smart routing — build the classifier

Any agent in your tenant can be the classifier; it must not also appear in `members`. Create it like any other agent from [Creating agents](/agents/creating-agents), then keep its prompt focused. The runtime asks the classifier only for a label, so the shortest possible prompt wins:

```text theme={null}
You are a message router. Classify the user's message into exactly one of
the squad's labels. Respond with only the label, nothing else.
```

The router also injects each member's first label plus its description, so the classifier decides from those — you don't need to paste the member list into the prompt yourself, and a brief classifier prompt is more reliable than one padded with personality instructions.

Classification runs at temperature `0` with a short max-token budget, so one word back is all it needs. Matching is tolerant: leading/trailing quotes, "Label:" prefixes, and trailing punctuation are normalised before comparison, so don't stress about the model emitting `"Sales."` instead of `sales`.

## Define the members

A squad needs at least two members and supports at most six. Each member entry carries:

| Field           | Required   | Meaning                                                                                                                                                           |
| --------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_id`      | Yes        | The specialist agent that owns the turn when its label matches.                                                                                                   |
| `intent_labels` | Yes (1–10) | The exact strings the classifier may emit that map to this member. Labels must be unique across the whole squad.                                                  |
| `description`   | Yes        | One sentence telling the classifier what this label covers, so it can distinguish semantically close members ("order status and refunds" vs "product questions"). |

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/squads \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support Router",
    "description": "Routes inbound support chats to the right specialist",
    "classifier_agent_id": "agent_classifier01",
    "members": [
      {
        "agent_id": "agent_sales01",
        "intent_labels": ["sales"],
        "description": "Pricing, quotes, and product questions"
      },
      {
        "agent_id": "agent_support01",
        "intent_labels": ["support"],
        "description": "Account issues, bugs, and how-to questions"
      },
      {
        "agent_id": "agent_returns01",
        "intent_labels": ["returns", "refunds"],
        "description": "Return requests and refund status"
      }
    ],
    "fallback_agent_id": "agent_general01",
    "max_cost_per_day_cents": 500
  }'
```

The response echoes the squad with its generated `id` (prefixed `squad_`). Members route by label: the member whose first `intent_labels` entry the classifier echoes back wins the turn.

## Fallback and cost cap semantics

**Fallback.** Set `fallback_agent_id` to a generalist agent that can handle anything:

* When the classifier returns "none" or any uncertainty signal, the turn goes to the fallback.
* When the classifier emits a label no member claims (normalisation aside), the turn goes to the fallback.
* When the classifier call itself fails (provider timeout, malformed response), the turn goes to the fallback rather than erroring the conversation.

If you omit `fallback_agent_id`, an unmatched or unavailable classifier returns no agent and the caller surfaces a generic routing error. Set a fallback unless you genuinely want unmatched intents to fail hard.

**Cost cap.** `max_cost_per_day_cents` (a positive integer of cents, per UTC day) bounds the squad's classifier plus member LLM spend. When the cap trips, the routing layer stops with a `429` immediately instead of routing to the fallback and spending again — a deliberate stop-spending decision, not an error to retry. Leave it `null` for no cap. Per-agent daily caps from [Cost controls](/agents/cost-controls) still apply to each member individually; the squad cap sits on top of them.

## Visualise the canvas

The dashboard surface at **Agents → Squads** renders each squad as a router node fanning out to specialist nodes, with a fallback badge where one is set. The canvas reads `GET /api/v1/agents/squads` and draws the classifier → member tree so you can audit label coverage and spot an uncaught-fallback gap before going live. An analytics panel on the squad detail page rolls up per-specialist utilisation, handoff success rate, and containment lift versus the classifier-alone baseline over a configurable window (default 30 days).

You can also dry-run the classifier without touching a live channel:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/squads/squad_abc123/test \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "message": "My order never arrived, I want a refund" }'
```

The response names the matched member (`source: "matched"`), the fallback (`source: "fallback"`), or `source: "unavailable"` when neither exists — plus the raw label the classifier returned, so you can tune descriptions before deploying.

## End-to-end example

1. Create the four agents: a classifier, a sales specialist, a support specialist, and a general fallback. Deploy the classifier to your inbound channel — the classifier is the squad's entry point.
2. Create the squad with `sales` → `agent_sales01`, `support` → `agent_support01`, fallback → `agent_general01`.
3. Test it with a few realistic messages (`POST /:id/test`) and adjust member descriptions until every sample lands on the intended label.
4. An inbound "I want to add three more seats" classifies as `sales` → the sales specialist takes over. "The export feature throws a 500" classifies as `support` → the support specialist takes over. "Hey, what do you folks do?" doesn't match → the fallback generalist handles it.
5. Watch the analytics panel for members with high handoff drop-off; tighten their label descriptions or add a second label to close the gap.

## See also

* [Agent handoff targets](/agents/handoff-targets) — the router-as-agent alternative for mid-conversation hand-off with an allowlist.
* [Cost controls](/agents/cost-controls) — per-agent daily spend limits that sit beneath the squad cap.
* [Model selection](/agents/model-selection) — pick a fast, cheap model for the classifier; heavier models for the specialists.
* [Creating agents](/agents/creating-agents) — build the classifier and specialist agents the squad references.
* [Agents API reference](/api-reference/endpoints/agents) — full request/response schemas for the squad endpoints.
