Voice Agent Handoff Made Simple in jambonz

Dave Horton

Voice Agent Handoff Made Simple in jambonz

Every production voice agent eventually needs to bring in a human, and there are three possible ways to do that:

jambonz has always had best-in-class support for telephony and all three methods have been supported for a long time. However, prior to v11, orchestrating a handoff required building a custom transfer workflow, and it could get a bit complicated.

So in v11, we’ve introduced a simple declarative approach to defining the handoff approach you want for your voice agent.

Why Handoff Usually Means Writing a Workflow

On most voice AI platforms, “transfer to a human” is not a feature. It’s a project. A warm transfer in particular means orchestrating it yourself: create a consultation room, place an outbound call to the specialist, move participants between rooms, tear it all down when someone hangs up. LiveKit, for example, exposes this as a WarmTransferTask built from CreateSIPParticipant and MoveParticipant primitives. Powerful, but it’s plumbing you own, test, and maintain.

jambonz collapses that into a single declarative handoff block on the agent verb (if you are building a cascaded voice pipeline) or the s2s verb if you are building a speech-to-speech voice agent.

You describe the outcome you want; jambonz injects a transfer_to_human tool for the model and runs the entire choreography when the model calls it: dialing, screening, briefing, bridging, and fallback.

The division of responsibilities is clear:

The Three Handoff Types in jambonz v11

All three are the same agent(or s2s) verb with a different handoff block. The full, runnable versions of each (built on @jambonz/sdk, ≥ 0.8.3) live in the transfer-apps examples.

1. Blind Transfer

The caller is connected directly to the human, with no briefing step. Fastest option; best for simple routing where context handoff doesn’t matter. Here’s the whole agent, so you can see where handoff fits:

session
  .agent({
    stt: { vendor: 'deepgram', language: 'en-US' },
    tts: { vendor: 'cartesia', voice: ttsVoice },
    llm: {
      vendor: 'openai',
      model: 'gpt-4.1-mini',
      llmOptions: { systemPrompt },
    },
    turnDetection: 'krisp',
    handoff: {
      mode: 'blind',
      blindMethod: 'refer',                       // 'refer' (SIP REFER) or 'dial' (bridged call)
      callerId,
      disposition: { onNoAnswer: 'return', onBusy: 'return', onFailure: 'return' },
      actionHook: '/transfer-done',
    },
    eventHook: '/agent-event',
    actionHook: '/agent-done',
  })
  .send();

2. Warm Transfer (Private Briefing)

The caller is parked on hold while the voice agent calls the human and briefs them privately. The human is screened with a confirm gate (they press a digit to accept), and only then is the caller connected. Just the handoff block (the rest of the agent is identical):

session
  .agent({
    stt: { vendor: 'deepgram', language: 'en-US' },
    tts: { vendor: 'cartesia', voice: ttsVoice },
    llm: {
      vendor: 'openai',
      model: 'gpt-4.1-mini',
      llmOptions: { systemPrompt },
    },
    turnDetection: 'krisp',
    handoff: {
      mode: 'warm',
      callerPresent: false,        // caller is parked; doesn't hear the brief
      target: [{ type: 'phone', number: target }],
      callerId: '+15085550101',    // optional: caller ID on outbound call to human agent
      brief: {
        template:
          'In one sentence, tell the specialist why the caller is calling, then say ' +
          'exactly: "Press one to connect, or hang up to decline."',
      },
      confirm: { prompt: 'Press one to connect.', digit: '1' },
      onHoldHook: '/on-hold',                        // what the parked caller hears during the brief
      disposition: { onNoAnswer: 'return', onBusy: 'return', onDecline: 'return', onFailure: 'return' },
      actionHook: '/transfer-done',
    },
    eventHook: '/agent-event',
    actionHook: '/agent-done',
  })
  .send();

3. Conferenced (Three-way) Handoff

The voice agent brings the human onto the call while the caller stays on the line, so the caller hears the introduction happen. The call continues as a three-way conversation until the agent drops off. The only difference from a warm transfer is a single property: callerPresent: true.

session
  .agent({
    stt: { vendor: 'deepgram', language: 'en-US' },
    tts: { vendor: 'cartesia', voice: ttsVoice },
    llm: {
      vendor: 'openai',
      model: 'gpt-4.1-mini',
      llmOptions: { systemPrompt },
    },
    turnDetection: 'krisp',
    handoff: {
      mode: 'warm',
      callerPresent: true,  // caller joins the three-way and hears the brief
      target: [{ type: 'phone', number: target }],
      callerId,                                      
      brief: 'auto',        // let the LLM create the introduction
      disposition: { onNoAnswer: 'return', onBusy: 'return', onDecline: 'return', onFailure: 'return' },
      actionHook: '/transfer-done',
    },
    eventHook: '/agent-event',
    actionHook: '/agent-done',
  })
  .send();

No conference verb, no REST api call to create a second call leg, no second WebSocket endpoint. jambonz joins the caller into the three-way and lets the voice agent introduce them before dropping out of the conference.

For details on each of the handoff properties please review our docs.

Call Transfer When Not Building a Voice Agent

Call transfer is also available for apps that do not implement a voice agent. Perhaps you are building a simple CPaaS-type callflow using say, gather etc and you want to do a call transfer?

No problem, in that case you can simply use the standalone transfer verb. The same options described above for the agent handoff are available in the transfer verb.

What Happens When the Handoff to Human Fails?

Of course, the call handoff / transfer to human can fail: the person targeted by the handoff may decline the call, the call attempt itself might fail for various reasons, etc. What control do you have over scenarios where this happens?

This is where you use the disposition object. A handoff can fail in a handful of distinct ways, and you configure what should happen for each one independently:

Each of these outcomes can be mapped to one of three values (return, voicemail, or hangup), and if you don’t specify one, it defaults to return.

The important thing to understand is that when a handoff fails, the caller is still on the line; they simply never got connected to a human. The disposition is your instruction for what to do with them at that point.

Setting a disposition to voicemail routes the caller straight to the voicemailUrl you provide (a SIP URI or an HTTP endpoint that returns verbs); they leave the AI conversation behind and land in your voicemail application flow, as defined by the jambonz application which is served from that URL.

Setting it to hangup ends the call outright. And setting it to return (the default) hands the caller back to the AI agent: jambonz re-attaches the live LLM session, and the agent picks the conversation right back up, so it can gracefully recover with something like “It looks like everyone’s busy right now. Is there anything else I can help you with?”

So the choice comes down to what you want the caller’s experience to be when no human is available. Use return for the failure cases where the AI should stay in control and keep helping; use voicemail when you’d rather let them leave a message; and use hangup when there’s genuinely nothing more to offer. A common pattern mixes them: send unanswered or busy calls to voicemail, but let declines and errors fall back to the agent:

session
  .agent({
    ...
    handoff: {
      mode: 'warm',
      target: [{type: 'phone', 'number': '+15551234567' }],
      disposition": {
        onNoAnswer: 'voicemail',
        onBusy: 'voicemail',
        onDecline: 'return',
        onFailure: 'return',
        voicemailUrl: '/voicemail'
    }
  }
}

How Does jambonz Compare to Other Voice AI Platforms?

Feature jambonz Other voice AI platforms
Blind (cold) transfer Built in Built in, or via SIP REFER
Warm transfer Built in Usually via transfer APIs
Conferenced handoff Built in Often requires custom implementation
Setup Voice agent configuration Platform config or custom code

Capabilities vary by platform. The conferenced handoff is the one that stands out: bringing a human into a live three-way with the caller, while the agent introduces them and then steps away, is rarely a built-in on other platforms. In jambonz it’s one property.

Getting Started with jambonz

Handoff is available now in jambonz v11. See the voice agent documentation for how to configure blind, warm, and conferenced transfer on your agent, and the transfer-apps examples for complete, deployable apps for all three.

Frequently asked questions

What is the difference between blind (cold) transfer and warm transfer in voice AI?

A blind transfer connects the caller straight to the destination with no briefing. It's the fastest option, best for simple routing. A warm transfer puts the caller on hold while the voice agent reaches the human first and briefs them privately, so the human has context before the caller is connected.

What is a conferenced handoff?

A conferenced (three-way) handoff brings the human onto the call while the caller stays on the line, so the caller hears the introduction happen. It continues as a three-way conversation until the voice agent drops off. In jambonz this is a warm transfer with callerPresent set to true.

Do I need to write custom code for voice agent handoff in jambonz?

No. All three handoff types are declared on the agent verb's handoff property. jambonz injects a transfer_to_human tool the model can call and runs the entire transfer choreography for you: dialing, screening, briefing, bridging, and fallback. There is no transfer workflow to build.

Does the LLM choose which type of transfer to use?

No. You configure the handoff type (blind, warm, or conferenced) on the agent. The LLM only decides when to hand off, by calling the injected transfer_to_human tool once it understands the caller's need.

What happens if the human doesn't answer or declines the transfer?

Each outcome (no answer, busy, declined, or failure) has a configurable disposition: return the caller to the voice agent, send them to voicemail, or hang up. By default the caller is returned to the agent, which resumes the conversation.

What does the transfer verb do in jambonz?

The transfer verb provides a general, simple and declarative means for performing a call transfer to another party from within any jambonz application. When using the agent or s2s verbs the transfer function is baked into the 'handoff' property, in all other cases you can use the transfer verb.