Skip to main content

Agent Squads

Group specialist voice agents behind one entry point, control handoffs with a policy, dry-run the routing decision, and draft a new agent from a description.

Overview

A squad is several specialist voice agents behind one entry point. The entry agent answers, and hands off to a member when the caller's need matches that member's role. A handoff policy bounds how far that can go, so a call cannot bounce between agents forever.

Two extras sit alongside the roster:

  • POST /{squad_id}/simulate-handoff — a pure dry run that shows which member would be picked and why.
  • POST /author/draft — describe an agent in prose and get a complete configuration proposal back. This one costs credits.

Authentication

Authorization: Bearer cm_your_api_key
OperationScope
List/get squads, list members, simulate a handoffsquads:read
Create/update/delete squads and members, draft an agentsquads:write

Limits

ThingLimit
Members per squad12. Past that, use a call flow
Handoffs per call0..5, default 3
Role name64 characters
Member description2,000 characters

Squads

{
  "id": "sq10…",
  "tenant_id": "a0b1…",
  "name": "Support desk",
  "description": "Front line plus billing and technical specialists.",
  "entry_bot_id": "b1f2…",
  "handoff_policy": {
    "max_handoffs": 3,
    "allow_return_to_previous": false,
    "min_score": 1,
    "fallback_role": "generalist"
  },
  "is_active": true,
  "created_at": "2026-08-11T09:00:00Z",
  "updated_at": "2026-08-11T09:00:00Z",
  "members": []
}

Handoff policy

FieldTypeDefaultConstraints
max_handoffsinteger30 <= n <= 5. 0 disables handoffs entirely
allow_return_to_previousbooleanfalseLeaving this false is what stops two agents ping-ponging a caller
min_scoreinteger11 <= n <= 20. The match strength a member must reach to be handed to
fallback_rolestring | nullnullRole to use when nothing scores high enough

Unknown keys inside the policy are rejected with 422.

GET /api/v1/voice/squads

Newest first. Filter by is_active. limit 1..200 (default 50), offset 0..100000.

POST /api/v1/voice/squads

FieldTypeRequiredConstraints
namestringYes1–255 characters, unique per tenant
descriptionstringNoAt most 500 characters
entry_bot_idUUIDYesThe agent that answers the call
entry_rolestringNo1–64 characters, default entry
entry_descriptionstringNoAt most 2,000 characters
handoff_policyobjectNoOmit for the defaults above
is_activebooleanNoDefault true

Creating a squad automatically enrols the entry agent as the first member at position: 0 — you do not add it yourself.

Returns 201 with the squad and its roster.

GET / PATCH / DELETE /api/v1/voice/squads/{squad_id}

PATCH takes name, description, entry_bot_id, handoff_policy (an explicit null clears it back to defaults) and is_active.

entry_bot_id must point at an agent already in the squad — 422 entry_bot_id must be a bot that is already a member of this squad. Add the member first, then promote it.

DELETE returns 204 and cascades the roster.


Members

{
  "id": "mb20…",
  "tenant_id": "a0b1…",
  "squad_id": "sq10…",
  "bot_id": "b7c8…",
  "role": "billing",
  "description": "Handles invoices, refunds and payment failures.",
  "position": 1,
  "created_at": "2026-08-11T09:02:00Z"
}

role and description are what the routing engine matches a caller's utterance against — write the description as the things this agent handles, in the caller's words.

GET /api/v1/voice/squads/{squad_id}/members

Ordered by position, then oldest first. No pagination — the 12-member cap bounds it.

POST /api/v1/voice/squads/{squad_id}/members

FieldTypeRequiredConstraints
bot_idUUIDYesMust be your agent, not already in this squad
rolestringYes1–64 characters, not blank
descriptionstringNoAt most 2,000 characters
positionintegerNo0 <= position <= 1000, default 0

422 A squad holds at most 12 agents. Past that, use a call flow. · 409 That agent is already in this squad.

PATCH / DELETE /api/v1/voice/squads/members/{member_id}

PATCH takes role, description (explicit null clears it) and position. bot_id is not editable — remove the member and add the other agent.

Removing the entry agent returns 409 This agent answers the call for the squad. Point entry_bot_id at another member before removing it.


Simulating a handoff

POST /api/v1/voice/squads/{squad_id}/simulate-handoff

Requires squads:read. Pure and read-only: no model call, no credits, nothing written.

FieldTypeRequiredConstraints
utterancestringYes1–2,000 characters
handoffs_usedintegerNo0 <= n <= 100, default 0
current_member_idUUIDNoWho is handling the call now
previous_member_idUUIDNoWho handled it before — used for the ping-pong check
curl -X POST https://api.callmissed.com/api/v1/voice/squads/sq10…/simulate-handoff \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "utterance": "my card was charged twice", "handoffs_used": 1, "current_member_id": "mb01…" }'
{
  "squad_id": "sq10…",
  "handoff": true,
  "target_member_id": "mb20…",
  "target_bot_id": "b7c8…",
  "target_role": "billing",
  "reason": "matched billing on 'charged'",
  "blocked_by": null,
  "score": 3,
  "handoffs_used": 1,
  "max_handoffs": 3,
  "scores": [
    { "member_id": "mb20…", "role": "billing", "score": 3, "eligible": true },
    { "member_id": "mb30…", "role": "technical", "score": 0, "eligible": true }
  ]
}

scores shows every member's match strength, so a wrong route is debuggable: if the right agent scored 0, its description is missing the words callers actually use.

Why a handoff was blocked

blocked_byMeaning
no_membersThe squad has no one to hand to
max_handoffsThe policy's handoff budget is spent
ping_pongThe target is the previous member and returns are disallowed
already_currentThe best match is already handling the call
no_matchNothing reached min_score
unknown_roleThe configured fallback role matches no member

Drafting an agent

POST /api/v1/voice/squads/author/draft

Requires squads:write. Charges credits. It creates nothing — you get a proposal to review and then submit yourself via the bots API.

FieldTypeRequiredConstraints
descriptionstringYes20–4,000 characters, not blank
bot_typestringNoinbound_call (default), outbound_call, ivr, whatsapp or whatsapp_voice
curl -X POST https://api.callmissed.com/api/v1/voice/squads/author/draft \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "An agent for a Pune dental clinic that books, moves and cancels appointments in Hindi and English, and escalates anything about pain to a human.",
    "bot_type": "inbound_call"
  }'
{
  "draft": {
    "name": "Clinic Reception",
    "bot_type": "inbound_call",
    "system_prompt": "You are the receptionist for a dental clinic in Pune…",
    "objective": "Book, reschedule or cancel appointments; escalate pain reports.",
    "response_guidelines": "Keep replies under two sentences…",
    "conversation_script": "",
    "first_message": "Namaste, thanks for calling. How can I help?",
    "tools": ["book_appointment", "cancel_appointment", "handoff_to_human"],
    "voice_model": "…",
    "tts_model": "…",
    "stt_model": "…",
    "voice": "anushka",
    "language": "hi-IN"
  },
  "dropped_tools": ["send_invoice"],
  "model": "…"
}

dropped_tools lists tools the draft asked for that are not in your tool registry — they were removed so the configuration is valid on submission. Check this list: a dropped tool usually means the capability you described is not wired up yet.

At most 12 tools are proposed, de-duplicated and validated against the registry.

StatusDetailNote
402Not enough credits to draft an agent. Top up to continue.Checked before any model runs — costs nothing
422Could not draft an agent: …The model returned an unusable draft. This attempt is still billed — the work was done
502The agent drafting service is unavailable.Retry

Cost appears in usage logs as service: "llm".

Errors

StatusWhen
402Credit balance exhausted before drafting
403Key is missing squads:read / squads:write
404Squad, member or agent not in your tenant
409Duplicate squad or role name, agent already a member, or removing the entry agent
422Over 12 members, a blank name/role, an unknown key in handoff_policy, or an entry_bot_id that is not a member