Skip to main content

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"
KeyRequiredDefaultMeaning
strategyyesringall, linear, roundrobin or fewestrecent.
agentsyes*Account extensions of this tenant.
ringTimeoutno20sOne attempt at one turn's agents, not the whole wait.
maxWaitnoforeverThen the caller is released to the next step.
mohnoringbackMusic-on-hold class while waiting.
announcenoPlayed periodically to a waiting caller.
announceEverynoHow often. Requires announce.
joinEmptynofalseQueue a caller even when nobody could answer.
dynamicnofalseSee 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.

StrategyWho is offered the call
ringallEvery free agent at once; the fastest hand wins.
linearAlways restarts at the top of the list — the first free agent.
roundrobinContinues after the agent offered last.
fewestrecentThe 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:

  • ringTimeout bounds one attempt at one turn's agents. When it expires the queue picks the next turn and rings again. It is not cut short by maxWait, so a phone that started ringing is allowed to finish ringing.
  • maxWait bounds 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 inregistration 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 inert

dynamic 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.