Skip to main content

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:

LayerWhat it doesOptions
LiveKit server + SIP serviceBridges your AI agent's audio to the phone networkLiveKit Cloud (SIP built-in) or self-hosted livekit-sip
A telephony provider + SIP trunkActually places the PSTN call and owns the phone numberTwilio, Telnyx, Plivo, …
OrcFlows workflowOrchestrates the trunk + call + AI agentthe example below

Your local docker-compose won't place real calls. It runs livekit-server --dev, which has no SIP service. For real telephony use LiveKit Cloud (fastest) or deploy the separate LiveKit SIP service. The error dial_failed / no trunk almost always means SIP isn't actually available.

Step 1 — LiveKit with SIP

  1. Create a free project at cloud.livekit.io. SIP is enabled automatically — nothing to deploy.
  2. 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:

  1. Buy a voice-capable number (Phone Numbers → Buy a number).
  2. Create an Elastic SIP Trunk (Elastic SIP Trunking → Trunks → Create).
  3. Under Termination, set a Termination SIP URI — e.g. your-trunk.pstn.twilio.com. This host is your SIP_TRUNK_ADDRESS.
  4. Under Termination → Authentication, add Credential List auth — the username/password become SIP_USERNAME / SIP_PASSWORD.
  5. 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:

SecretValue
LIVEKIT_API_KEYLiveKit API key
LIVEKIT_API_SECRETLiveKit API secret
LIVEKIT_WS_URLwss://<project>.livekit.cloud (or ws://host:7880 self-hosted)
LIVEKIT_HTTP_URLhttps://<project>.livekit.cloud (or http://host:7880)
SIP_TRUNK_ADDRESSprovider SIP host, e.g. your-trunk.pstn.twilio.com
SIP_USERNAMEprovider SIP auth username
SIP_PASSWORDprovider SIP auth password
SIP_FROM_NUMBERyour caller-ID number, E.164, e.g. +15551234567
OPENAI_API_KEYfor 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

  1. Workflows → Import, choose examples/livekit_phone_call.goagents.json.
  2. Open it, toggle it Enabled, click Run, and enter a phone_number to 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:

  1. Phone Numbers → your number → Voice Configuration.
  2. 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-sip service'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

  1. Workflows → Import, choose examples/livekit_inbound_call_setup.goagents.json. Open it, Run once (manual trigger). This calls create_inbound_trunk (registers SIP_FROM_NUMBER for inbound) and create_dispatch_rule (routes every call on that trunk into a room named inbound-support).
  2. Workflows → Import, choose examples/livekit_inbound_call_agent.goagents.json. Toggle it Enabled and leave it running — it has a livekit webhook trigger (room_started, room_filter: inbound-support).
  3. In Settings, configure LiveKit to send webhooks to https://your-domain/webhooks/livekit (see the livekit trigger'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

  • Campaignslivekit-cold-call's customers accepts a list (or {{ steps.fetch.results }} from a database/sheet step) and dials them sequentially.
  • Transfer to a humanlivekit-sip with action: transfer moves a live caller to another number.
  • Manage dispatch ruleslivekit-sip with action: list_dispatch_rules / delete_dispatch_rule to inspect or clean up routing rules.
  • Tune the voice — swap stt_provider / tts_provider to Deepgram, ElevenLabs or NVIDIA NIM; see the LiveKit & Voice Agents node reference.

Troubleshooting

SymptomCause
customers must resolve to an arrayFixed — paste a literal [{...}] or wire {{ steps.x.results }}.
dial_failed immediately, no ringNo SIP service (running --dev), or the provider trunk host/credentials are wrong.
403 / permission denied on the trunk stepLiveKit token missing the SIP grant — update to the current build.
Call connects but agent is silentCheck OPENAI_API_KEY; confirm STT/LLM/TTS providers have valid keys.
no_answer on a number you controlProvider 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 createdNo 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 joinsAI 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.