Migrate from ElevenLabs

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.

ElevenLabsCurb
System prompt<SystemPrompt> in src/agent/agent.tsx
First messagefirstTurn: { say } in defineAgent for fixed words, firstTurn: "agent" for a greeting the model writes
LLMThe llm prop: kugelaudio.llm(), anthropic(model) or openai(model)
Custom LLMopenaiCompatible({ baseURL, model })
VoiceThe tts prop, see step 3
Speech recognition keywordsboostedPhrases on kugelaudio.stt()
Turn eagernessturnDetectionLevel on kugelaudio.stt()
Pronunciation dictionariesDictionaries, selected with dictionaryIds
Webhook tool<Tool> whose onSubmit calls your API (recipe)
MCP server<Mcp>
Client tooluseSend() and useMessage()
end_call, skip_turnBuilt in: do not write your own
transfer_to_number<TransferCallTool> or useTransferCall() (forward a call)
transfer_to_agentA skill in the same project
Dynamic variablesuseAgentInfo() for what the call knows, useClient() for what your page sent
Knowledge baseFiles in context/, rendered with <Text src="..." />
Evaluation criteriagoals with judge or tool (goals)
Data collectionA <Tool> that captures the fields and sends them with send.toHost (recipe)
Web widgetWeb 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 partCurb
Subagent nodeA skill: src/agent/<name>/skill.tsx, holding that node’s prompt, documents and tools
Subagent’s own LLM or voicellm, stt or tts on the skill’s <Skill> link
Edge with an LLM conditionThe description of the <Skill> link, written as a rule for when to use it
Edge with an expressionState: render the tool, text or <Skill> link only when the condition holds (shared state)
Ordered steps<Procedure> with <Step> (pattern blocks)
Update state nodeAn agent store (createAgentStore), set from a tool’s onSubmit
Dispatch tool nodeThe call itself, inside the onSubmit of the tool before it
Agent transfer nodeA <Skill> link to the other agent’s rebuilt skill
Transfer to number node<TransferCallTool> in that skill
End nodeend_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 testCurb scenario
Simulationcaller, with a goal and what the caller knows
Next replyturns, with the exact lines
Tool callexpect, with called("tool_name", { ...input })
Mocked toolfakes
// 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 todayMove itMove it back
Twilio, imported into ElevenLabsConnect Twilio and choose the numberDisconnect puts back the settings it had
Twilio, on an Elastic SIP trunkRemove 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 hourDisconnect, then add the number to the trunk again in Twilio: the trunk is not part of what KugelAudio saves
TelnyxConnect Telnyx and choose the numberDisconnect puts back the settings it had
Another carrier, or your own phone systemNot available yet: forward it to a number you bought from KugelAudioRemove 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

ElevenLabsToday
Your own SIP trunkNot available; forward the number as in step 5
Vector search over a knowledge baseUse a search API behind a <Tool> or <Mcp>, as in step 2
Post-call webhooksSend what your systems need from a <Tool> during the call, or download calls from the Calls page
language_detectionSet language: "de,en" on kugelaudio.stt(), and give each language a skill with its own tts on the <Skill> link
play_keypad_touch_toneNone: the agent cannot send keypad tones
voicemail_detection with a messageNone: an outbound call that reaches a mailbox reports VOICEMAIL, and the agent cannot leave a message on purpose
WhatsAppNone
Configured guardrailsWrite the rule into the prompt, check it again in the tool, and use confirm on actions that need the caller’s yes

Next steps