Call Queues and Agents
Activate this skill when the user is configuring queues.conf on an Asterisk PBX, choosing a ring strategy, managing agents who log in and out, tuning hold announcements, or explaining queue statistics to a call-centre manager. Triggers on "queues.conf," "app_queue," "ringall," "rrmemory," "leastrecent," "penalty," "AddQueueMember," "PauseQueueMember," "wrapuptime," "queue_log," "service level," "abandon rate," "queue show," or "asterisk call center." Covers strategies, penalties and queue rules, static and dynamic members, announcements, wrap-up, the queue_log format, the metrics that matter, and the bugs every queue deployment hits.
You are a VoIP engineer who has run `app_queue` in production call centres on Asterisk 16 through 21, from a five-agent help desk to floors with several hundred agents across dozens of queues. You have explained service level to operations managers, rewritten queue configurations that rang agents who were already on calls, and rebuilt reports from `queue_log` after a vendor dashboard lied. You know which queue options change caller experience and which only change the log. ## Key Points - **The caller's time is the metric.** Strategy, penalties, announcements and timeouts all trade agent convenience against caller wait. Decide which you are optimising before you edit. - **`queue_log` is the truth.** Every dashboard is a query over it. Learn the event format and you can audit any vendor's numbers. - **Offered** = count of `ENTERQUEUE` - **Answered** = count of `CONNECT` - **Abandoned** = count of `ABANDON`; abandonment rate = abandoned / offered - **Service level** = `CONNECT` rows with holdtime at or under `servicelevel` divided by offered (decide whether short abandons under a few seconds count; state the choice) - **ASA** (average speed of answer) = mean holdtime over `CONNECT` - **AHT** (average handle time) = mean calltime over `COMPLETE*` plus mean wrap-up - **Occupancy** = talk plus wrap time divided by logged-in time from `ADDMEMBER`/`REMOVEMEMBER`/`PAUSE` intervals 1. `queue show sales`: every expected member listed, with state `Not in use`, not `Invalid`, `Unavailable`, `Unknown` or `(paused)`. 2. `Invalid` means the interface string does not resolve to a device: check spelling and technology (`PJSIP/101`, not `SIP/101`). 3. `Unknown` or permanently `Not in use` on a Local member means `state_interface` is missing. ## Quick Example ```ini [tiered] penaltychange => 30,+1 ; after 30 seconds, allow penalty up to 1 penaltychange => 90,+2 ; after 90 seconds, allow penalty up to 2 ```
skilldb get asterisk-pbx-skills/call-queues-and-agentsFull skill: 234 linesCall Queues and Agents
You are a VoIP engineer who has run app_queue in production call centres on Asterisk 16 through 21, from a five-agent help desk to floors with several hundred agents across dozens of queues. You have explained service level to operations managers, rewritten queue configurations that rang agents who were already on calls, and rebuilt reports from queue_log after a vendor dashboard lied. You know which queue options change caller experience and which only change the log.
Core Philosophy
A queue is a waiting room with a routing policy. Everything in queues.conf answers one of three questions: who gets the next call, what the caller hears while waiting, and what happens when nobody can take it. Get those three right and the rest is reporting.
- Device state is the foundation. Every member must map to a device whose state Asterisk can see. A member with unknown state is a member the queue will ring while they are busy, or never ring at all.
- Agents are members; members are interfaces.
app_queuerings channel interfaces (PJSIP/101,Local/101@from-queue/n). Login, logout and pause are dialplan or AMI operations on those interfaces, not a separate agent subsystem. - The caller's time is the metric. Strategy, penalties, announcements and timeouts all trade agent convenience against caller wait. Decide which you are optimising before you edit.
queue_logis the truth. Every dashboard is a query over it. Learn the event format and you can audit any vendor's numbers.
Strategies
strategy | Behaviour | Use when |
|---|---|---|
ringall | Ring every available member until one answers | Small teams, pickup-anywhere reception desks |
leastrecent | Member who was least recently called | Fair spread by time, tolerates uneven call lengths |
fewestcalls | Member with the fewest completed calls | Fair spread by count; punishes agents who take long calls |
random | Random available member | Rarely; testing |
rrmemory | Round robin, remembering where it stopped | The default for most floors |
rrordered | Round robin in configuration order | When member order in the file has meaning |
linear | Always start with the first listed member | Overflow tiers, primary/backup routing |
wrandom | Random weighted by penalty | Skill-weighted spread without strict tiers |
ringall on a large team floods every phone with an INVITE per waiting caller and fills phones with missed-call logs; beyond eight or so members switch to rrmemory.
Penalties and Queue Rules
Each member carries a penalty (0 or higher). The queue only rings members whose penalty is within the caller's current QUEUE_MIN_PENALTY and QUEUE_MAX_PENALTY window; with no window set, lower penalties are tried first and higher ones only when nothing lower is available. queuerules.conf raises the window as the caller waits:
[tiered]
penaltychange => 30,+1 ; after 30 seconds, allow penalty up to 1
penaltychange => 90,+2 ; after 90 seconds, allow penalty up to 2
Attach with defaultrule = tiered in the queue or the rule argument of Queue(). penaltymemberslimit stops the queue from ignoring penalties when few members are logged in. This is how you build "specialists first, generalists after a minute" without a second queue.
Configuration
[general]
persistentmembers = yes ; dynamic members survive restarts (stored in AstDB)
autofill = yes ; distribute waiting callers to free agents in parallel
shared_lastcall = yes ; wrapuptime honoured across queues an agent is in
monitor-type = MixMonitor
log_membername_as_agent = yes
[queue-template](!)
musicclass = default
strategy = rrmemory
timeout = 20 ; seconds to ring one member before moving on
retry = 2 ; pause between ring attempts
wrapuptime = 20
ringinuse = no ; do not ring a member already on a call
autopause = yes ; pause a member who does not answer (RINGNOANSWER)
autopausedelay = 60
maxlen = 0 ; 0 = unlimited waiting callers
joinempty = strict ; refuse entry when no usable members
leavewhenempty = strict ; eject callers when the last usable member leaves
servicelevel = 30 ; target for "answered within N seconds" statistic
announce-frequency = 90
min-announce-frequency = 30
announce-holdtime = once
announce-position = limit
announce-position-limit = 5
periodic-announce = queue-periodic-announce
periodic-announce-frequency = 120
reportholdtime = yes
setinterfacevar = yes ; sets MEMBERINTERFACE, MEMBERNAME etc. on the caller channel before bridging
setqueueentryvar = yes ; QEHOLDTIME, QEORIGINALPOS on the caller channel
timeoutrestart = no
memberdelay = 0
[sales](queue-template)
member => Local/101@from-queue/n,0,Alice,PJSIP/101
member => Local/102@from-queue/n,1,Bob,PJSIP/102
member => PJSIP/103,2,Carol
[support](queue-template)
strategy = leastrecent
defaultrule = tiered
The member line is member => interface,penalty,membername,state_interface,ringinuse. A Local/ member lets you run dialplan on the agent leg (screen pops, whisper announcements, per-agent recording), and /n prevents the Local channel from optimising itself away, which would break recording and state. state_interface tells the queue which real device's state to watch, which is mandatory for Local members.
joinempty and leavewhenempty accept yes, no, strict, loose, or a list of member conditions to treat as "empty": paused,penalty,inuse,ringing,unavailable,invalid,unknown,wrapup. strict treats paused, invalid and unavailable members as absent, which matches what an operations manager means by "nobody is there".
Entering the Queue
[queue-entry]
exten => sales,1,Answer()
same => n,Set(QUEUE_MIN_PENALTY=0)
same => n,Set(QUEUE_MAX_PENALTY=0)
same => n,Set(CDR(userfield)=queue:sales)
same => n,Set(__QUEUE=sales) ; inherited by the agent leg
same => n,Queue(sales,tC,,,600)
same => n,NoOp(Left queue: ${QUEUESTATUS} after ${QEHOLDTIME}s from position ${QEORIGINALPOS})
same => n,GotoIf($["${QUEUESTATUS}"="TIMEOUT"]?overflow)
same => n,VoiceMail(sales@default,u)
same => n,Hangup()
same => n(overflow),Queue(overflow,t,,,300)
same => n,VoiceMail(sales@default,u)
same => n,Hangup()
[from-queue]
exten => _1XX,1,NoOp(Queue ${QUEUE} call to ${EXTEN} from ${CALLERID(num)}) ; QUEUE inherited via __
same => n,Set(CALLERID(name)=Q:${CALLERID(name)}) ; agent sees which queue
same => n,Dial(PJSIP/${EXTEN},,tU(sub-agent-answered^${EXTEN}))
same => n,Hangup()
[sub-agent-answered]
exten => s,1,MixMonitor(/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d)}/${UNIQUEID}.wav,b)
same => n,Return()
Queue(queuename,options,URL,announceoverride,timeout,AGI,gosub,rule,position) on 21; versions through 20 have a macro argument before gosub, so confirm positions with core show application Queue. The fifth argument is the total seconds the caller may wait; timeout in queues.conf is the per-member ring time. Options worth memorising: t/T transfer permission, h/H hangup key, r ringback instead of music, n no retries after one pass, c continue in the dialplan when the callee hangs up, C mark cancelled legs as answered elsewhere so phones do not log missed calls, i ignore call forwarding, x/X one-touch MixMonitor, b(sub^args) and B(sub^args) pre-dial subroutines on callee and caller.
${QUEUESTATUS} after Queue returns: TIMEOUT, FULL, JOINEMPTY, LEAVEEMPTY, JOINUNAVAIL, LEAVEUNAVAIL, CONTINUE. Handle each.
Dynamic Members: Login, Logout, Pause
[agent-codes]
exten => *20,1,Set(IFACE=Local/${CALLERID(num)}@from-queue/n)
same => n,AddQueueMember(sales,${IFACE},0,,${CALLERID(name)},PJSIP/${CALLERID(num)})
same => n,Playback(${IF($["${AQMSTATUS}"="ADDED" | "${AQMSTATUS}"="MEMBERALREADY"]?agent-loginok:pbx-invalid)})
same => n,Hangup()
exten => *21,1,RemoveQueueMember(sales,Local/${CALLERID(num)}@from-queue/n)
same => n,Playback(agent-loggedoff)
same => n,Hangup()
exten => *22,1,PauseQueueMember(,Local/${CALLERID(num)}@from-queue/n,,break)
same => n,Playback(${IF($["${PQMSTATUS}"="PAUSED"]?dictate/paused:pbx-invalid)})
same => n,Hangup()
exten => *23,1,UnpauseQueueMember(,Local/${CALLERID(num)}@from-queue/n)
same => n,Hangup()
AddQueueMember(queue,interface,penalty,options,membername,stateinterface,wrapuptime) sets ${AQMSTATUS} to ADDED, MEMBERALREADY or NOSUCHQUEUE. RemoveQueueMember sets ${RQMSTATUS} to REMOVED, NOTINQUEUE, NOSUCHQUEUE or NOTDYNAMIC. PauseQueueMember with an empty queue name pauses the interface in every queue; the reason string lands in queue_log, which is how you report break versus training versus after-call work. From AMI the equivalents are QueueAdd, QueueRemove, QueuePause, QueueStatus and QueueSummary, which is what agent desktops use.
Read-only state from the dialplan: ${QUEUE_MEMBER(sales,logged)}, ${QUEUE_MEMBER(sales,free)}, ${QUEUE_MEMBER(sales,paused,PJSIP/101)}, ${QUEUE_WAITING_COUNT(sales)}, ${QUEUE_MEMBER_LIST(sales)}, and QUEUE_VARIABLES(sales) which populates QUEUEHOLDTIME, QUEUECALLS, QUEUEABANDONED, QUEUESRVLEVELPERF and friends.
CLI: queue show sales, queue add member PJSIP/104 to sales penalty 1 as Dave, queue remove member PJSIP/104 from sales, queue pause member PJSIP/101 queue sales reason lunch, queue unpause member PJSIP/101, queue set penalty 2 on PJSIP/101 in sales, queue set ringinuse no on PJSIP/101, queue reload all, queue reset stats sales.
Announcements and Wrap-Up
Hold announcements draw on the queue-* sound settings (queue-youarenext, queue-thereare, queue-callswaiting, queue-holdtime, queue-minutes, queue-seconds, queue-thankyou) and fire every announce-frequency seconds, never more often than min-announce-frequency. announce-holdtime = once avoids telling a caller the estimate is falling every minute; estimates are computed from recent answered calls and are unreliable for the first few dozen calls after a restart or queue reset stats. periodic-announce takes a comma list of files played in rotation; random-periodic-announce = yes shuffles them.
wrapuptime is the seconds after a completed call before a member is offered another. Per-member wrap-up via AddQueueMember's last argument overrides the queue value. Without shared_lastcall = yes, an agent in two queues is protected only in the queue that just delivered the call.
queue_log
/var/log/asterisk/queue_log (or a table via extconfig.conf: queue_log => odbc,asterisk,queue_log) records pipe-separated lines: epoch|callid|queuename|agent|event|data1|data2|data3. callid is the caller channel's uniqueid, so it joins to CDR.
| Event | Data fields |
|---|---|
ENTERQUEUE | url, caller ID |
CONNECT | holdtime, bridged channel uniqueid, ringtime |
COMPLETECALLER / COMPLETEAGENT | holdtime, calltime, original position (who hung up first is the event name) |
ABANDON | position, original position, waittime |
EXITWITHTIMEOUT, EXITEMPTY | position, original position, waittime |
EXITWITHKEY | key, position, original position, waittime |
RINGNOANSWER | ringtime in milliseconds |
RINGCANCELED | member was rung, caller left or another member answered |
TRANSFER, BLINDTRANSFER, ATTENDEDTRANSFER | destination and timings |
ADDMEMBER, REMOVEMEMBER, PAUSE, UNPAUSE | reason for pause |
QUEUESTART, CONFIGRELOAD, AGENTDUMP | system and agent-hung-up-during-announce |
Core metrics, all from this file:
- Offered = count of
ENTERQUEUE - Answered = count of
CONNECT - Abandoned = count of
ABANDON; abandonment rate = abandoned / offered - Service level =
CONNECTrows with holdtime at or underserviceleveldivided by offered (decide whether short abandons under a few seconds count; state the choice) - ASA (average speed of answer) = mean holdtime over
CONNECT - AHT (average handle time) = mean calltime over
COMPLETE*plus mean wrap-up - Occupancy = talk plus wrap time divided by logged-in time from
ADDMEMBER/REMOVEMEMBER/PAUSEintervals
SELECT DATE(FROM_UNIXTIME(time)) AS day,
SUM(event='ENTERQUEUE') AS offered,
SUM(event='CONNECT') AS answered,
SUM(event='ABANDON') AS abandoned,
SUM(event='CONNECT' AND data1 <= 30) / SUM(event='ENTERQUEUE') AS sl30,
AVG(CASE WHEN event='CONNECT' THEN data1 END) AS asa
FROM queue_log WHERE queuename='sales' GROUP BY day;
Procedure: Diagnosing "Agents Are Not Being Rung"
queue show sales: every expected member listed, with stateNot in use, notInvalid,Unavailable,Unknownor(paused).Invalidmeans the interface string does not resolve to a device: check spelling and technology (PJSIP/101, notSIP/101).Unknownor permanentlyNot in useon a Local member meansstate_interfaceis missing.In usewhile the agent is idle means device state is stuck;core show hintsandpjsip show endpoint 101(device_state_busy_at).- Member penalty outside the caller's window: check
QUEUE_MIN_PENALTY/QUEUE_MAX_PENALTYand the rule. - Agent in wrap-up:
queue showreportshas taken N calls (last was X secs ago); compare withwrapuptime. autopausehas paused them after a missed ring; look forPAUSErows with no reason inqueue_log.core set verbose 5and watch the queue's attempt lines during a test call.
Checklist
ringinuse = noon every human-staffed queue- Every
Local/member has/nand astate_interface joinemptyandleavewhenemptyset deliberately, not defaulted${QUEUESTATUS}handled after everyQueue()servicelevelset so the statistic means somethingpersistentmembers = yesif agents log in dynamicallyshared_lastcall = yesif agents sit in several queues- Pause reasons standardised for reporting
queue_logrotated or written to a database before it reaches gigabytes
Common Mistakes
- Leaving
ringinuseat its default of yes, so agents on a call are rung again and the caller hears ringing into a busy phone. - Local members without
/n: recordings vanish, state goes wrong, and CDRs show the wrong channel. - Confusing the two timeouts: 20 seconds in
queues.confis per ring attempt; 600 inQueue()is the caller's total patience. - Using
fewestcallson a floor with mixed call lengths, which starves agents who just finished a long call of nothing and hands the next call to whoever hangs up fastest. - Announcing estimated hold time immediately after restart with no history, promising two minutes to callers who will wait ten.
ringallwith fifty members, generating fifty INVITEs per caller and a wall of missed-call notifications.- Reporting from AMI
QueueSummarysnapshots rather thanqueue_log, then wondering why the totals do not reconcile. - Stale persistent members after an agent leaves the company;
queue remove memberand check AstDB withdatabase show Queue.
Limits and When Not to Use This
app_queue handles inbound voice queues well into the hundreds of agents per server. It does not do skills-based routing beyond penalties, does not blend outbound, does not support multi-channel (chat, email) work, and its statistics are per-queue rather than per-interaction. When those are requirements, keep Asterisk as the media and queue engine and put routing logic in an ARI application or a contact-centre layer above it. Predictive dialling is a separate discipline with its own compliance rules; do not build it from Queue() and Originate.
Install this skill directly: skilldb add asterisk-pbx-skills
Related Skills
CDR, CEL and Reporting
Activate this skill when the user needs billing-grade call records from an Asterisk PBX: configuring cdr.conf with a CSV or ODBC backend, enabling CEL for per-event detail, adding custom fields to records, writing call reports in SQL, or reconciling minutes against a carrier invoice. Triggers on "CDR," "cdr.conf," "Master.csv," "cdr_adaptive_odbc," "CEL," "cel.conf," "linkedid," "billsec," "disposition," "call report," "ASR," "ACD," "carrier invoice," "CDR(userfield)," or "asterisk billing." Covers what each CDR field really means, backend configuration for CSV and ODBC, CEL event types and how to read them, custom variables, the SQL reports managers actually ask for, and a reconciliation method that survives rounding, time zones and Local channels.
Dialplan Programming
Activate this skill when the user is writing or debugging extensions.conf for an Asterisk PBX: routing inbound DIDs, building outbound rules, structuring contexts, or replacing Macro() with GoSub. Triggers on "asterisk dialplan," "extensions.conf," "pattern matching," "_NXXXXXX," "GoSub," "Dial options," "hangup handler," "priorities," "same => n," "include =>," "DIALSTATUS," or "dialplan reload." Covers contexts, extensions, priorities, pattern matching, variables and functions, subroutines, Dial behaviour and worked dialplans you can paste and adapt.
IVR Design and Implementation
Activate this skill when the user is building an auto-attendant or interactive voice menu on an Asterisk PBX, recording prompts, routing by business hours, or debugging callers who get stuck or hang up in a menu. Triggers on "IVR," "auto attendant," "Background," "WaitExten," "Read()," "GotoIfTime," "business hours," "invalid extension," "timeout extension," "prompt recording," "sln," "asterisk sounds," or "press 1 for." Covers menu design callers tolerate, the applications that collect digits, timeout and error handling, prompt formats and recording, time-based routing and a test procedure that finds the bugs before callers do.
PJSIP Configuration
Activate this skill when the user is configuring pjsip.conf on an Asterisk PBX: registering phones, building a trunk to a SIP provider, fixing NAT audio problems, or migrating from chan_sip. Triggers on "pjsip.conf," "pjsip endpoint," "aor," "identify," "registration," "external_media_address," "rtp_symmetric," "force_rport," "rewrite_contact," "SIP trunk," "pjsip show endpoints," "codecs," "direct_media," or "asterisk pjsip." Covers every section type, the NAT settings that actually matter, codec negotiation, provider trunks with and without registration, phone templates and the CLI commands for verification.
SIP and RTP Troubleshooting
Activate this skill when the user has a call that fails, drops, or has bad or missing audio on an Asterisk PBX and needs to capture a SIP trace, interpret a response code, or diagnose NAT, codec, DTMF or registration problems. Triggers on "one-way audio," "no audio," "pjsip set logger on," "rtp set debug," "sngrep," "tcpdump," "SIP trace," "403 Forbidden," "401 Unauthorized," "408 Request Timeout," "488 Not Acceptable," "registration failed," "call drops after 30 seconds," "codec mismatch," "DTMF not working," or "asterisk troubleshooting." Covers the capture tools, a methodical way to read a trace, what each SIP response means on an Asterisk system, the NAT settings and what each one fixes, and a symptom-to-cause table built from real incidents.
Voicemail and Call Recording
Activate this skill when the user is configuring voicemail.conf, delivering voicemail by email, recording calls with MixMonitor, deciding how long to keep recordings, or wiring recordings and messages into a transcription service on an Asterisk PBX. Triggers on "voicemail.conf," "VoiceMail," "VoiceMailMain," "MWI," "MixMonitor," "call recording," "record calls," "recording retention," "voicemail to email," "externnotify," "wav49," or "asterisk voicemail." Covers mailbox configuration, greetings and folders, email notification, MixMonitor options and post-processing, storage sizing and retention, consent and compliance notices, and transcription hooks.