ElevenLabs setup¶
How the fleet described in The subagents is actually configured in this repository, and how it gets deployed.
One agent, thirteen subagent nodes¶
There is exactly one agent in the ElevenLabs account:
Questionnaire Intake Agent. The subagents are not separate agents — they
are override_agent nodes inside that agent's workflow, and the workflow graph
is what moves the call from one to the next.
elevenlabs/
├── agents.json the one agent, and its ID once pushed
├── tools.json custom tools (none yet)
└── agent_configs/
└── questionnaire-intake-agent.json the agent, workflow included
That single file holds everything: the shared prompt, the voice and ASR settings, and the fifteen-node workflow. There is no per-subagent config file and no linking step.
Always invoke the CLI through bunx
Not npx, and not a global install. See CLAUDE.md at the repository
root. The CLI must also be run from inside elevenlabs/, since that is
where agents.json lives.
The workflow¶
workflow.nodes and workflow.edges are both objects keyed by ID, not
arrays. Fifteen nodes:
| Node ID | Type | Label |
|---|---|---|
start_node |
start |
— |
intro |
override_agent |
Intro |
section_01 … section_11 |
override_agent |
One per section (ten of them) |
stop_and_refer |
override_agent |
Stop and refer |
handover |
override_agent |
Handover |
end_node |
end |
— |
Each override_agent node carries:
| Field | Holds |
|---|---|
label |
What shows in the workflow editor |
additional_prompt |
This subagent's own instructions, appended to the shared prompt |
additional_knowledge_base |
The pages this subagent loads — see the load map |
entry_behavior |
wait_for_user for the intro, generate_immediately for the rest |
position |
{x, y} so the editor lays out cleanly |
The shared scribe rules live once in the agent's base
conversation_config.agent.prompt.prompt, not thirteen times. A node's
additional_prompt is appended to it, which is what makes a subagent a
specialisation of the interviewer rather than a fresh one.
Edges¶
Twenty-six of them. Each has a source, a target, and a forward_condition
that is either {"type": "unconditional"} or
{"type": "llm", "condition": "…"}.
"section_02_to_section_03": {
"source": "section_02",
"target": "section_03",
"forward_condition": {
"type": "llm",
"condition": "Section 2 is complete and a break has been offered."
}
}
The eleven red-flag edges also carry a backward_condition, which is how
de-escalation is expressed without a second edge:
"section_02_to_stop": {
"source": "section_02",
"target": "stop_and_refer",
"forward_condition": {
"type": "llm",
"condition": "An answer in section 2 matches the stop and refer immediately list…"
},
"backward_condition": {
"type": "llm",
"condition": "The trigger was checked against the red flags reference and does not qualify, so section 2 resumes where it left off."
}
}
Every question-asking node has one of these, so a red flag can be caught anywhere in the call and, if it turns out not to qualify, control returns to exactly the section it came from.
Fifteen nodes exceeds the usual guidance
ElevenLabs suggests keeping workflows to four to seven nodes. This one is deliberately larger: one node per section is the context-isolation strategy, and collapsing sections together would undo it. See why a fleet.
Deploying¶
One pass. There are no cross-agent IDs to resolve, so the two-step deploy the old fleet needed is gone.
Preview first with --dry-run. Note that --dry-run does not validate against
the server schema — it only reports which agents would be sent.
The knowledge base¶
Every node carries its own pages explicitly, and inherits nothing. Two fields do that job together:
| Field on the node | Set to | Effect |
|---|---|---|
conversation_config.agent.prompt.knowledge_base |
[] |
Inherit nothing from the agent |
additional_knowledge_base |
Its own pages, listed one by one | The node's entire knowledge base |
The agent's own knowledge_base is [] as well, so there is nothing to
inherit even by accident. A subagent sees exactly the pages named against it in
the load map — never a neighbouring section's.
"conversation_config": {
"agent": { "prompt": { "knowledge_base": [] } }
},
"additional_knowledge_base": [
{
"type": "file",
"id": "AOekAr0fqiJvLIxCfWbn",
"name": "section-02-health-history",
"usage_mode": "prompt"
},
{
"type": "file",
"id": "834jHRo5jDGXEUoEnGhh",
"name": "ref-core-rules",
"usage_mode": "prompt"
}
]
Seventeen documents are uploaded — the ten client-facing section pages and the seven reference pages subagents load. Upload with:
curl -X POST "https://api.elevenlabs.io/v1/convai/knowledge-base/file" \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
-F "file=@docs/sections/02-health-history.md;type=text/markdown" \
-F "name=section-02-health-history"
Attach documents through the config, not a PATCH
workflow.nodes replaces rather than merges, so a partial PATCH that
sends one node drops the other fourteen — the API rejects it for having no
start node. Edit the config file and agents push; the CLI always sends
the workflow whole.
usage_mode is prompt, and RAG stays off
rag.enabled is false and every document is attached with
"usage_mode": "prompt", so a section page is injected whole rather than
retrieved against.
This is a correctness decision. Retrieval optimises for relevance; asking a
questionnaire needs completeness — every question, in order, with its
conditional logic intact. A question that is not retrieved is silently
never asked, and the handover record cannot then tell that apart from
skipped — not triggered. The full reasoning, and the one narrow
exception, are recorded in CLAUDE.md.
Each node holds one section — 12.8 KB at worst, ~3.5k tokens — which is what makes injecting it whole affordable. That is the context budget doing its job: the workflow split already solved the problem RAG would otherwise be solving.
Safety-critical material never goes in the knowledge base at all. The scribe rules and the stop-and-refer triggers live in the agent's base prompt, where they are present on every turn rather than depending on a retriever.
Re-upload when a question changes
The site stays the source of truth only if the uploaded copy tracks it. Reword a question here, re-upload that section's page, and the next call asks the new wording.
Placing a call¶
One agent means one entry point — there is no risk of dialling into the middle of the questionnaire.
curl -X POST "https://api.elevenlabs.io/v1/convai/twilio/outbound-call" \
-H "xi-api-key: $ELEVENLABS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "<the Questionnaire Intake Agent id>",
"agent_phone_number_id": "<linked number id>",
"to_number": "+972500000000",
"conversation_initiation_client_data": {
"dynamic_variables": {
"client_name": "Dana",
"practice": "the practice",
"coach": "Inbal",
"form_version": "2026-08"
}
}
}'
Those four dynamic variables seed the call; everything else accumulates as the
workflow runs. They are declared under dynamic_variable_placeholders so a
test call still works when they are not supplied.
Language¶
The agent is set to "language": "he", with the questionnaire pages in English
— each subagent reads the English page and asks in Hebrew.
Per CLAUDE.md, all prompt and first_message strings in the config are
written in English regardless of the spoken language. Change language in the
config, not the strings, if the agent should speak something else.
Voice settings¶
Set once on the agent and inherited by every node, which is what keeps the call sounding like one interviewer rather than fifteen.
| Setting | Value | Why |
|---|---|---|
voice_id |
one voice | Fifteen nodes, one apparent interviewer |
model_id |
eleven_v3_conversational |
Built for turn-taking |
asr.provider |
scribe_realtime, high quality |
Numbers and medication names have to survive |
turn_timeout |
10s | Longer than default: these questions need thinking time |
silence_end_call_timeout |
60s | Ends a dead call without chasing |
max_duration_seconds |
7200 | A full intake runs ~90 minutes, with breaks |
temperature |
0 | A scribe should not be creative |
One voice is a presentation choice, not a disclosure choice
The client is told once, by the intro subagent, that they are talking to an automated system. A single voice across the workflow stops the call feeling broken; it must never be used to imply a human is on the line.