Migrate from ElevenLabs
Move an agent from ElevenLabs Agents to Curb in five steps. There is no importer: you export the agent’s configuration and rebuild it as code, then test it and move your phone numbers one at a time. Your ElevenLabs agent keeps answering until the last number has moved.
A coding agent connected to KugelAudio can do most of the rebuild. Give it the exported files and this page.
If you only use ElevenLabs for text to speech, you do not need this page: see ElevenLabs API compatibility.
Before you start
Read what has no equivalent yet first. If your agent depends on your own SIP trunk or on one of the other features listed there, plan for that before you rebuild anything.
1. Export your agent
Export into a folder of its own, outside your Curb project, because curb deploy uploads
the project’s source. Install the ElevenLabs CLI
and pull the agent, its tools and its tests:
npm install -g @elevenlabs/cli
mkdir elevenlabs-export && cd elevenlabs-export
elevenlabs auth login
elevenlabs agents init
elevenlabs agents pull --agent AGENT_ID
elevenlabs tools pull
elevenlabs tests pull
The configs land in agent_configs/, tool_configs/ and test_configs/. Command names
change between CLI versions, so check elevenlabs --help if one is refused.
Then fetch what the configs only point to. Each knowledge base document has an ID under
conversation_config.agent.prompt.knowledge_base, and its text comes from the
document content endpoint:
curl -H "xi-api-key: $ELEVENLABS_API_KEY" \
https://api.elevenlabs.io/v1/convai/knowledge-base/DOCUMENT_ID/content
Past calls make the best tests, so fetch some from the
conversations endpoints.
The list gives you their IDs, and each conversation gives you its transcript, its tool calls
and its analysis. /audio on a conversation returns its recording.
curl -H "xi-api-key: $ELEVENLABS_API_KEY" \
"https://api.elevenlabs.io/v1/convai/conversations?agent_id=AGENT_ID&page_size=100"
curl -H "xi-api-key: $ELEVENLABS_API_KEY" \
https://api.elevenlabs.io/v1/convai/conversations/CONVERSATION_ID
Transcripts and recordings hold what your callers said, so keep them out of your repository. Past calls do not move to KugelAudio. Export whatever you must keep before you close your ElevenLabs account.
2. Rebuild it in Curb
Create the project as in the quickstart:
npm create curb.js my-agent
cd my-agent && npx curb login
Then carry the configuration over. The authoring reference covers everything named here.
| ElevenLabs | Curb |
|---|---|
| System prompt | <SystemPrompt> in src/agent/agent.tsx |
| First message | firstTurn: { say } in defineAgent for fixed words, firstTurn: "agent" for a greeting the model writes |
| LLM | The llm prop: kugelaudio.llm(), anthropic(model) or openai(model) |
| Custom LLM | openaiCompatible({ baseURL, model }) |
| Voice | The tts prop, see step 3 |
| Speech recognition keywords | boostedPhrases on kugelaudio.stt() |
| Turn eagerness | turnDetectionLevel on kugelaudio.stt() |
| Pronunciation dictionaries | Dictionaries, selected with dictionaryIds |
| Webhook tool | <Tool> whose onSubmit calls your API (recipe) |
| MCP server | <Mcp> |
| Client tool | useSend() and useMessage() |
end_call, skip_turn | Built in: do not write your own |
transfer_to_number | <TransferCallTool> or useTransferCall() (forward a call) |
transfer_to_agent | A skill in the same project |
| Dynamic variables | useAgentInfo() for what the call knows, useClient() for what your page sent |
| Knowledge base | Files in context/, rendered with <Text src="..." /> |
| Evaluation criteria | goals with judge or tool (goals) |
| Data collection | A <Tool> that captures the fields and sends them with send.toHost (recipe) |
| Web widget | Web widget |
Keep your keys out of the code. Put them in .env, then send them to the agent with
npx curb secrets push.
Turn a workflow into skills
A workflow is a graph the conversation moves through. Curb has no graph: each part of the conversation is a skill, and React state decides what the model can see and do.
| Workflow part | Curb |
|---|---|
| Subagent node | A skill: src/agent/<name>/skill.tsx, holding that node’s prompt, documents and tools |
| Subagent’s own LLM or voice | llm, stt or tts on the skill’s <Skill> link |
| Edge with an LLM condition | The description of the <Skill> link, written as a rule for when to use it |
| Edge with an expression | State: render the tool, text or <Skill> link only when the condition holds (shared state) |
| Ordered steps | <Procedure> with <Step> (pattern blocks) |
| Update state node | An agent store (createAgentStore), set from a tool’s onSubmit |
| Dispatch tool node | The call itself, inside the onSubmit of the tool before it |
| Agent transfer node | A <Skill> link to the other agent’s rebuilt skill |
| Transfer to number node | <TransferCallTool> in that skill |
| End node | end_call, with the reason in the skill’s instructions |
A model picks its own tools, so a tool you render is not guaranteed to run. When a step must
always happen, do it in code, then check the path with called() in a
scenario.
Move the knowledge base
Curb has no vector search. A file you render with <Text src="..." /> reaches the model in
full while its skill is loaded, so split documents by topic and put each one in the skill
that needs it. For a knowledge base too large for that, give the agent a <Tool> or an
<Mcp> server that searches it.
3. Choose a voice
ElevenLabs voice clones cannot be exported, so pick a KugelAudio voice or clone the voice again, with the speaker’s consent.
To compare, run the same agent on your machine with each voice. Set ELEVEN_API_KEY in
.env and start npm run dev:
import { elevenlabs, kugelaudio } from "curb/providers";
// Your current voice, for the comparison on your machine:
const current = elevenlabs.tts({ voice: "ELEVENLABS_VOICE_ID" });
// The voice you are moving to:
const next = kugelaudio.tts({ voice: 1887, language: "de" });
Pass one of them to <Agent tts={...}>, and talk to the agent with each.
Deploy with kugelaudio.tts() and kugelaudio.stt(). elevenlabs.tts() and
elevenlabs.stt() open a direct connection, and a hosted agent’s traffic has to leave
through its egress proxy, so
they work on your machine but not in a hosted agent.
4. Test it before a caller reaches it
Check that the project builds, then try one turn:
npx curb check
npx curb say "I want a refund for a damaged order"
Turn your ElevenLabs tests and past calls into scenarios for
curb test. Pick calls your old agent handled well and calls it handled
badly, and use the analysis of each conversation to find them.
| ElevenLabs test | Curb scenario |
|---|---|
| Simulation | caller, with a goal and what the caller knows |
| Next reply | turns, with the exact lines |
| Tool call | expect, with called("tool_name", { ...input }) |
| Mocked tool | fakes |
// src/agent/refund.scenario.ts
import { scenario } from "curb/testing";
export default scenario({
name: "refund for a damaged order",
caller: {
goal: "Get a refund for a damaged order",
knows: "Order number 48213, name Max Mustermann",
},
fakes: { create_refund: { ok: true, message: "Refund created." } },
expect: ({ called }) => [called("create_refund")],
});
A tool runs for real in every scenario, so fake anything that books, sends or deletes.
npx curb test
Then listen. Scenarios run on text, so they cannot show how the agent sounds or how it hears your callers. Deploy the agent, call it on the test line, and try the names, numbers and spellings that matter to your business.
To check how real calls are heard, transcribe a few exported recordings with speech to text, including noisy calls and spelled names.
5. Move your phone numbers
Move one number at a time, starting with your quietest. Connect it, watch its calls on the agent’s Calls page and its results on the Goals tab, then move the next one. Keep the ElevenLabs agent until the last number has moved and stayed quiet.
How you move a number depends on how it reaches ElevenLabs today:
| Number today | Move it | Move it back |
|---|---|---|
| Twilio, imported into ElevenLabs | Connect Twilio and choose the number | Disconnect puts back the settings it had |
| Twilio, on an Elastic SIP trunk | Remove the number from the trunk in Twilio, then connect it as above. Calls reach no agent between those two steps, so do it at a quiet hour | Disconnect, then add the number to the trunk again in Twilio: the trunk is not part of what KugelAudio saves |
| Telnyx | Connect Telnyx and choose the number | Disconnect puts back the settings it had |
| Another carrier, or your own phone system | Not available yet: forward it to a number you bought from KugelAudio | Remove the forwarding |
If your agent also calls people, those calls need a number bought from KugelAudio, because your own Twilio and Telnyx numbers cannot place calls yet. See outbound calls.
What has no equivalent yet
| ElevenLabs | Today |
|---|---|
| Your own SIP trunk | Not available; forward the number as in step 5 |
| Vector search over a knowledge base | Use a search API behind a <Tool> or <Mcp>, as in step 2 |
| Post-call webhooks | Send what your systems need from a <Tool> during the call, or download calls from the Calls page |
language_detection | Set language: "de,en" on kugelaudio.stt(), and give each language a skill with its own tts on the <Skill> link |
play_keypad_touch_tone | None: the agent cannot send keypad tones |
voicemail_detection with a message | None: an outbound call that reaches a mailbox reports VOICEMAIL, and the agent cannot leave a message on purpose |
| None | |
| Configured guardrails | Write the rule into the prompt, check it again in the tool, and use confirm on actions that need the caller’s yes |
Next steps
- Agents and skills: how a Curb project is built.
- Testing:
curb check,curb sayandcurb test.