How-to guide

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.

0.8 is the bridge release: it prints what to change while your agent keeps running as before. 0.x gets no releases after it, security fixes included, and keeps working until 2027-03-15. Move inside that window.

Prerequisites

  • Your 0.x agent, upgraded to @voicelayer/sdk 0.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:

VariableUsed byKey
VOICELAYER_API_KEYThe command line (laptop, CI)A secret API key
VOICELAYER_WORKER_KEYThe running workerA 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.x1.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 providerNot 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 helperRefused: 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.x1.0
ctx.call, ctx.process, ctx.channelSnapshots, refreshed for each invocation
Call state in module variablesctx.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.roomNot available yet
ctx.livekit, ctx.adapter, ctx.audio.*Removed
onCallRefused: 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:

HookDeadlineIf it is late
A tool's run10 sThe model gets a failed result
trigger.on / trigger.run1 s / 2 sNo match / the turn continues
process.completeWhen / onField / onComplete2 s / 2 s / 5 sNot complete / the field stays / it stays complete
handoff.briefing (a function)2 sVoiceLayer's own briefing
participants.attributes / endCall.onEnd1 s / 2 sNo attributes / the call ends
onUtterance, onFieldCaptured, onCallEnd, onDtmfNoneNotifications, 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 allowPrivateNetwork is gone. Call private services from a defineTool handler, which runs on your network.
  • toolBindings is 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 connected

Roll 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

ProblemFix
on_call_unsupportedMove the opening line to greeting or the prompt.
models_vad_unsupportedDelete models.vad.
A tool gets <PII_PHONE_1> instead of a numberAdd sendRealValues: true to that tool.
A transfer is refusedThe 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.

Next steps

On this page