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:
| Field | Type | Default | Description |
|---|---|---|---|
api_key | string | — | Required (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_secret | string | — | Required (most nodes). LiveKit API secret, used to sign admin/participant JWTs (HS256). |
http_url | string | — | Required (most nodes). LiveKit server HTTP URL for Twirp/REST calls (e.g. http://localhost:7880). |
ws_url | string | — | Required 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
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / http_url | string | — | Required. LiveKit server credentials. |
action | string | "create" | One of create, list, delete, get_participants, remove_participant, update_participant, send_data, mute_track. |
room | string | — | Required for create, delete, get_participants, remove_participant, update_participant, send_data, mute_track. Room name. |
empty_timeout | number | 300 | (create) Seconds an empty room is kept alive before LiveKit deletes it. |
max_participants | number | 0 | (create) Hard cap on participants; 0 = unlimited. |
metadata | string | — | (create, update_participant) Arbitrary metadata string. |
identity | string | — | Required for remove_participant, update_participant, mute_track. Participant identity. |
name | string | — | (update_participant) New display name. |
can_publish / can_subscribe / can_publish_data / hidden / recorder | boolean | — | (update_participant) Sets the corresponding permission only if the key is present in config. |
data | string | — | Required for send_data. Payload string (sent base64-encoded, reliable delivery). |
topic | string | — | (send_data) Topic/channel label for the data message. |
destination_identities | string | array | — | (send_data) Comma-separated string or array of participant identities to target; omit to broadcast. |
track_sid | string | — | Required for mute_track. Track SID to mute/unmute. |
muted | boolean | true | (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
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret | string | — | Required. LiveKit credentials. |
ws_url | string | — | WebSocket URL, passed through unchanged into the output for the client to connect with. |
room | string | — | Required. Room the token grants access to. |
identity | string | — | Required. Unique participant identity within the room. |
name | string | identity | Display name embedded in the token. |
ttl_seconds | number | 21600 (6h) | Token lifetime in seconds. |
can_publish | boolean | true | Grants canPublish. Set to false to issue a subscribe-only token. |
can_subscribe | boolean | true | Grants canSubscribe. |
can_publish_data | boolean | true | Grants canPublishData. |
room_admin | boolean | false | Grants roomAdmin on the token. |
room_create | boolean | false | Grants 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}}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / http_url | string | — | Required. LiveKit server credentials. |
action | string | "create" | One of create, delete, list. |
agent_name | string | — | Required for create. Name the agent is registered under in the LiveKit Agents server. |
room | string | — | Required for create. Optional filter for list. Room to dispatch the agent into. |
metadata | string | — | (create) JSON string passed to the dispatched agent job. |
dispatch_id | string | — | Required 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"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / http_url | string | — | Required. LiveKit server credentials. |
action | string | "create" | One of create, list, delete. |
input_type | string | "rtmp" | (create) "rtmp", "whip", or "url". |
name | string | "ingress" | (create) Display name for the ingress. |
room_name | string | — | Required for create. Destination room. Optional filter for list. |
participant_identity | string | "ingress" | (create) Identity the ingress publishes as. |
participant_name | string | name | (create) Display name for the ingress participant. |
url | string | — | (create, input_type: "url") Source URL to pull from. |
ingress_id | string | — | Required for delete. Optional filter for list. |
Output: depends on action:
create:{ ingress_id, name, room_name, input_type, status, raw }, plusstream_urlandstream_keyfor"rtmp"ingresses (configure these in OBS/your encoder), orwhip_urlfor"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"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / http_url | string | — | Required. LiveKit server credentials. |
action | string | "list" | One of start_room_composite, start_track_composite, stop, list. |
room_name | string | — | Required for start_room_composite / start_track_composite. Room to record. Optional filter for list. |
layout | string | "speaker-dark" | (start_room_composite) "speaker-dark", "grid-dark", "speaker-light", or "grid-light". |
audio_only | boolean | false | (start_room_composite) Record audio only. |
output_type | string | "file" | (start_room_composite) "file", "stream", or "segments". |
filepath | string | "{room_name}/{time}.mp4" (file) / "{room_name}/{time}" (segments) | Output path/prefix; supports {room_name} and {time} placeholders and S3/GCS URIs. |
rtmp_url | string | — | Required for output_type: "stream". RTMP destination URL. |
audio_track_id / video_track_id | string | — | (start_track_composite) Track SIDs to record; at least one is required. |
egress_id | string | — | Required for stop. Optional filter for list. |
active | boolean | — | (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}}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / http_url | string | — | Required. LiveKit server credentials. |
action | string | "call" | One of call, transfer, create_outbound_trunk, create_inbound_trunk, list_trunks, delete_trunk. |
sip_trunk_id | string | — | Required for call. Outbound SIP trunk to dial through. |
phone_number | string | — | Required for call. Number to dial (E.164). |
room | string | — | Required for call and transfer. Room to add the SIP participant to. |
participant_identity | string | "phone-<phone_number>" | (call) Identity for the dialed-in participant. |
participant_name | string | phone_number | (call) Display name for the dialed-in participant. |
metadata | string | — | (call) Participant metadata. |
dtmf_after_answer | string | — | (call) DTMF digits to send once the call is answered. |
participant_identity (transfer) | string | — | Required for transfer. Identity of the SIP participant to transfer. |
transfer_to | string | — | Required for transfer. Number/SIP URI to transfer the call to. |
name | string | — | Required for create_outbound_trunk / create_inbound_trunk. Trunk display name. |
address | string | — | Required for create_outbound_trunk. SIP provider address (e.g. Twilio/Telnyx termination URI). |
username / password | string | — | (create_outbound_trunk, create_inbound_trunk) SIP auth credentials. |
numbers | string | — | (create_outbound_trunk) Number associated with the trunk. |
phone_number (inbound trunk) | string | — | Required for create_inbound_trunk. Number that routes inbound calls into rooms. |
trunk_id | string | — | Required 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\"}"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / http_url | string | — | Required. LiveKit server credentials. |
room | string | — | Required. Target room name. |
data | string | object | — | Required. Payload to send. If not a string, it is JSON-marshaled first; sent base64-encoded with reliable delivery. |
topic | string | — | Optional topic/channel label (e.g. "agent-status", "control"). |
format | string | "text" | "text" or "json". When "json", the resolved data string is validated as JSON before sending. |
destination_identities | string | array | — | Comma-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"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / http_url / ws_url | string | — | Required. LiveKit server credentials and WebSocket URL returned to clients. |
room_name | string | — | Required. Room to create. |
user_identity | string | — | Required. Identity for the human participant. |
user_name | string | user_identity | Display name for the human. |
agent_identity | string | "agent" | Identity reserved for the AI agent slot. |
agent_name | string | "AI Agent" | Display name for the agent participant. |
user_ttl_seconds | number | 21600 (6h) | Lifetime of the user's token. |
agent_ttl_seconds | number | 21600 (6h) | Lifetime of the agent's token. |
empty_timeout | number | 300 | Seconds until an empty room is deleted. |
max_participants | number | 0 | Hard cap on participants; 0 = unlimited. |
metadata | string | — | Room metadata JSON string. |
dispatch_agent | boolean | false | If true, calls the AgentDispatch API after room creation (requires agent_name_dispatch). |
agent_name_dispatch | string | — | Required if dispatch_agent is true. Agent name registered in the LiveKit Agents server. |
dispatch_metadata | string | — | Metadata 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"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / ws_url | string | — | Required. LiveKit credentials and WebSocket URL the agent connects to. |
http_url | string | — | LiveKit HTTP URL (used for REST calls). |
room | string | — | Required. Room the agent joins. |
action | string | "start" | "start", "stop", or "status". |
identity | string | "goagents-bot" | Agent's participant identity in the room. |
display_name | string | "AI Assistant" | Agent's display name. |
system_prompt | string | (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_provider | string | "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_key | string | (see fallback) | API key for the STT provider. Falls back to llm_api_key if empty. |
stt_model | string | provider default | whisper-1 (openai), nvidia/parakeet-ctc-1.1b (nvidia), scribe_v1 (elevenlabs); default depends on stt_provider. |
stt_language | string | auto-detect | BCP-47 language code. Empty = auto-detect. |
llm_provider | string | "anthropic" | "anthropic", "openai", or "nvidia". |
llm_api_key | string | — | API key for the LLM provider. |
llm_model | string | provider default | Default depends on llm_provider: claude-opus-4-8 (anthropic), gpt-4o (openai), nvidia/llama-3.1-nemotron-70b-instruct (nvidia). |
tts_provider | string | "openai" | "openai" or "elevenlabs". NVIDIA NIM has no REST TTS endpoint — use "openai" or "elevenlabs" even if STT/LLM use "nvidia". |
tts_api_key | string | (see fallback) | API key for the TTS provider. Falls back to llm_api_key if empty. |
tts_voice | string | "alloy" | Voice ID — e.g. alloy, nova, shimmer (OpenAI) or an ElevenLabs voice ID. |
nvidia_api_key | string | — | Shared NVIDIA NIM API key, used for STT and/or LLM when their provider is "nvidia" and no provider-specific key is set. |
nvidia_base_url | string | https://integrate.api.nvidia.com/v1 | Override base URL used for STT and LLM requests when their provider is "nvidia". |
silence_threshold_ms | number | 1500 | Milliseconds 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
}
}
| Field | Type | Default | Description |
|---|---|---|---|
api_key / api_secret / ws_url / http_url | string | — | Required. LiveKit server credentials. |
sip_trunk_id | string | — | Required. Outbound SIP trunk (created via livekit-sip create_outbound_trunk) used to dial each customer. |
customers | array | — | Required. Array of customer objects to call, e.g. "{{steps.fetch_leads.output.results}}". |
phone_field | string | "phone" | Field in each customer object holding the phone number. |
name_field | string | "name" | Field in each customer object holding the display name. |
system_prompt | string | (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_seconds | number | 30 | Seconds to wait for the call to be answered before marking it no_answer. |
max_call_seconds | number | 300 | Maximum conversation duration once answered (maps to the agent's MaxDurationSeconds). |
delay_between_calls_seconds | number | 0 | Pause between consecutive calls. |
cleanup_room | boolean | true | Delete 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.