Moving from 0.x to 1.0
Move a self-hosted 0.x voice worker to the 1.0 SDK, with the 0.8 release telling you exactly what to change.
What you'll build
Your 0.x voice agent, running on the 1.0 SDK. In 0.x your process was the whole call: it held a LiveKit connection, ran the speech pipeline and called your functions. In 1.0, VoiceLayer runs every call and your worker answers the functions it needs over one outbound connection. You gain the Playground, VoiceLayer numbers and every guard for code agents. You give up the raw audio, and a handler can no longer keep call state in memory between invocations.
Prerequisites
- Your 0.x agent, upgraded to
@voicelayer/sdk0.8. - Node.js 22 or later.
- Access to your workspace's dashboard, to add provider keys and create a worker key.
Step 1: Read what the bridge prints
Upgrade to 0.8 and start your worker. Each time it starts, it prints what 1.0 would refuse (error), what behaves differently (change) and what is worth knowing (note):
[voicelayer] agent "venue-escalation" needs changes before @voicelayer/sdk 1.0
error on_call_unsupported (onCall): onCall can't run on the VoiceLayer pool — move its opening line to `greeting` …
error models_vad_unsupported (models.vad): models.vad can't be set: VoiceLayer always runs its own VAD
change tool_real_values (tools): code tool lookup_resident: in 0.x a code tool is handed the caller's real values …
change tool_bindings_implicit (toolBindings): in 0.x an agent attaches every enabled dashboard tool …
note start_runs_the_runner (start): `.start(import.meta.url)` boots a LiveKit worker in 0.x; in 1.0 …Run the same check without starting anything:
import { checkMigration, formatMigrationReport } from '@voicelayer/sdk';
console.log(formatMigrationReport(checkMigration(myAgentConfig)));Step 2: Move provider keys out of your process
Remove the plugin packages and the LIVEKIT_*, OpenAI and Deepgram variables from the worker. Add those provider accounts in the dashboard under Settings → Provider accounts instead. Two keys replace them:
| Variable | Used by | Key |
|---|---|---|
VOICELAYER_API_KEY | The command line (laptop, CI) | A secret API key |
VOICELAYER_WORKER_KEY | The running worker | A worker key (vlw_…) for one environment |
Step 3: Switch the import
// before (0.x)
import { defineAgent } from '@voicelayer/sdk';
export default await defineAgent({ name: 'support', prompt: '…' }).start(import.meta.url);
// after (1.0; from 0.8 on the /v1 subpath)
import { defineAgent } from '@voicelayer/sdk/v1';
export default defineAgent({ name: 'support', prompt: '…' });Then check the manifest offline: npx voicelayer manifest ./agent.ts --check.
Step 4: Update models
| 0.x | 1.0 |
|---|---|
deepgram.stt({ model }), openai.llm({ model }), … | Unchanged |
models.llm: 'gpt-4o' (every string ran on OpenAI) | 'gpt-4o' is OpenAI's; 'anthropic/…' and 'google/…' run on that provider |
wrap(...), a plugin instance, a custom provider | Not supported: use a helper or a provider/model string |
models.vad: silero.vad() | Delete it: VoiceLayer runs its own |
models.turnDetector: livekitTurn.english() | models.turnDetection: 'model' (or 'vad') |
apiKey / baseURL in a helper | Refused: add the key under Provider accounts |
connector.llm({ onQuery }) | onQuery on the agent (see Brains in code) |
connector.llm({ url, apiKey }) | A brain connector, bound to the agent |
Step 5: Update ctx and onCall
say, respond, ask, handoff, endCall, sendDtmf, recordEvent and memory keep their signatures. Each is now a request to VoiceLayer, checked against the system.* capability you declare in capabilities.
| 0.x | 1.0 |
|---|---|
ctx.call, ctx.process, ctx.channel | Snapshots, refreshed for each invocation |
| Call state in module variables | ctx.metadata (at most 64 KiB), visible to the next handler on any process |
ctx.onDtmf(handler) | A top-level onDtmf(event, ctx) (works in 0.8) |
ctx.room | Not available yet |
ctx.livekit, ctx.adapter, ctx.audio.* | Removed |
onCall | Refused: put the opening line in greeting (spoken as written) or in the prompt |
Step 6: Respect hook deadlines
In 0.x every hook ran in your process with no limit. In 1.0 a hook the call waits on has a deadline and a safe default, so a slow worker never hangs a call:
| Hook | Deadline | If it is late |
|---|---|---|
A tool's run | 10 s | The model gets a failed result |
trigger.on / trigger.run | 1 s / 2 s | No match / the turn continues |
process.completeWhen / onField / onComplete | 2 s / 2 s / 5 s | Not complete / the field stays / it stays complete |
handoff.briefing (a function) | 2 s | VoiceLayer's own briefing |
participants.attributes / endCall.onEnd | 1 s / 2 s | No attributes / the call ends |
onUtterance, onFieldCaptured, onCallEnd, onDtmf | None | Notifications, delivered at least once and never waited on |
Step 7: Opt tools in to real values
In 0.x a code tool got the caller's real values. In 1.0 it gets placeholders unless it opts in, and each release is recorded on the call:
...defineTool({
name: 'lookup_resident',
description: 'Find the resident calling by their phone number',
input: { phone: 'string' },
sendRealValues: true, // this tool gets the real number; every other one gets <PII_PHONE_1>
run: async ({ phone }) => residents.byPhone(phone)
})Step 8: Private networks, tool bindings and transfers
- VoiceLayer never reaches a private address, so
allowPrivateNetworkis gone. Call private services from adefineToolhandler, which runs on your network. toolBindingsis explicit: an empty list means no dashboard tools, and leaving it out is refused.ctx.handoff(to)only dials destinations the manifest declares or the workspace's transfer policy allows. Ask your workspace owner to allow the numbers your code looks up. See Transfers.
Step 9: Deploy
Create a worker key per environment, then deploy in the fixed order: upload, start the new workers beside the old, publish, let the old drain. See Voice agents in code.
npx voicelayer deploy ./agent.ts --env prd --no-publish # upload; writes voicelayer.lock.json
# roll out your workers with the new lock file
npx voicelayer publish --env prd --min-workers 2 # publish once 2 new workers are connectedRoll back with voicelayer publish --manifest <older hash> while the old workers are still running.
Test it
import { assertAgent, invoke, testContext } from '@voicelayer/sdk/testing';
import agent from './agent.js';
assertAgent(agent); // the manifest builds and passes the upload's checks
const ctx = testContext({ call: { callerId: '+15550100' } });
console.log(await invoke(agent, 'tool.lookup_resident', { phone: '+15550100' }, ctx));Then run npx voicelayer dev ./agent.ts and call the agent from the Playground.
Common errors
| Problem | Fix |
|---|---|
on_call_unsupported | Move the opening line to greeting or the prompt. |
models_vad_unsupported | Delete models.vad. |
A tool gets <PII_PHONE_1> instead of a number | Add sendRealValues: true to that tool. |
| A transfer is refused | The destination is not declared or allowed by the transfer policy. |
When you need the whole call
If you need raw audio, a custom pipeline or your own conversation loop, the call runs in your own runtime on your own LiveKit project, and VoiceLayer hands it over at SIP. The call then leaves VoiceLayer's guard. This is set up on request and is not part of the SDK.