Telephony & Phone Calls
OrcFlows can place and receive real phone calls and put an AI voice agent (speech-to-text → LLM → text-to-speech) on the line. This runs on LiveKit SIP — the same infrastructure used for production voice AI.
This guide gets two examples running:
LiveKit AI Phone Call(outbound) — registers your phone provider as an outbound trunk, then dials a number and has an AI agent hold a short conversation.LiveKit Inbound Call(Setup + AI Agent) — lets people call you, with an AI agent answering.
What you need
A phone call travels through three layers — all three must be in place:
| Layer | What it does | Options |
|---|---|---|
| LiveKit server + SIP service | Bridges your AI agent's audio to the phone network | LiveKit Cloud (SIP built-in) or self-hosted livekit-sip |
| A telephony provider + SIP trunk | Actually places the PSTN call and owns the phone number | Twilio, Telnyx, Plivo, … |
| OrcFlows workflow | Orchestrates the trunk + call + AI agent | the example below |
Your local
docker-composewon't place real calls. It runslivekit-server --dev, which has no SIP service. For real telephony use LiveKit Cloud (fastest) or deploy the separate LiveKit SIP service. The errordial_failed/ no trunk almost always means SIP isn't actually available.
Step 1 — LiveKit with SIP
Option A — LiveKit Cloud (recommended)
- Create a free project at cloud.livekit.io. SIP is enabled automatically — nothing to deploy.
- From Settings → Keys, copy your API Key, API Secret, and the project URL (
wss://<project>.livekit.cloud).
Option B — Self-hosted
Run the LiveKit SIP service alongside your LiveKit server (it's a separate container that needs Redis to coordinate with the server). The --dev server alone is not enough. Use your server's API key/secret and ws:///http:// URLs.
Step 2 — A provider SIP trunk (Twilio example)
You need a number that can dial out and SIP credentials to authenticate. Using Twilio Elastic SIP Trunking:
- Buy a voice-capable number (Phone Numbers → Buy a number).
- Create an Elastic SIP Trunk (Elastic SIP Trunking → Trunks → Create).
- Under Termination, set a Termination SIP URI — e.g.
your-trunk.pstn.twilio.com. This host is yourSIP_TRUNK_ADDRESS. - Under Termination → Authentication, add Credential List auth — the username/password become
SIP_USERNAME/SIP_PASSWORD. - Assign your purchased number to the trunk. That E.164 number is your
SIP_FROM_NUMBER(caller ID).
Telnyx and Plivo are equivalent — you're after the same four things: SIP host, username, password, from-number.
Step 3 — Add the secrets in OrcFlows
Settings → Secrets, add:
| Secret | Value |
|---|---|
LIVEKIT_API_KEY | LiveKit API key |
LIVEKIT_API_SECRET | LiveKit API secret |
LIVEKIT_WS_URL | wss://<project>.livekit.cloud (or ws://host:7880 self-hosted) |
LIVEKIT_HTTP_URL | https://<project>.livekit.cloud (or http://host:7880) |
SIP_TRUNK_ADDRESS | provider SIP host, e.g. your-trunk.pstn.twilio.com |
SIP_USERNAME | provider SIP auth username |
SIP_PASSWORD | provider SIP auth password |
SIP_FROM_NUMBER | your caller-ID number, E.164, e.g. +15551234567 |
OPENAI_API_KEY | for the voice agent (STT + LLM + TTS) |
Secrets are AES-256-GCM encrypted and resolved at runtime via {{ secret.NAME }}.
Step 4 — Outbound: import and run
- Workflows → Import, choose
examples/livekit_phone_call.goagents.json. - Open it, toggle it Enabled, click Run, and enter a
phone_numberto call (E.164).
You'll watch the two steps execute: Register outbound SIP trunk (creates the trunk, outputs trunk_id), then AI voice call (dials the number, the agent talks, returns the transcript). The number you entered rings — an AI agent, not you, speaks to whoever answers.
How it works
trigger (phone_number)
└─▶ livekit-sip · create_outbound_trunk ── registers your provider → trunk_id
└─▶ livekit-cold-call ── for each customer:
1. create a LiveKit room
2. start the AI voice agent in it (STT → LLM → TTS)
3. CreateSIPParticipant → dial the number through the trunk
4. wait_until_answered: block until pickup (or no-answer/busy)
5. agent converses; on hang-up the room empties and the call ends
6. collect { status, duration_seconds, transcript }
Every SIP API call is authorized with a LiveKit token carrying the sip grant (admin + call) — required by CreateSIPParticipant and the trunk-management methods. The trunk step is one-time setup; once you have a trunk_id you can delete the setup_trunk step and pass the ID straight into the call step (avoids creating a duplicate trunk on every run).
Inbound calls — letting people call you
Inbound needs one more piece beyond an inbound trunk: a dispatch rule. When a call arrives at your number, LiveKit's SIP service accepts it but has no idea which room to put the caller in — the dispatch rule answers that question. Without one, inbound calls fail silently (no room is ever created).
Step 5 — Point your number at LiveKit for inbound
In Twilio, the same number from Step 2 can receive calls:
- Phone Numbers → your number → Voice Configuration.
- Set "A call comes in" to SIP Trunk → your Elastic SIP Trunk's origination URI pointing at LiveKit's SIP signaling address (LiveKit Cloud projects show this under Settings → SIP; self-hosted, it's your
livekit-sipservice's public address).
This is the reverse direction of Step 2 (which sends outbound calls to Twilio) — now Twilio sends inbound calls to LiveKit.
Step 6 — Import and run both inbound workflows
- Workflows → Import, choose
examples/livekit_inbound_call_setup.goagents.json. Open it, Run once (manual trigger). This callscreate_inbound_trunk(registersSIP_FROM_NUMBERfor inbound) andcreate_dispatch_rule(routes every call on that trunk into a room namedinbound-support). - Workflows → Import, choose
examples/livekit_inbound_call_agent.goagents.json. Toggle it Enabled and leave it running — it has alivekitwebhook trigger (room_started,room_filter: inbound-support). - In Settings, configure LiveKit to send webhooks to
https://your-domain/webhooks/livekit(see thelivekittrigger's setup hint).
Now call your number. LiveKit creates the inbound-support room, fires room_started, OrcFlows' webhook handler starts the AI Agent workflow, and livekit-voice-agent joins the room and greets the caller.
How it works
inbound call ──▶ create_inbound_trunk (Setup, one-time)
└─▶ create_dispatch_rule (rule_type: direct, room_name: "inbound-support")
└─▶ caller is added to room "inbound-support" (created on demand)
└─▶ LiveKit fires webhook: room_started
└─▶ "livekit" trigger (room_filter: inbound-support)
└─▶ livekit-voice-agent · start
joins the room, talks (STT → LLM → TTS)
Dispatch rule types
The example uses rule_type: direct — every caller joins the same fixed room (room_name). Simple, and the room_filter exact-match on the AI Agent workflow's livekit trigger works out of the box. Tradeoff: if two people call at once, they land in the same room together.
For one-room-per-caller, use rule_type: individual with a room_prefix (e.g. call-) — LiveKit creates a uniquely-named room per call (call-<random>). To auto-join an agent per call without a webhook round-trip, set the dispatch rule's agent_name field — LiveKit dispatches a registered LiveKit Agents worker directly via room_config.agents. That requires running a separate Agents worker process (Python/Node), which is outside OrcFlows' built-in Go voice agent — useful if you're scaling to many concurrent calls.
Doing more
- Campaigns —
livekit-cold-call'scustomersaccepts a list (or{{ steps.fetch.results }}from a database/sheet step) and dials them sequentially. - Transfer to a human —
livekit-sipwithaction: transfermoves a live caller to another number. - Manage dispatch rules —
livekit-sipwithaction: list_dispatch_rules/delete_dispatch_ruleto inspect or clean up routing rules. - Tune the voice — swap
stt_provider/tts_providerto Deepgram, ElevenLabs or NVIDIA NIM; see the LiveKit & Voice Agents node reference.
Troubleshooting
| Symptom | Cause |
|---|---|
customers must resolve to an array | Fixed — paste a literal [{...}] or wire {{ steps.x.results }}. |
dial_failed immediately, no ring | No SIP service (running --dev), or the provider trunk host/credentials are wrong. |
403 / permission denied on the trunk step | LiveKit token missing the SIP grant — update to the current build. |
| Call connects but agent is silent | Check OPENAI_API_KEY; confirm STT/LLM/TTS providers have valid keys. |
no_answer on a number you control | Provider rejected the caller ID (SIP_FROM_NUMBER must be a number you own on that trunk) or geographic permissions are off. |
| Inbound call rings then drops, no room created | No dispatch rule (or trunk_ids on the rule doesn't match your inbound trunk's ID) — run the inbound Setup workflow. |
| Room is created but no AI agent joins | AI Agent workflow not enabled, LiveKit webhook not configured/pointing at /webhooks/livekit, or room_filter doesn't match the dispatch rule's room_name/room_prefix. |