Call queues
A queue holds callers until one of its agents is free. Like a ring group or a menu it is a document of its own, named within its tenant.
configVersion: lyno/v1
kind: Queue
metadata:
name: support
tenant: acme
spec:
strategy: fewestrecent
agents: ["100", "101"]
ringTimeout: 20s
maxWait: 5m
moh: default
announce: ../sounds/still-holding.wav
announceEvery: 45s
joinEmpty: false
configVersion: lyno/v1
kind: Extension
metadata:
name: "300"
tenant: acme
spec:
description: Support queue
steps:
- id: wait
type: queue
queue: support
timeout: 3m
moh: jazz
- id: after-hours
type: voicemail
mailbox: "100"
| Key | Required | Default | Meaning |
|---|---|---|---|
strategy | yes | — | ringall, linear, roundrobin or fewestrecent. |
agents | yes* | — | Account extensions of this tenant. |
ringTimeout | no | 20s | One attempt at one turn's agents, not the whole wait. |
maxWait | no | forever | Then the caller is released to the next step. |
moh | no | ringback | Music-on-hold class while waiting. |
announce | no | — | Played periodically to a waiting caller. |
announceEvery | no | — | How often. Requires announce. |
joinEmpty | no | false | Queue a caller even when nobody could answer. |
dynamic | no | false | See the caveat below. |
* Either agents or dynamic: true is required; a queue with neither is a
validation error, because nothing could ever answer.
The strategies
An agent is offered a call only when it is free: staffed, not paused, not on another call, and not already reserved by another caller waiting in the same queue. That last one is what stops two callers ringing one phone.
| Strategy | Who is offered the call |
|---|---|
ringall | Every free agent at once; the fastest hand wins. |
linear | Always restarts at the top of the list — the first free agent. |
roundrobin | Continues after the agent offered last. |
fewestrecent | The free agent idle the longest; an agent never rung yet wins outright. |
ringall is also the fallback for a strategy string the parser does not
recognise, though validation rejects one first.
"Idle the longest" is measured from the last time the agent was offered a
call, answered or not. Without that, fewestrecent would keep picking the same
dead phone.
Callers are served strictly first-in, first-out. If the caller at the head cannot be offered anybody, the walk stops rather than letting the caller behind them jump the line.
Timing
The two timeouts answer different questions and it is worth keeping them apart:
ringTimeoutbounds one attempt at one turn's agents. When it expires the queue picks the next turn and rings again. It is not cut short bymaxWait, so a phone that started ringing is allowed to finish ringing.maxWaitbounds the whole wait, measured from the moment the caller joined. When it expires the caller leaves the queue and the plan continues with the next step.
A queue step's own timeout: also becomes a maximum wait, and it can only
shorten the queue's maxWait, never lengthen it. That is how one dialplan
holds a caller for less time than another using the same queue.
The fall-through is the whole reason "queue, then voicemail" needs nothing special — the same rule as a dial nobody answered.
Music and announcements
The caller hears the music-on-hold class for the whole wait, including while
an agent's phone is ringing; it stops only when somebody actually answers. A
step's moh: overrides the queue's own class.
announceEvery is a floor, not a schedule. Music and announcement share one
playback on the call — two at once would overlap in the RTP stream — so the
announcement waits for the current pass of music to finish. Expect it a little
later than the interval, never earlier.
A music class that cannot start is a warning, not a failure: the caller waits in silence rather than losing their place.
An empty queue
With joinEmpty: false (the default) a caller who arrives when no agent is
staffed is not queued at all. The step returns immediately and the plan
continues, so they reach the voicemail step in the same second instead of
holding for five minutes to reach it anyway.
"Staffed" is narrower than it sounds. It means not paused, and for a dynamic
agent logged in — registration is never consulted. An agent listed in
agents: therefore counts as staffed with their phone switched off, so on a static
roster joinEmpty makes no observable difference: the queue rings a phone that is
not there, ringTimeout expires, and the caller waits for maxWait.
Where it does decide something is a queue that relies on dynamic: true alone.
Those agents can never log in — see the next section — so joinEmpty: false makes
such a queue refuse every caller.
Dynamic agents do not work
dynamic: true and the queue feature codes are inertdynamic is accepted, validated and offered by the editor, and the
queueLogin: / queueLogout: feature codes exist in the tenant schema — but
nothing can call them. There is no queueLogin dialplan step, and the
queue's login and logout functions have no callers in the running system.
An agent listed in agents: works normally. An agent who is not listed can never
join. Treat agents: as the complete roster, and use dynamic: true only to
satisfy validation on a queue you are still building.
Agent state is lost on restart
Which agents are busy, which were rung last, and how long each has been idle all
live in memory. A restart rebuilds every queue from the YAML, so
fewestrecent and roundrobin start from a clean slate.
This is the opposite of do-not-disturb and forwarding, which are written to disk.
Do-not-disturb applies inside a queue
An agent with DND set contributes no ring targets, exactly like an agent whose phone is not registered. The queue simply moves on to the next turn.
What is not exposed
There is no queue view on the dashboard and no queue route on the management API. Queue depth, the longest wait and the agent roster are not reported anywhere at run time — the log is the only place a queued call shows up.
At shutdown every waiting caller is released before the drain begins, so a
restart does not block for the full maxWait of the last caller.
Reference
Key-by-key schema: the Queue document.