AgentRunner. This page traces both directions, the states a call moves
through, and how it can end.
The state machine
Visible status | Internal state | What is happening |
|---|---|---|
pending | queued | Enqueued; waiting for a concurrency slot (outbound only). |
ringing | dialing / ringing | The SIP leg is originating, or the far end is ringing. |
active | active | Both legs connected; your agent is talking to the caller. |
completed | ended | The conversation finished after being active. |
failed | not_connected / canceled | Never reached a live conversation, or canceled pre-connect. |
Inbound calls skip
pending and dialing - the caller is already on the line,
so the call starts at ringing/active. Only outbound calls pass through the
full sequence. You never drive these states yourself; you observe them via the
call status, the call_end hook, and the final end_reason.Outbound: what happens after calls.create()
calls.create() is asynchronous. It does not wait for the phone to ring -
it puts your call on a queue and returns immediately with status: "pending".
The actual dialing happens in the background, gated by your plan’s per-account
concurrency limit.
You enqueue the call
calls.create(pipe_id, to_number, ...) records the call and returns 201
with status: "pending". Your request is never blocked on the network.Unpod gates on concurrency
A background worker picks up the call. If your account is at its
concurrent-call cap, the call is automatically rescheduled and retried
shortly - no error, no dropped call.
Unpod dials the number
Unpod creates a room and originates the SIP call to
to_number. The call
row moves to ringing, and you get a session_id.Inbound: what happens when someone calls your number
Inbound is simpler - there is no queue, because there is nothing to rate-limit. The moment a caller dials a number attached to your Speech Pipe, the call is already live and Unpod connects your agent to it.Caller dials your number
The carrier delivers the call over SIP. LiveKit answers, creates a room,
and adds the caller as a participant.
The audio + transcript path
Once both legs are in the room, every call works the same way. Unpod’s speech stack transcribes the caller and streams plain text to yourAgentRunner;
your dialog logic replies with text; Unpod synthesizes it back to speech.
The animated lifecycle above is the same loop in motion: call audio stays on
Unpod’s side, text crosses into your runner, and reply text comes back for TTS.
Your
agent_id selects which dialog brain runs for the call - it is not
how Unpod routes the phone number. Number routing is handled by the
Speech Pipe the number is attached to.Voicemail detection
Unpod automatically detects when an outbound call has reached a voicemail system so your agent does not waste a turn talking to a machine. There are two detection points:Pre-connect (during ringing)
Many carriers route “forwarded to voicemail” to a SIP user-unavailable
signal after a brief ring. When Unpod sees this pattern, it classifies the
call as voicemail before it ever connects, ends the call, and reports
end_reason: "VOICEMAIL_DETECTED_PRECONNECT".End reasons
When a call finishes, Unpod records why. These are the reasons you are most likely to act on:end_reason | Meaning |
|---|---|
USER_HUNG_UP_IN_CALL | The caller hung up during the conversation. |
USER_DID_NOT_PICK_UP | Outbound call rang out with no answer. |
USER_HUNG_UP_RINGING | The caller rejected the call while ringing. |
AGENT_HUNG_UP_SILENCE_DETECTED | The agent ended the call after prolonged user silence. |
AGENT_HUNG_UP_VOICEMAIL_DETECTED | Voicemail detected after connect; agent hung up. |
VOICEMAIL_DETECTED_PRECONNECT | Voicemail detected during ringing; call never connected. |
MAX_DURATION_REACHED | The hard per-call duration cap was hit. |
IDLE_TIMEOUT | The call was idle too long and was reaped. |
SIP_FAILED_WRONG_NUMBER | The number was invalid or unreachable. |
SIP_FAILED_NUMBER_BLOCKED | The carrier blocked the call. |
This is not the full set - additional reasons exist for SIP/trunk
configuration errors and handover edge cases. Treat unknown reasons as a
generic failure and log them.
Reacting to outcomes in your agent
Observe telephony outcomes through hooks rather than polling:Related
- Outbound Calls - the
calls.create()API in detail - Speech Pipes - what ties a number, voice profile, and agent together
- Hooks & Events - every lifecycle event you can hook
- AgentRunner & Sessions - ending and transferring live calls