how to inject per call metadata for Inbound , inst...
# ask-ai
c
how to inject per call metadata for Inbound , instead of static dispatch rule
t
Thanks for reaching out, faiq. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you want to inject per-call metadata for inbound calls dynamically instead of relying on a static dispatch rule. You can do that — here are the practical options and how to use them so agents see call-specific data at runtime. What you can do (three approaches) • Use explicit agent dispatch with job metadata (recommended) ◦ When you create a dispatch (via the dispatch API), include a
metadata
field on the dispatch/job. That
metadata
can be any freeform string (commonly JSON) and will be available to the agent at runtime as
ctx.job.metadata
in the agent entrypoint. ◦ This lets you pass caller-specific info (caller ID, account id, routing context, etc.) per inbound call rather than baking it into a static rule. ◦ See: Agent dispatch / CreateAgentDispatchRequest for the
metadata
field. Agent dispatch • Inject metadata via SIP headers -> participant attributes (for SIP trunks) ◦ If the inbound call arrives over SIP, map SIP headers to participant attributes in your trunk/connector configuration (for example
headers_to_attributes
). Those attributes become part of the call/participant and can be consumed by the agent. ◦ You can also map attributes back to SIP headers (
attributes_to_headers
) when necessary. ◦ This is useful when your SIP provider (Five9, etc.) already sends custom headers with the call and you want those forwarded into LiveKit as per-call metadata. ◦ See: Five9 / headers mapping guide. Five9 integration guide • Use participant attributes in your room_config ◦ For non-SIP flows, set participant attributes on the incoming participant or set them programmatically when the call arrives. These attributes are accessible to the agent and can carry per-call context. ◦ This is a flexible method when you control the signaling or when another service in your flow can attach attributes at call time. ◦ See: Job lifecycle / participant attributes. Job lifecycle How the agent reads the data • For explicit dispatch jobs: read
ctx.job.metadata
inside the agent entrypoint / handler. • For participant attributes: read the participant attributes exposed to the agent in the job/room context. Quick decision guide • If you control the dispatch call: use explicit dispatch with
metadata
— simplest and most direct. • If call arrives from a SIP provider and carries custom SIP headers: use headers->attributes mapping. • If you need attributes attached after call setup (or from other services): set participant attributes programmatically. If you want, tell me: • Which inbound path you use (SIP trunk or other), • Whether you create dispatches via API or rely on automatic dispatch rules, and I’ll give a precise code/config example for that flow (API payload or trunk config). Sources: Agent dispatch | LiveKit Documentation | Job lifecycle | LiveKit Documentation | How to do call transfers with Five9 | LiveKit, with interactive elements
c
so If 3 people call at once, at the same number ? how it will work ?
t
Thanks — good question. I’m responding to how concurrent inbound calls to the same phone number are handled when you inject per-call metadata. Short answer • Each inbound call becomes its own job/participant and gets its own metadata. Even if three callers dial the same number at the same time, each call is isolated and the agent receives the metadata for that specific call. How it works (practical details) • When a call arrives it produces a distinct job/participant instance. You can attach per-call data in one of the ways we discussed: ◦ explicit dispatch with
metadata
on the job, ◦ mapping SIP headers into participant attributes, ◦ or setting participant attributes programmatically. • That per-call metadata is stored on the job/participant associated with that single call. Agents read the call-specific data from
ctx.job.metadata
(explicit dispatch) or the participant attributes for that job. • Concurrency: Multiple jobs may be created simultaneously for the same phone number; each job keeps its own metadata payload and state. There’s no cross-talk between the metadata of separate jobs. Routing/agent assignment considerations • If you use automatic/static dispatch rules, ensure your rules and agent capacity support parallel handling of multiple jobs. Static rules don’t mix metadata between calls — they just control routing. • If you use explicit dispatching via API, create a separate dispatch request per incoming call and include the correct
metadata
in each request so the agent gets the right context. • If your SIP provider sends headers, make sure the header-to-attribute mapping is applied per call (SIP headers are per-call), so each call’s attributes remain separate. Potential pitfalls to watch for • Shared identifiers: Don’t reuse a single job ID or shared global state for multiple simultaneous calls — give each call its own job/participant context. • Agent load: If many concurrent calls arrive to the same queue or phone number, ensure you have enough agent capacity or queue logic so calls don’t get dropped or misrouted. • Metadata size/format: Keep metadata reasonably sized (JSON string is common) and validate fields so agents can reliably parse per-call context. If you want, I can: • Show an example API payload for creating separate dispatch jobs with different
metadata
for three simultaneous calls. • Or show SIP trunk header->attribute config for per-call headers if you’re using a SIP provider. Which would you like: an API example or a SIP header mapping example? Also, are you using explicit dispatch or automatic/static dispatch rules today? , with interactive elements
c
warm transfer vs cold transfer . How to Implement .
t
Thanks — you want the difference between warm vs cold transfer and how to implement each in LiveKit. Below is a concise guide and the APIs/tasks to use. Warm transfer (brief human before connecting) • What it is: Caller is placed on hold while the transferring agent briefs the receiving agent; then the receiver is joined to the caller. • How to implement: a. Play a hold/announcement to the caller and disable their audio if desired. b. Create a private consultation room and dial the receiving human (use
CreateSIPParticipant
). c. Pass full context (conversation history) via
chat_ctx
so the receiving agent sees the job context. d. Use
WarmTransferTask
to orchestrate hold, briefing, and move the human into the caller room. e. Disconnect interim agents so only the human and caller remain. • Key APIs/examples: ◦
WarmTransferTask
(provides orchestration) ◦
CreateSIPParticipant
to dial the supervisor ◦ Pass
chat_ctx
/
ctx.job.metadata
for context • When to use: complex issues or when you want the receiving agent pre-briefed. Cold transfer (straight handoff) • What it is: Caller is immediately transferred to the target destination (no private briefing). • How to implement: a. Use SIP REFER or the transfer API to move the call. b. Call
TransferSIPParticipant
/
api.sip.transfer_sip_participant
with
room_name
,
participant_identity
, and
transfer_to
. c. Ensure your SIP trunk/provider supports SIP REFER / call transfer and caller ID passthrough if needed. • Key APIs/examples: ◦
TransferSIPParticipant
/
transfer_sip_participant
API • When to use: simple forwarding, fast routing, or offloading the call without briefing. Context & best practices • Always pass per-call context (use
chat_ctx
or job
metadata
) so the receiver has necessary info. • Use a short announcement/filler speech to smooth perceived latency. • Verify SIP trunk supports transfers and caller ID passthrough for cold transfers. • For high-concurrency flows, make each call its own job/participant so contexts remain isolated. If you want, I can: • Provide a concrete code snippet for warm transfer in Python or JS, or • Show the
transfer_sip_participant
API payload for cold transfer. Which example would you like? Sources: The Handoff Pattern for Voice Agents That Replaces IVR Menus | LiveKit | The Human-in-the-Loop (HITL) Pattern for Voice Agents | LiveKit | Handling payments with PCI compliance | LiveKit | Preserving Caller ID During SIP Transfers in LiveKit Agents | Preserve caller ID during SIP transfers with Twilio | LiveKit, with interactive elements
c
will we need warn transfer outbound trunk ?
t
Short answer: yes — warm transfers typically require an outbound SIP trunk (or some outbound dialing path). Why: during a warm transfer LiveKit places the caller on hold, creates a consultation (private) room, and then dials the receiving human from the platform. That dial step needs an outbound path (a SIP trunk or PSTN provider) so LiveKit can call the target number and bring that participant into the consultation room. What to check • Do you already have an outbound SIP trunk (Twilio, your SBC, etc.) with call transfer enabled? If not, you’ll need one. • Ensure the trunk supports caller-ID passthrough and SIP REFER (if you plan to use cold transfer or to preserve caller ID). • Confirm your trunk credentials and trunk ID so
WarmTransferTask
or
CreateSIPParticipant
can use it when dialing the agent. If you’d like, tell me: • Which SIP provider or trunk you use (or none), and • Whether you want an example of the
WarmTransferTask
call that references
sip_trunk_id
— I’ll provide the exact payload/snippet., with interactive elements
c
from livekit.agents import Agent, function_tool, RunContext from livekit.agents.beta.workflows import WarmTransferTask SUPERVISOR_PHONE = "+15551234567" SIP_TRUNK_ID = "your-sip-trunk-id" class SupportAgent(Agent): @function_tool() async def escalate_to_human(self, context: RunContext): """Transfer to a human supervisor when: - The customer explicitly asks for a person - The issue involves a billing dispute over $500 - Sentiment indicates high frustration - The problem requires access to internal tools""" await self.session.say( "I'm connecting you with a specialist now. " "I'll share our conversation so you won't " "need to repeat anything.", allow_interruptions=False, ) await context.wait_for_playout() result = await WarmTransferTask( sip_call_to=SUPERVISOR_PHONE, sip_trunk_id=SIP_TRUNK_ID, chat_ctx=self.chat_ctx, ) return result so what does it show?
t
Thanks — I’ll explain what your snippet does and what you’ll see at runtime. What this code does • Declares a tool method
escalate_to_human
on
SupportAgent
that performs a warm transfer. • Plays a scripted message to the caller: “I’m connecting you with a specialist now…” • Waits for that message to finish with
context.wait_for_playout()
so the caller hears the full announcement. • Calls
WarmTransferTask
to dial the supervisor number via the specified SIP trunk and carry
chat_ctx
(full conversation context) into the consultation. • Returns the
WarmTransferTask
result so your agent logic can react to success/failure. What you’ll see at runtime • Caller hears the announcement and is placed on hold (optionally with hold music if configured). • LiveKit uses your
sip_trunk_id
to place an outbound call to
SUPERVISOR_PHONE
. • A private consultation room is created and the supervisor is dialed in. • The supervisor receives the conversation context (from
chat_ctx
) so they’re briefed before joining. • When the supervisor accepts, they are moved into the caller’s room and interim agents disconnect, leaving a human-to-human call. •
result
contains the transfer outcome (success, failure, or any error) so you can log, retry, or notify the caller. Notes and checks • Ensure your SIP trunk supports outbound dialing and caller ID passthrough. • If you need hold music or to disable caller audio explicitly, configure that in your room/hold settings. • If you want a code example showing how to inspect
result
and handle failures, I can provide one. Would you like a minimal example that handles transfer failures and notifies the caller? , with interactive elements
c
but there is no outbound trunk ID ?
t
Good question — you’re right to notice that. Warm transfers do require an outbound dialing path, so you must provide either a saved SIP trunk ID or a SIP outbound connection when calling
WarmTransferTask
. If no
sip_trunk_id
(or
sip_connection
) is provided, the transfer will fail because LiveKit can’t place the outbound leg. What you can do next • Use a saved outbound trunk: create/configure an outbound SIP trunk in your LiveKit portal (or via API) and pass its ID as
sip_trunk_id
. • Or provide a direct SIP outbound config: use
sip_connection
/
SIPOutboundConfig
in the
WarmTransferTask
call to specify hostname, auth, etc., instead of a trunk ID. • You can also set a default via the environment variable
LIVEKIT_SIP_OUTBOUND_TRUNK
so code can omit the explicit ID. If you want, I can: • Show the portal/API steps to create a trunk and get its ID, or • Give a concrete code snippet using
sip_trunk_id
or an example
sip_connection
payload. Which would you prefer? Sources: How to do call transfers with Five9 | LiveKit | Job lifecycle | LiveKit Documentation | sip | livekit/livekit_composite | DeepWiki | Creating SIP Inbound Trunks and Dispatch Rules with Python SDK | The Handoff Pattern for Voice Agents That Replaces IVR Menus | LiveKit, with interactive elements
c
give me a code snippet for warm transfer
t
Below is a minimal Python example showing a warm transfer using
WarmTransferTask
. It includes imports, optional env var fallback for
sip_trunk_id
, and basic result handling.
Copy code
import os
from livekit.agents import Agent, function_tool, RunContext
from livekit.agents.beta.workflows import WarmTransferTask

SUPERVISOR_PHONE = "+15551234567"
# prefer explicit config, fallback to environment variable
SIP_TRUNK_ID = os.getenv("LIVEKIT_SIP_OUTBOUND_TRUNK", "your-sip-trunk-id")

class SupportAgent(Agent):
    @function_tool()
    async def escalate_to_human(self, context: RunContext):
        await self.session.say(
            "I'm connecting you with a specialist now. I'll share our conversation so you won't need to repeat anything.",
            allow_interruptions=False,
        )
        await context.wait_for_playout()

        try:
            result = await WarmTransferTask(
                sip_call_to=SUPERVISOR_PHONE,
                sip_trunk_id=SIP_TRUNK_ID,
                chat_ctx=self.chat_ctx,
            )
        except Exception as e:
            # handle immediate failure to start transfer
            await self.session.say("Sorry, I couldn't connect you right now. Please hold while I try again.")
            # log or rethrow as needed
            raise

        # Inspect result for success/failure
        if getattr(result, "success", False):
            # transfer succeeded; result may include details depending on SDK version
            await self.session.say("I have connected you to a specialist. Thank you.")
        else:
            # transfer failed or was declined
            await self.session.say("I couldn't reach the specialist. Would you like to wait or leave a callback number?")
            # optional: log result.error or result.reason

        return result
Notes •
sip_trunk_id
is required (or use a `sip_connection`/`SIPOutboundConfig` alternative) so LiveKit can place the outbound call. •
chat_ctx
carries conversation context for the supervisor. • Add logging and retry/backoff as needed for production., with interactive elements
c
{"message": "failed to synthesize speech: no audio frames were pushed for text
t
Thanks — I’m responding to your speech synthesis error: "failed to synthesize speech: no audio frames were pushed for text." Quick checklist to diagnose and fix • Text content ◦ Ensure the string passed to TTS is not empty or just whitespace. ◦ Avoid extremely long single strings; try a short test phrase like "Hello world." • TTS provider / voices ◦ Confirm a valid voice/model is configured and available. ◦ If you switched providers or models, revert to a known-working voice to test. • Audio pipeline (frames) ◦ Verify your TTS integration actually pushes audio frames into the session (check logs where TTS produces frames). ◦ If you generate raw PCM/Opus, ensure frames are encoded and sent in the expected sample rate/format. • Rate/timing ◦ Make sure you wait for TTS generation to start before streaming frames. Race conditions can lead to no frames being pushed. ◦ If you use an async generator/callback, add logging to confirm frames are emitted. • Runtime errors ◦ Inspect agent/server logs for exceptions during TTS creation (authorization, model errors). ◦ Look for errors like auth failures, quota limits, or network timeouts that prevent audio generation. • Fallback test ◦ Replace your TTS call with a simple pre-recorded audio frame or a known-good TTS snippet to confirm the rest of the audio path works. What I need if the issue persists • The exact code snippet that calls TTS (the function that produces/streams frames). • The voice/model/provider you’re using and any relevant logs around the failure. • Whether this happens for all text or only certain phrases. Tell me which of the above you’ve already checked and paste the small TTS call code (or log excerpt). I’ll point out the precise fix. , with interactive elements
c
min_endpointing_delay=0.3, max_endpointing_delay=1.5,
t
You’re asking about
min_endpointing_delay
and
max_endpointing_delay
and how they relate to the TTS error ("no audio frames were pushed for text"). Here’s a concise explanation and next steps. What those settings do •
min_endpointing_delay
(seconds): minimum time the system waits after speech generation stops before declaring the utterance finished. Prevents premature endpointing. •
max_endpointing_delay
(seconds): maximum time the system will wait for additional audio before forcing endpointing. Prevents hanging when no more audio arrives. Why they matter for your error • If
min_endpointing_delay
is too large relative to when audio frames are produced, the system might expect frames that never arrive and eventually time out in a way that surfaces as “no audio frames were pushed.” • If
max_endpointing_delay
is too small, you may cut off streaming before frames arrive, also leading to no-audio behavior for short/slow TTS streams. Suggested starting values • Keep
min_endpointing_delay
small but not zero: 0.2–0.5s is reasonable. • Keep
max_endpointing_delay
larger to tolerate network/processing latency: 1.0–2.0s is typical. Your values (0.3 and 1.5) are within recommended ranges. Next diagnostics to run • Confirm the TTS generator actually emits frames: add logging where frames are created and pushed. • Test with a short known phrase ("Hello world") and verify frames appear. • Check timing: log timestamps for first/last frame vs. endpoint timers to see if endpointing fires before frames arrive. • Ensure encoding/format matches expected pipeline (sample rate, codec, Opus vs PCM). • If using async streaming, add a small artificial delay before endpointing to rule out race conditions. If you want, paste the small TTS call/streaming snippet and I’ll point out exactly where to log or adjust timers to confirm frames are produced and accepted before endpointing. , with interactive elements
c
explain this 2 parameters in layman terms.
t
Sure — in plain terms: • min_endpointing_delay: the minimum wait time after speech seems to stop before the system says “the person finished talking.” Think of it as pausing a short moment to make sure they’re really done and not just taking a breath. • max_endpointing_delay: the maximum wait time the system will tolerate waiting for more speech before it gives up and decides the person finished. Think of it as a safety cutoff so the call flow doesn’t hang forever if no more audio comes. Together they tell the system: "wait at least this long to avoid cutting someone off, but don’t wait longer than this in case audio never shows up." If you want, I can suggest exact values for your setup or explain how changing them might fix your TTS timing issue. , with interactive elements