Skip to main content
Technology & EngineeringAsterisk PBX234 lines

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.

Quick Summary26 lines
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 lines
Paste into your CLAUDE.md or agent config

Call 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_queue rings 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_log is the truth. Every dashboard is a query over it. Learn the event format and you can audit any vendor's numbers.

Strategies

strategyBehaviourUse when
ringallRing every available member until one answersSmall teams, pickup-anywhere reception desks
leastrecentMember who was least recently calledFair spread by time, tolerates uneven call lengths
fewestcallsMember with the fewest completed callsFair spread by count; punishes agents who take long calls
randomRandom available memberRarely; testing
rrmemoryRound robin, remembering where it stoppedThe default for most floors
rrorderedRound robin in configuration orderWhen member order in the file has meaning
linearAlways start with the first listed memberOverflow tiers, primary/backup routing
wrandomRandom weighted by penaltySkill-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.

EventData fields
ENTERQUEUEurl, caller ID
CONNECTholdtime, bridged channel uniqueid, ringtime
COMPLETECALLER / COMPLETEAGENTholdtime, calltime, original position (who hung up first is the event name)
ABANDONposition, original position, waittime
EXITWITHTIMEOUT, EXITEMPTYposition, original position, waittime
EXITWITHKEYkey, position, original position, waittime
RINGNOANSWERringtime in milliseconds
RINGCANCELEDmember was rung, caller left or another member answered
TRANSFER, BLINDTRANSFER, ATTENDEDTRANSFERdestination and timings
ADDMEMBER, REMOVEMEMBER, PAUSE, UNPAUSEreason for pause
QUEUESTART, CONFIGRELOAD, AGENTDUMPsystem 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 = 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
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"

  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.
  4. In use while the agent is idle means device state is stuck; core show hints and pjsip show endpoint 101 (device_state_busy_at).
  5. Member penalty outside the caller's window: check QUEUE_MIN_PENALTY/QUEUE_MAX_PENALTY and the rule.
  6. Agent in wrap-up: queue show reports has taken N calls (last was X secs ago); compare with wrapuptime.
  7. autopause has paused them after a missed ring; look for PAUSE rows with no reason in queue_log.
  8. core set verbose 5 and watch the queue's attempt lines during a test call.

Checklist

  • ringinuse = no on every human-staffed queue
  • Every Local/ member has /n and a state_interface
  • joinempty and leavewhenempty set deliberately, not defaulted
  • ${QUEUESTATUS} handled after every Queue()
  • servicelevel set so the statistic means something
  • persistentmembers = yes if agents log in dynamically
  • shared_lastcall = yes if agents sit in several queues
  • Pause reasons standardised for reporting
  • queue_log rotated or written to a database before it reaches gigabytes

Common Mistakes

  • Leaving ringinuse at 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.conf is per ring attempt; 600 in Queue() is the caller's total patience.
  • Using fewestcalls on 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.
  • ringall with fifty members, generating fifty INVITEs per caller and a wall of missed-call notifications.
  • Reporting from AMI QueueSummary snapshots rather than queue_log, then wondering why the totals do not reconcile.
  • Stale persistent members after an agent leaves the company; queue remove member and check AstDB with database 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

Get CLI access →

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.

Asterisk PBX255L

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.

Asterisk PBX241L

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.

Asterisk PBX180L

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.

Asterisk PBX238L

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.

Asterisk PBX201L

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.

Asterisk PBX168L