Skip to content
DocsStart free

Queues & ACD

A queue holds callers waiting for an agent and distributes them by an ACD (Automatic Call Distribution) strategy. In SIP.IO the live ACD brain is the per-account PresenceDO (a single-threaded stateful edge object), and the queue row is the static config it loads.

This page covers the queue model and the distribution mechanics. For the agent side, see Agents & Presence.

{
"id": "q_01j160r23gw5zkpfjme3fwzs5k", "account_id": "acc_01jf18ah3jeb5w6dfp27sgjsbt", "name": "Support",
"strategy": "longest_idle",
"agent_ring_timeout_sec": 20,
"max_wait_sec": 600,
"wrap_up_sec": 10,
"max_no_answer": 3,
"announce_position": 1,
"announce_first_sec": 15,
"announce_interval_sec": 30,
"moh_mode": "stream",
"language": null,
"timeout_dest_kind": "voicemail", "timeout_dest_id": "vm_01jg8asp59ps9rtt7azhrhvcre",
"no_agents_dest_kind": "flow", "no_agents_dest_id": "flow_01jzqbs3hn9rb3sp06ypecjn0t",
"schedule_id": "sch_01j0jvpe0wzc6g05bktfew0xyx"
}
FieldPurpose
strategyHow the next agent is picked (see below).
agent_ring_timeout_secHow long an agent’s phone rings before it’s a no-answer (the fence lease TTL).
max_wait_secMaximum caller hold before the timeout exit.
wrap_up_secPost-call wrap-up time before the agent takes another call.
max_no_answerAuto-pause an agent after this many consecutive misses (0 = off).
announce_position, announce_first_sec, announce_interval_secPosition announcements.
moh_modestream or file; see Music & Message on Hold.
languageAnnouncement language override (else the account/system default).
*_dest_*Exit destinations: timeout, abandon, no_agents (in-queue) plus queue_full, ewt (deflect at join).
max_queued, ewt_threshold_sec, ewt_aht_secCapacity + estimated-wait gates for the queue_full/ewt deflects.
schedule_id + closed_dest_*/holiday_dest_*Business hours on the queue.

Callers waiting in the queue can also press a key for in-queue options (callback webhook, route out, hang up).

Agents are attached via agent_queue_assignment with a skill tier and a membership mode:

{ "queue_id": "q_01j160r23gw5zkpfjme3fwzs5k", "user_id": "us_01jatt0336xwzg2zbf3q9pyfm5", "tier_level": 1, "position": 0, "delay_sec": 0, "mode": "static" }

tier_level/position/delay_sec set priority within the queue; mode (static·dynamic·mandatory·supervisor_managed) sets who activates the agent. See Agent States & Readiness.

The strategy chooses which ready agent is offered the front caller:

StrategyPicks
longest_idle (default)The agent idle the longest (lower tier first).
round_robinRotates through agents (by idle order within tier).
top_downAlways the highest-priority agent first (tier, then position).
fewest_callsThe agent with the fewest recent calls.
randomA random ready agent.
sequentialIn configured order.
ring_allRing all ready agents at once; first to answer wins.

Tiers matter: agents are ordered by tier_level first (lower tier rings first), so you can model skill levels: a tier-1 specialist is offered before a tier-2 generalist.

An agent is ready to be offered a call only when: available and waiting and reachable (a live SIP registration) and not on an external call and not in wrap-up.

This is the heart of the ACD, and the reason SIP.IO never double-dispatches a caller or an agent. Because the PresenceDO is single-threaded, the whole sequence is atomic: the object is the lock.

agent free ──▶ reserve(agent, caller) ── CAS the "ready" predicate
│ state: waiting → offering
│ epoch++ · leaseId = uuid · offer_expires = now + ring_timeout
│ bind caller to this lease (WHERE lease_id IS NULL)
│ arm an alarm
offer the call to exactly ONE agent (its phone rings)
┌─────────┴───────────────┐
agent answers no answer / crash
│ │
confirm(lease) alarm fires → release(no_answer)
state → in_call caller back to front → re-offer
caller dequeued agent: no_answer_count++ (auto-pause at max)

Reserve compare-and-sets the agent’s ready predicate to offering, bumps an epoch, mints a leaseId, sets offer_expires, and binds the caller to that lease (only if the caller isn’t already bound). It returns a fence token { leaseId, epoch }, or null if the agent was already taken.

Because the winner reserves before the agent’s phone is dialed, the agent only ever receives one offer. That means its 200 OK can be bridged unconditionally, which is exactly why this works with stock SIP phones that have no concept of “confirm before connect.”

Confirm (fired at the moment the agent answers) matches the offering state, epoch, and lease, flips the agent to in_call, and dequeues the caller. It fails closed if anything doesn’t match.

Release ends the offer: on no-answer the caller goes back to the front of the queue for re-offer and the agent’s miss counter increments (auto-pausing at max_no_answer); on answer/hangup the agent enters wrap-up, then is pushed the next caller.

For the ring_all strategy there’s no single pre-reservation: the queue forks all ready agents at once, and the first agent to answer claims the caller (again, atomic in the single-threaded session object; the losers’ legs are cancelled). Non-answering agents are briefly rested before the next round.

The ACD runs in two complementary modes:

  • Pull: while a caller is on a polled hold, the edge polls the queue each cycle; the brain offers an agent only when the caller is at the front and an agent is free. Stale waiters are reaped.
  • Push: when a caller is on a node-local hold (no per-cycle poll), the moment an agent frees the brain finds the front waiting caller in that agent’s queues (in tier order), reserves, and pushes a dispatch to the caller’s edge node over the node-agent channel.

The push path is what keeps hold music gapless and dispatch instant: there’s no polling latency between “agent free” and “caller ringing.”

By default, when a caller enqueues and an agent is already free, the queue connects them immediately and skips the hold entirely: no music, no announcement, just a fast bridge. This is on by default and exposed as an advanced per-queue setting.

There’s an important difference between “all agents are busy” and “there are no agents who could ever take this call.” SIP.IO distinguishes them:

  • If at least one member is reachable and not offline (even if currently busy or paused), the caller waits, and eventually hits the timeout exit.
  • If no member is viable (none registered and logged in), the caller overflows immediately via the no_agents exit, instead of waiting out the full max_wait_sec for help that isn’t coming.

See Overflow & Exit Destinations for wiring the exits.

After a call ends, an agent enters wrap-up for wrap_up_sec before becoming available again, time to disposition the call. If an agent misses max_no_answer consecutive offers, they’re auto-paused (on_break) so the queue stops routing to a phone that isn’t being answered.

A queue can prefer the agent a repeat caller spoke to last, useful for continuity (“get me the person I talked to yesterday”). It’s a per-queue option:

FieldPurpose
sticky_agent1 enables last-agent affinity for the queue.
sticky_modepreferred (soft): use the last agent if free right now, otherwise distribute normally; strict: briefly hold the caller for their agent.
sticky_window_daysHow long an affinity lasts (default 14); older pairings are ignored.
sticky_wait_secIn strict mode, how long to hold for the preferred agent (default 20) before falling back.

Affinity is keyed on the caller (their caller-ID) and recorded when a call bridges. Before normal distribution, the brain checks for a viable preferred agent (available, reachable, not in wrap-up, within the window). In preferred mode a busy preferred agent simply falls through to the normal strategy; in strict mode the caller waits up to sticky_wait_sec, then falls back.

In a multi-region deployment, distribution is location-aware: an agent’s region is tracked from where their device registered, and the queue prefers an agent in the caller’s region first, bridging to a remote agent only when no local one is free. This keeps the media path short without sacrificing answer speed, and the caller’s edge node bridges directly to the agent’s own node.

The PresenceDO exposes live queue stats (waiting callers, longest wait, agent buckets) and emits a delta on every change to power live wallboards.

For a step-by-step, see Set up a call queue.