Skip to main content

LiveKit & Voice Agents

LiveKit provides the real-time audio/video transport (rooms, participants, SIP, recording) behind OrcFlows' voice features. Voice agent nodes combine STT (speech-to-text), an LLM, and TTS (text-to-speech) into a real-time conversational loop that joins a LiveKit room as a participant. For starting a workflow from a LiveKit event, see Triggers — LiveKit; the underlying webhook is documented in Provider webhooks.

All nodes on this page share a common set of LiveKit server credentials:

FieldTypeDefaultDescription
api_keystringRequired (most nodes). LiveKit API key. In the bundled dev docker-compose.yml, the LiveKit server runs in --dev mode with api_key=devkey / api_secret=secret on http://localhost:7880.
api_secretstringRequired (most nodes). LiveKit API secret, used to sign admin/participant JWTs (HS256).
http_urlstringRequired (most nodes). LiveKit server HTTP URL for Twirp/REST calls (e.g. http://localhost:7880).
ws_urlstringRequired where noted. LiveKit WebSocket URL returned to clients/agents for connecting (e.g. ws://localhost:7880).

All config values support {{...}} expressions, resolved against steps, vars, trigger, secret, and input (see Workflows & the DSL).

livekit-room

CRUD and participant management for LiveKit rooms. The action field selects the operation.

{
"id": "create_room",
"type": "livekit-room",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"http_url": "http://localhost:7880",
"action": "create",
"room": "support-call-{{trigger.ticket_id}}",
"empty_timeout": 300,
"max_participants": 4
}
}
FieldTypeDefaultDescription
api_key / api_secret / http_urlstringRequired. LiveKit server credentials.
actionstring"create"One of create, list, delete, get_participants, remove_participant, update_participant, send_data, mute_track.
roomstringRequired for create, delete, get_participants, remove_participant, update_participant, send_data, mute_track. Room name.
empty_timeoutnumber300(create) Seconds an empty room is kept alive before LiveKit deletes it.
max_participantsnumber0(create) Hard cap on participants; 0 = unlimited.
metadatastring(create, update_participant) Arbitrary metadata string.
identitystringRequired for remove_participant, update_participant, mute_track. Participant identity.
namestring(update_participant) New display name.
can_publish / can_subscribe / can_publish_data / hidden / recorderboolean(update_participant) Sets the corresponding permission only if the key is present in config.
datastringRequired for send_data. Payload string (sent base64-encoded, reliable delivery).
topicstring(send_data) Topic/channel label for the data message.
destination_identitiesstring | array(send_data) Comma-separated string or array of participant identities to target; omit to broadcast.
track_sidstringRequired for mute_track. Track SID to mute/unmute.
mutedbooleantrue(mute_track) Mute state to apply.

Output: depends on action:

  • create: { room, sid, created: true, raw }
  • list: { rooms: [...], count }
  • delete: { deleted: true, room }
  • get_participants: { participants: [...], count, room }
  • remove_participant: { removed: true, room, identity }
  • update_participant: { updated: true, room, identity, raw }
  • send_data: { sent: true, room, topic, bytes }
  • mute_track: { muted, room, identity, track_sid }

livekit-token

Generates a LiveKit room-join JWT for a specific participant — a human user, an AI agent, or an observer. Use this when a workflow needs to hand connection credentials to a client without going through livekit-voice-session.

{
"id": "issue_token",
"type": "livekit-token",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"ws_url": "wss://my-livekit.example.com",
"room": "support-call-{{trigger.ticket_id}}",
"identity": "{{trigger.user_id}}",
"name": "{{trigger.user_name}}",
"ttl_seconds": 3600
}
}
FieldTypeDefaultDescription
api_key / api_secretstringRequired. LiveKit credentials.
ws_urlstringWebSocket URL, passed through unchanged into the output for the client to connect with.
roomstringRequired. Room the token grants access to.
identitystringRequired. Unique participant identity within the room.
namestringidentityDisplay name embedded in the token.
ttl_secondsnumber21600 (6h)Token lifetime in seconds.
can_publishbooleantrueGrants canPublish. Set to false to issue a subscribe-only token.
can_subscribebooleantrueGrants canSubscribe.
can_publish_databooleantrueGrants canPublishData.
room_adminbooleanfalseGrants roomAdmin on the token.
room_createbooleanfalseGrants roomCreate on the token.

Output: { token, room, identity, name, ws_url }token is the signed JWT a client passes to the LiveKit SDK along with ws_url.

livekit-dispatch

Dispatches a job to a running LiveKit Agents server (a separate Python/Node process registered against the same LiveKit instance), instructing it to join a room. Use this when your voice agent logic lives outside OrcFlows; for the built-in Go agent, use livekit-voice-agent instead.

{
"id": "dispatch_agent",
"type": "livekit-dispatch",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"http_url": "http://localhost:7880",
"action": "create",
"agent_name": "support-agent",
"room": "support-call-{{trigger.ticket_id}}"
}
}
FieldTypeDefaultDescription
api_key / api_secret / http_urlstringRequired. LiveKit server credentials.
actionstring"create"One of create, delete, list.
agent_namestringRequired for create. Name the agent is registered under in the LiveKit Agents server.
roomstringRequired for create. Optional filter for list. Room to dispatch the agent into.
metadatastring(create) JSON string passed to the dispatched agent job.
dispatch_idstringRequired for delete. ID of the dispatch to cancel.

Output: depends on action:

  • create: { dispatch_id, agent_name, room, dispatched: true, raw }
  • delete: { deleted: true, dispatch_id }
  • list: { dispatches: [...], count }

livekit-ingress

Manages LiveKit Ingress endpoints, which bring an external media stream (RTMP encoder, WHIP browser publisher, or a remote URL) into a room as a published track — for example, so a downstream vision node can analyze a live camera or OBS feed.

{
"id": "create_ingress",
"type": "livekit-ingress",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"http_url": "http://localhost:7880",
"action": "create",
"input_type": "rtmp",
"name": "obs-stream",
"room_name": "studio"
}
}
FieldTypeDefaultDescription
api_key / api_secret / http_urlstringRequired. LiveKit server credentials.
actionstring"create"One of create, list, delete.
input_typestring"rtmp"(create) "rtmp", "whip", or "url".
namestring"ingress"(create) Display name for the ingress.
room_namestringRequired for create. Destination room. Optional filter for list.
participant_identitystring"ingress"(create) Identity the ingress publishes as.
participant_namestringname(create) Display name for the ingress participant.
urlstring(create, input_type: "url") Source URL to pull from.
ingress_idstringRequired for delete. Optional filter for list.

Output: depends on action:

  • create: { ingress_id, name, room_name, input_type, status, raw }, plus stream_url and stream_key for "rtmp" ingresses (configure these in OBS/your encoder), or whip_url for "whip" ingresses.
  • list: { ingresses: [...], count }
  • delete: { deleted: true, ingress_id }

livekit-egress

Starts, stops, and lists Egress jobs — recording or restreaming a room's audio/video.

{
"id": "record_call",
"type": "livekit-egress",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"http_url": "http://localhost:7880",
"action": "start_room_composite",
"room_name": "support-call-{{trigger.ticket_id}}",
"layout": "speaker-dark",
"output_type": "file",
"filepath": "s3://my-bucket/recordings/{room_name}/{time}.mp4"
}
}
FieldTypeDefaultDescription
api_key / api_secret / http_urlstringRequired. LiveKit server credentials.
actionstring"list"One of start_room_composite, start_track_composite, stop, list.
room_namestringRequired for start_room_composite / start_track_composite. Room to record. Optional filter for list.
layoutstring"speaker-dark"(start_room_composite) "speaker-dark", "grid-dark", "speaker-light", or "grid-light".
audio_onlybooleanfalse(start_room_composite) Record audio only.
output_typestring"file"(start_room_composite) "file", "stream", or "segments".
filepathstring"{room_name}/{time}.mp4" (file) / "{room_name}/{time}" (segments)Output path/prefix; supports {room_name} and {time} placeholders and S3/GCS URIs.
rtmp_urlstringRequired for output_type: "stream". RTMP destination URL.
audio_track_id / video_track_idstring(start_track_composite) Track SIDs to record; at least one is required.
egress_idstringRequired for stop. Optional filter for list.
activeboolean(list) Only return active egresses.

Output: depends on action:

  • start_room_composite / start_track_composite: { egress_id, room_name, status, raw }
  • stop: { stopped: true, egress_id, raw }
  • list: { egresses: [...], count }

livekit-sip

Manages SIP trunks and bridges PSTN phone calls into LiveKit rooms — outbound dialing, inbound trunk routing, transferring an active call, and trunk CRUD.

{
"id": "dial_customer",
"type": "livekit-sip",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"http_url": "http://localhost:7880",
"action": "call",
"sip_trunk_id": "{{vars.outbound_trunk_id}}",
"phone_number": "{{steps.fetch_lead.output.phone}}",
"room": "coldcall-{{trigger.lead_id}}"
}
}
FieldTypeDefaultDescription
api_key / api_secret / http_urlstringRequired. LiveKit server credentials.
actionstring"call"One of call, transfer, create_outbound_trunk, create_inbound_trunk, list_trunks, delete_trunk.
sip_trunk_idstringRequired for call. Outbound SIP trunk to dial through.
phone_numberstringRequired for call. Number to dial (E.164).
roomstringRequired for call and transfer. Room to add the SIP participant to.
participant_identitystring"phone-<phone_number>"(call) Identity for the dialed-in participant.
participant_namestringphone_number(call) Display name for the dialed-in participant.
metadatastring(call) Participant metadata.
dtmf_after_answerstring(call) DTMF digits to send once the call is answered.
participant_identity (transfer)stringRequired for transfer. Identity of the SIP participant to transfer.
transfer_tostringRequired for transfer. Number/SIP URI to transfer the call to.
namestringRequired for create_outbound_trunk / create_inbound_trunk. Trunk display name.
addressstringRequired for create_outbound_trunk. SIP provider address (e.g. Twilio/Telnyx termination URI).
username / passwordstring(create_outbound_trunk, create_inbound_trunk) SIP auth credentials.
numbersstring(create_outbound_trunk) Number associated with the trunk.
phone_number (inbound trunk)stringRequired for create_inbound_trunk. Number that routes inbound calls into rooms.
trunk_idstringRequired for delete_trunk.

Output: depends on action:

  • call: { participant_id, participant_identity, room, phone_number, called: true, raw }
  • transfer: { transferred: true, transfer_to, identity }
  • create_outbound_trunk: { trunk_id, name, address, created: true }
  • create_inbound_trunk: { trunk_id, name, number, created: true }
  • list_trunks: { trunks: [...], count, outbound_count, inbound_count }
  • delete_trunk: { deleted: true, trunk_id }

livekit-data-send

Sends a structured message into a LiveKit room over WebRTC data channels — for agent-to-client signaling (status updates, control messages, JSON events) without audio/video overhead.

{
"id": "notify_client",
"type": "livekit-data-send",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"http_url": "http://localhost:7880",
"room": "support-call-{{trigger.ticket_id}}",
"topic": "agent-status",
"format": "json",
"data": "{\"status\": \"transferring\"}"
}
}
FieldTypeDefaultDescription
api_key / api_secret / http_urlstringRequired. LiveKit server credentials.
roomstringRequired. Target room name.
datastring | objectRequired. Payload to send. If not a string, it is JSON-marshaled first; sent base64-encoded with reliable delivery.
topicstringOptional topic/channel label (e.g. "agent-status", "control").
formatstring"text""text" or "json". When "json", the resolved data string is validated as JSON before sending.
destination_identitiesstring | arrayComma-separated string or array of participant identities to target; omit to broadcast to all.

Output: { sent: true, room, topic, bytes }bytes is the length of the payload before base64 encoding.

livekit-voice-session

Provisions a complete voice session: creates a LiveKit room, generates a join token for a human participant and a separate token for an AI agent slot, and optionally dispatches a LiveKit Agents server job into the room. Use this when a human and an external (Python/Node) agent will both connect — the workflow returns connection credentials, then later steps (egress for recording, livekit-data-send for messages) can act on the same room.

{
"id": "start_session",
"type": "livekit-voice-session",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"http_url": "http://localhost:7880",
"ws_url": "wss://my-livekit.example.com",
"room_name": "support-call-{{trigger.ticket_id}}",
"user_identity": "{{trigger.user_id}}",
"user_name": "{{trigger.user_name}}",
"dispatch_agent": true,
"agent_name_dispatch": "support-agent"
}
}
FieldTypeDefaultDescription
api_key / api_secret / http_url / ws_urlstringRequired. LiveKit server credentials and WebSocket URL returned to clients.
room_namestringRequired. Room to create.
user_identitystringRequired. Identity for the human participant.
user_namestringuser_identityDisplay name for the human.
agent_identitystring"agent"Identity reserved for the AI agent slot.
agent_namestring"AI Agent"Display name for the agent participant.
user_ttl_secondsnumber21600 (6h)Lifetime of the user's token.
agent_ttl_secondsnumber21600 (6h)Lifetime of the agent's token.
empty_timeoutnumber300Seconds until an empty room is deleted.
max_participantsnumber0Hard cap on participants; 0 = unlimited.
metadatastringRoom metadata JSON string.
dispatch_agentbooleanfalseIf true, calls the AgentDispatch API after room creation (requires agent_name_dispatch).
agent_name_dispatchstringRequired if dispatch_agent is true. Agent name registered in the LiveKit Agents server.
dispatch_metadatastringMetadata string passed to the dispatched agent job.

Output:

{
"room_name": "support-call-1234",
"room_sid": "RM_...",
"user_identity": "user-42",
"user_token": "<jwt>",
"agent_identity": "agent",
"agent_token": "<jwt>",
"ws_url": "wss://my-livekit.example.com",
"dispatch_id": "AD_...",
"dispatched": true
}

The agent token includes an agent: true video grant. If dispatch_agent is true and dispatch fails, the step does not fail — dispatched: false and dispatch_error are set instead, since the room and tokens are already valid.

livekit-voice-agent

Starts, stops, or checks the status of a built-in Go voice agent that joins a LiveKit room directly over WebRTC — no external Python/Node Agents server required. The agent runs an STT → LLM → TTS loop: it listens to participant audio, detects end-of-utterance via voice activity detection (VAD), transcribes, generates a reply, and speaks it back into the room in real time.

action: "start" is non-blocking — the agent runs in a background goroutine managed per-room, so the workflow step returns immediately while the agent continues the conversation.

{
"id": "join_agent",
"type": "livekit-voice-agent",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"ws_url": "wss://my-livekit.example.com",
"http_url": "http://localhost:7880",
"room": "support-call-{{trigger.ticket_id}}",
"action": "start",
"system_prompt": "You are a calm, friendly support agent for Acme. Keep responses to 1-2 sentences.",
"llm_provider": "anthropic",
"llm_model": "claude-opus-4-8",
"stt_provider": "openai",
"tts_provider": "openai",
"tts_voice": "alloy"
}
}
FieldTypeDefaultDescription
api_key / api_secret / ws_urlstringRequired. LiveKit credentials and WebSocket URL the agent connects to.
http_urlstringLiveKit HTTP URL (used for REST calls).
roomstringRequired. Room the agent joins.
actionstring"start""start", "stop", or "status".
identitystring"goagents-bot"Agent's participant identity in the room.
display_namestring"AI Assistant"Agent's display name.
system_promptstring(built-in default)LLM system prompt. Default: "You are a helpful voice assistant. Keep your responses brief and conversational (1-3 sentences). Avoid markdown formatting."
stt_providerstring"openai""openai", "deepgram", "nvidia", or "elevenlabs". NVIDIA NIM has no REST STT endpoint — use "openai" or "elevenlabs" for STT even if the LLM uses "nvidia".
stt_api_keystring(see fallback)API key for the STT provider. Falls back to llm_api_key if empty.
stt_modelstringprovider defaultwhisper-1 (openai), nvidia/parakeet-ctc-1.1b (nvidia), scribe_v1 (elevenlabs); default depends on stt_provider.
stt_languagestringauto-detectBCP-47 language code. Empty = auto-detect.
llm_providerstring"anthropic""anthropic", "openai", or "nvidia".
llm_api_keystringAPI key for the LLM provider.
llm_modelstringprovider defaultDefault depends on llm_provider: claude-opus-4-8 (anthropic), gpt-4o (openai), nvidia/llama-3.1-nemotron-70b-instruct (nvidia).
tts_providerstring"openai""openai" or "elevenlabs". NVIDIA NIM has no REST TTS endpoint — use "openai" or "elevenlabs" even if STT/LLM use "nvidia".
tts_api_keystring(see fallback)API key for the TTS provider. Falls back to llm_api_key if empty.
tts_voicestring"alloy"Voice ID — e.g. alloy, nova, shimmer (OpenAI) or an ElevenLabs voice ID.
nvidia_api_keystringShared NVIDIA NIM API key, used for STT and/or LLM when their provider is "nvidia" and no provider-specific key is set.
nvidia_base_urlstringhttps://integrate.api.nvidia.com/v1Override base URL used for STT and LLM requests when their provider is "nvidia".
silence_threshold_msnumber1500Milliseconds of silence (via VAD) that marks the end of an utterance and triggers STT/LLM/TTS for that turn — the primary turn-detection / barge-in tuning knob.

Output: depends on action:

  • start: { started: true, room, identity, display_name, stt_provider, llm_provider, tts_provider }
  • stop: { stopped: true, room }
  • status: { running: <bool>, room }

There is one running agent per room, tracked by a process-wide manager — calling start again for the same room while one is running, or stop/status for a room with no agent, are handled gracefully by the manager.

livekit-cold-call

Runs an outbound calling campaign: for each customer in a list, dials their phone number via LiveKit SIP into a fresh room, has the same built-in voice agent (STT → LLM → TTS) hold the conversation, waits for the call to end, and collects the transcript. Calls are placed sequentially, one at a time — this node blocks for the duration of the entire campaign, so set a generous step timeout (e.g. "3600s" for several calls at up to 5 minutes each).

Accepts the same stt_* / llm_* / tts_* / nvidia_* fields as livekit-voice-agent (provider, model, API key, voice, silence_threshold_ms), applied identically to every call.

{
"id": "run_campaign",
"type": "livekit-cold-call",
"timeout": "3600s",
"config": {
"api_key": "{{secret.LIVEKIT_API_KEY}}",
"api_secret": "{{secret.LIVEKIT_API_SECRET}}",
"ws_url": "wss://my-livekit.example.com",
"http_url": "http://localhost:7880",
"sip_trunk_id": "{{vars.outbound_trunk_id}}",
"customers": "{{steps.fetch_leads.output.results}}",
"phone_field": "phone",
"name_field": "name",
"system_prompt": "You are calling {{customer.name}} on behalf of Acme about their renewal. Be brief and friendly.",
"ring_timeout_seconds": 30,
"max_call_seconds": 300,
"delay_between_calls_seconds": 5,
"cleanup_room": true
}
}
FieldTypeDefaultDescription
api_key / api_secret / ws_url / http_urlstringRequired. LiveKit server credentials.
sip_trunk_idstringRequired. Outbound SIP trunk (created via livekit-sip create_outbound_trunk) used to dial each customer.
customersarrayRequired. Array of customer objects to call, e.g. "{{steps.fetch_leads.output.results}}".
phone_fieldstring"phone"Field in each customer object holding the phone number.
name_fieldstring"name"Field in each customer object holding the display name.
system_promptstring(built-in default)Agent system prompt. {{customer.*}} placeholders are resolved per-customer against that customer's object (e.g. {{customer.name}}), in addition to normal {{steps...}}/{{vars...}} expressions.
ring_timeout_secondsnumber30Seconds to wait for the call to be answered before marking it no_answer.
max_call_secondsnumber300Maximum conversation duration once answered (maps to the agent's MaxDurationSeconds).
delay_between_calls_secondsnumber0Pause between consecutive calls.
cleanup_roombooleantrueDelete the per-call LiveKit room after each call finishes (or fails).
stt_* / llm_* / tts_* / nvidia_* / silence_threshold_ms(see livekit-voice-agent)Same voice agent configuration as livekit-voice-agent, applied to every call.

For each customer, the node creates a temporary room (coldcall-<index>-<random>), starts a voice agent configured with ExitWhenAlone: true (so the session ends gracefully once the callee hangs up) and MaxDurationSeconds: max_call_seconds, then dials the customer's number into that room via SIP.

Output:

{
"calls": [
{
"phone": "+15551234567",
"name": "Jane Doe",
"status": "completed",
"duration_seconds": 87.4,
"transcript": [
{ "role": "user", "content": "Hello?" },
{ "role": "assistant", "content": "Hi Jane, this is Acme calling about..." }
]
}
],
"total": 10,
"completed": 7,
"no_answer": 2,
"failed": 1
}

Each entry in calls has status of completed, no_answer, failed, or dial_failed, plus duration_seconds and transcript ([]{role, content}); failed/dial_failed entries also include an error string. total/completed/no_answer/failed summarize the campaign — entries missing the phone_field value are also counted under failed.

Next