Skip to main content

Subagents

A subagent is a focused second persona on the same agent: its own prompt, voice, model and tools, taking over part of a call and handing back when it is done. The agent bound to the phone number stays the main agent for the whole call - it keeps the call log, the recording, the webhook and the billing - so a subagent only changes how the assistant sounds and reasons while it is active.

Subagents and the transitions between them live under /agents/{agent_id}. Every route accepts the same authentication as the rest of the public API: send your key in the X-API-Key header (a Bearer JWT also works). The base URL is https://api.usevocals.com/api/v1.

Nodes and transitions

The model has two pieces:

  • A node is the main agent or one of its subagents. The main agent is always a node and needs no row of its own: wherever an id is expected, null means the main agent.
  • A transition is a one-way move from one node to another node of the same agent, with a trigger that decides when it fires.

Each agent can hold up to 20 subagents, and each node up to 20 outgoing transitions. Going past either cap returns 422 Unprocessable Entity. Neither list is paginated.

A single call may take at most 10 transitions, however large the flow is. Once a call reaches that limit the active node is told no further handoff is available and simply carries on with the caller: reaching the limit never ends a call. The same holds when a subagent cannot be entered at all - it was deleted after the call started, or a provider it depends on is unavailable - in which case the call returns to the main agent and continues.

What a subagent can and cannot change

A subagent can only override how the assistant behaves. It cannot touch anything that belongs to the call as a whole - the phone number, the speech-to-text provider and language, the response-trigger sensitivity, call and silence limits, answering-machine detection, recording, automatic analysis, the webhook, or the declared custom variables. Those are the main agent's for the entire call, and there is no field to override them.

Leaving an override unset (null) means "behave as the main agent does". A subagent that sets only system_prompt keeps the main agent's voice, model and tools.

Access

Access to an agent's subagents follows the sharing permission already granted on that agent. The owning workspace has full access; an agent shared with read permission can view subagents and transitions but not change them (403 Forbidden on a write); an agent shared with write permission can edit everything. An agent that is neither yours nor shared with you returns 404 Not Found.

Sharing is granted to a person, so it applies to the dashboard's Bearer JWT. An X-API-Key reaches only its own workspace's agents.

Subagent Schema

SubagentResponse is returned by the list, get, create, and update routes.

FieldTypeDescription
iduuidSubagent ID
agent_iduuidOwning main agent ID
namestringIdentifies the subagent, and is the name the AI uses when handing over to it. Unique within the agent and different from the main agent's own name.
system_promptstring | nullPrompt used while this subagent is active. null inherits the main agent's.
llm_provider_iduuid | nullLanguage-model provider to use. null inherits.
tts_provider_iduuid | nullVoice provider to use. null inherits.
voice_idstring | nullVoice to speak with. null inherits.
tts_configobject | nullVoice settings, same shape as the agent's. null inherits.
opening_linestring | nullSpoken the first time the caller reaches this subagent in a call, and never repeated on a return visit.
opening_line_enabledbooleanWhether the opening line is spoken at all (default true).
interruptibleboolean | nullWhether the caller can interrupt. null inherits.
barge_in_sensitivitystring | nullvery_low, low, medium, high, or very_high. null inherits.
thinking_cue_enabledboolean | nullWhether a thinking cue is played. null inherits.
transfer_enabledboolean | nullWhether this subagent may transfer the caller to a human. null inherits.
transfer_destinationsarray | nullHuman-transfer destinations, same shape as the agent's, up to 50. null inherits.
included_integration_idsarray | nullWhich of the agent's integrations this subagent may use. null means all of them, [] means none.
included_custom_endpoint_idsarray | nullSame, for custom endpoints.
included_knowledge_entry_idsarray | nullSame, for knowledge base entries.
entry_checkobject | nullAn on-entry check - see below.
created_atdatetimeCreation timestamp
updated_atdatetimeLast update timestamp

Entry check

An entry check is one custom endpoint the subagent calls automatically the moment it becomes active, before the caller says anything. Its result can fire a transition, which is how a flow routes on a fact ("this customer has an overdue invoice") rather than on what the caller happens to say.

{
"custom_endpoint_id": "3f1c9b7e-2a4d-4f88-9b0e-6d5c4a2b1e70",
"arguments": { "invoice_number": "4711" }
}

The endpoint must belong to the same agent and be either agent-wide or scoped to this subagent. It runs under its own configured timeout, is not retried, and a failure simply fires no transition - the call carries on.

Node-scoped tools and sources

A custom endpoint or knowledge base entry can be attached to a single subagent instead of the whole agent by sending subagent_id when you create it - see Custom Endpoints and Knowledge Base. Node-scoped rows count against the same per-agent caps as agent-wide ones. A node-scoped knowledge entry is only searched while its subagent is answering; the main agent's own retrieval never sees it.

Transition Schema

FieldTypeDescription
iduuidTransition ID
agent_iduuidOwning main agent ID
source_subagent_iduuid | nullNode the transition leaves. null is the main agent.
target_subagent_iduuid | nullNode the transition enters. null is the main agent.
trigger_typestringvariable_match, entry_check_match, or model_judgement
trigger_configobjectKeys depend on trigger_type - see below
positionintegerEvaluation order within the source node, lowest first
fire_countintegerHow many times this transition has fired across all calls
last_fired_atdatetime | nullWhen it last fired, null if it never has
created_atdatetimeCreation timestamp
updated_atdatetimeLast update timestamp

Source and target must be different, and both must be nodes of the same agent - a subagent belonging to another agent is rejected with 422.

Trigger types

trigger_typetrigger_configFires when
variable_match{"variable": "language", "value": "es"}A call variable equals the value
entry_check_match{"field": "status", "operator": "equals", "value": "200"}The source node's entry-check result matches. field is status or body, operator is equals or contains.
model_judgement{"condition": "the caller wants to pay an invoice"}The AI judges the plain-English condition true, in the same style as a human-transfer condition

Deterministic triggers are evaluated first, in position order; model judgement is the fallback when none of them fires. An entry_check_match transition needs its source node to declare an entry_check, otherwise creating it returns 422.

What the caller hears

A handover is meant to be inaudible. The subagent handing over says one short sentence in its own voice - it finishes that sentence before anything changes - and the subagent taking over then says its opening line, if it has one and this is the caller's first visit to it. Nothing else is spoken: the caller is never told a handover happened, never hears a destination name, and never has to repeat anything already covered. The whole conversation so far goes with them, along with a short note from the subagent handing over.

Everything the caller can perceive changes at that point: the voice, the language model, the tools, the knowledge base, the escalation destinations and the prompt. Nothing about the call itself does. The line stays up, speech-to-text keeps running on the main agent's settings, and no telephony operation takes place - which is what makes this different from transferring the caller to a human, where the call actually leaves VOCALS. Both work on the same agent, and a subagent can still escalate to a person.

After the call

A call that used subagents produces one call log, one recording, one transcript and one webhook, all belonging to the main agent. What each subagent did is recorded alongside:

  • node_journey and node_usage on the call detail response - who handled what, and what each one used and cost. See Calls.
  • node_id / node_name on every transcript turn.
  • nodes in the call.completed webhook payload, carrying the same journey.
  • fire_count and last_fired_at on each transition, so you can see which routes callers actually take.

Endpoints

MethodPathPurpose
GET/agents/{agent_id}/subagentsList the agent's subagents (oldest first)
POST/agents/{agent_id}/subagentsCreate a subagent
GET/agents/{agent_id}/subagents/{subagent_id}Fetch a single subagent
PUT/agents/{agent_id}/subagents/{subagent_id}Update a subagent (partial - send only what changes)
DELETE/agents/{agent_id}/subagents/{subagent_id}Delete a subagent
GET/agents/{agent_id}/transitionsFetch the whole flow: every node and every transition
POST/agents/{agent_id}/transitionsCreate a transition
PUT/agents/{agent_id}/transitions/{transition_id}Update a transition
DELETE/agents/{agent_id}/transitions/{transition_id}Delete a transition

List Subagents

GET /agents/{agent_id}/subagents
curl -H "X-API-Key: voc_a1b2c3d4e5f6..." \
https://api.usevocals.com/api/v1/agents/{agent_id}/subagents

Response

[
{
"id": "8c2e5a10-99f4-4c1e-9a6b-2d7f0e4b8a13",
"agent_id": "9f8b7c6d-1234-4a5b-9c8d-abcdef012345",
"name": "billing",
"system_prompt": "You handle invoices and payments. Be precise with amounts.",
"llm_provider_id": null,
"tts_provider_id": null,
"voice_id": null,
"tts_config": null,
"opening_line": "Billing here, I can help with that.",
"opening_line_enabled": true,
"interruptible": null,
"barge_in_sensitivity": null,
"thinking_cue_enabled": null,
"transfer_enabled": null,
"transfer_destinations": null,
"included_integration_ids": null,
"included_custom_endpoint_ids": null,
"included_knowledge_entry_ids": null,
"entry_check": null,
"created_at": "2026-08-01T09:12:04Z",
"updated_at": "2026-08-01T09:12:04Z"
}
]

Create Subagent

POST /agents/{agent_id}/subagents

Only name is required. Every other field is an override you may leave out.

curl -X POST \
-H "X-API-Key: voc_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{
"name": "billing",
"system_prompt": "You handle invoices and payments. Be precise with amounts.",
"opening_line": "Billing here, I can help with that.",
"barge_in_sensitivity": "high"
}' \
https://api.usevocals.com/api/v1/agents/{agent_id}/subagents

Returns 201 Created with the stored subagent.

Update Subagent

PUT /agents/{agent_id}/subagents/{subagent_id}

Partial: only the fields you send are changed. Sending a field as null clears that override, so the subagent goes back to inheriting the main agent's value.

curl -X PUT \
-H "X-API-Key: voc_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{"voice_id": null}' \
https://api.usevocals.com/api/v1/agents/{agent_id}/subagents/{subagent_id}

Delete Subagent

DELETE /agents/{agent_id}/subagents/{subagent_id}

Returns 204 No Content. Past calls keep their record of the subagent, so historic transcripts and analytics stay attributable to it.

Get the Flow

GET /agents/{agent_id}/transitions

Returns the whole flow in one call - every node, including the main agent, and every transition between them. tts_vendor is the voice provider each node actually speaks with once inheritance is resolved, so you can spot a handoff that changes voice vendor mid-call.

curl -H "X-API-Key: voc_a1b2c3d4e5f6..." \
https://api.usevocals.com/api/v1/agents/{agent_id}/transitions

Response

{
"nodes": [
{ "node_id": null, "name": "Reception", "tts_vendor": "elevenlabs" },
{
"node_id": "8c2e5a10-99f4-4c1e-9a6b-2d7f0e4b8a13",
"name": "billing",
"tts_vendor": "elevenlabs"
}
],
"transitions": [
{
"id": "b7d1f0a2-3c44-4e91-8a0d-1f2e3d4c5b6a",
"agent_id": "9f8b7c6d-1234-4a5b-9c8d-abcdef012345",
"source_subagent_id": null,
"target_subagent_id": "8c2e5a10-99f4-4c1e-9a6b-2d7f0e4b8a13",
"trigger_type": "model_judgement",
"trigger_config": { "condition": "the caller wants to pay an invoice" },
"position": 0,
"fire_count": 42,
"last_fired_at": "2026-08-28T16:03:51Z",
"created_at": "2026-08-01T09:14:22Z",
"updated_at": "2026-08-01T09:14:22Z"
}
]
}

fire_count and last_fired_at are what tell you which routes callers actually take, and which transition never fires and should be reworded or removed.

Create Transition

POST /agents/{agent_id}/transitions
curl -X POST \
-H "X-API-Key: voc_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{
"source_subagent_id": null,
"target_subagent_id": "8c2e5a10-99f4-4c1e-9a6b-2d7f0e4b8a13",
"trigger_type": "model_judgement",
"trigger_config": { "condition": "the caller wants to pay an invoice" },
"position": 0
}' \
https://api.usevocals.com/api/v1/agents/{agent_id}/transitions

Returns 201 Created. To let the caller come back, add the mirror transition with source_subagent_id set to the subagent and target_subagent_id set to null.

Update Transition

PUT /agents/{agent_id}/transitions/{transition_id}

Partial, and the whole transition is re-checked afterwards: changing only trigger_type still verifies the trigger against the transition's stored source node.

Delete Transition

DELETE /agents/{agent_id}/transitions/{transition_id}

Returns 204 No Content. The fire history stays on the calls that recorded it.

Errors

StatusMeaning
403 ForbiddenThe agent is shared with you read-only and you attempted a write
404 Not FoundThe agent is not yours and not shared with you, or the subagent/transition does not exist
422 Unprocessable EntityValidation failed - a duplicate name, a name matching the main agent's, a cap reached, a node from another agent, or a malformed trigger. The reason is in detail.