Chapter 12. Automatic Call Distribution Queues

An Englishman, even if he is alone, forms an orderly queue of one.

George Mikes

Automatic call distribution (ACD), or call queuing, provides a way for a PBX to queue up incoming calls from a group of users. It aggregates multiple calls into a holding pattern, assigns each call a rank, and determines the order in which that call should be delivered to an available agent (typically, first in first out). When an agent becomes available, the highest-ranked caller in the queue is delivered to that agent, and everyone else moves up a rank.

If you have ever called an organization and heard “all of our representatives are busy,” you have experienced ACD. The advantage of ACD to the callers is that they don’t have to keep dialing back in an attempt to reach someone, and the advantages to the organizations are that they are able to better serve their customers and to temporarily handle situations where there are more callers than there are agents.1

Note

There are two types of call centers: inbound and outbound. ACD refers to the technology that handles inbound call centers, whereas the term Dialer (or Predictive Dialer) refers to the technology that handles outbound call centers. In this book we will primarily focus on inbound calling.

We’ve all been frustrated by poorly designed and managed queues: enduring hold music from a radio that isn’t in tune, mind-numbing wait times, and pointless messages that tell you every 20 seconds how important your call is, despite that fact that you’ve been waiting for 30 minutes and have heard the message so many times you can quote it from memory. From a customer service perspective, queue design may be one of the most important aspects of your telephone system. As with an automated attendant, what must be kept in mind above all else is that your callers are not interested in holding in a queue. They called because they want to talk to you. All your design decisions must keep this crucial fact front-and-center in your mind: people want to talk to other people, not to your phone system.2

The purpose of this chapter is to teach you how to create and design queues that get callers to their intended destinations as quickly and painlessly as possible.

Note

In this chapter, we may flip back and forth between the usage of the terms queue members and agents. Since we’re not going to spend much time on the Asterisk module named chan_agent (using AgentLogin()), we need to make it clear that in this book, when we use the term agent, we’re referring to an endpoint—a human being, and not the channel technology in Asterisk named chan_agent. Read on, and this should make more sense.

Creating a Simple ACD Queue

To start with, we’re going to create a simple ACD queue. It will accept callers and attempt to deliver them to a member of the queue.

Note

In Asterisk, the term member refers to a channel (typically a SIP peer) assigned to a queue that can be dialed, such as SIP/0000FFFF0001. An agent technically refers to the Agent channel also used for dialing endpoints. Unfortunately, the Agent channel is a deprecated technology in Asterisk, as it is limited in flexibility and can cause unexpected issues that can be hard to diagnose and resolve. We will not be covering the use of chan_agent, so be aware that we will generally use the term member to refer to the telephone device and agent to refer to the person who handles the call. Since one isn’t generally effective without the other, either term may refer to both.

We’ll create the queue(s) in the queues.conf file, and manually add queue members to it through the Asterisk console. In the section “Queue Members”, we’ll look into how to create a dialplan that allows us to dynamically add and remove queue members (as well as pause and unpause them).

The first step is to create an empty agents.conf file in your /etc/asterisk configuration directory. We will not use or edit this file, but the app_queue module expects to find it, and will not load if it does not exist:

$ cd /etc/asterisk

$ sudo -u asterisk touch agents.conf

Since we haven’t done so yet, we’re also going to configure basic music on hold, using the sample file:

$ sudo cp ~/src/asterisk-16.<TAB>/configs/samples/musiconhold.conf.sample 
/etc/asterisk/musiconhold.conf 

$ sudo chown asterisk:asterisk /etc/asterisk/musiconhold.conf

Next you need to create the queues.conf file, which we’re not going to edit because we’ll be creating our queues in the database (it just needs to be there):

$ sudo touch -u asterisk queues.conf

Next, we’re going to create some queues in our database:

MySQL> INSERT INTO `asterisk`.`queues` 
(name,strategy,joinempty,leavewhenempty,ringinuse,autofill,musiconhold, 
monitor_format,monitor_type) 

VALUES
'sales','rrmemory','unavailable,invalid,unknown','unavailable,invalid,unknown','no','yes',
'default','wav','MixMonitor'), 
('support','rrmemory','unavailable,invalid,unknown','unavailable,invalid,unknown','no',
'yes','default','wav','MixMonitor') ; 

This will give us two queues named sales and support. You can name them anything you want, but we will be using these names later in the book, so if you use different queue names from what we’ve recommended here, make note of your choices for future reference.

We have also defined the parameters outlined in Table 12-1.

Table 12-1. Sample queue parameters
ParameterPurpose
strategy=rrmemoryUse the round robin with memory strategy
joinempty=unavailable,invalid,unknownDo not join the queue when no members available
leavewhenempty=unavailable,invalid,unknownLeave the queue when no members available
ringinuse=noDon’t ring members when already InUse (prevents multiple calls to an agent)
autofill=yesDistribute all waiting callers to available members
musiconhold=defaultPlay music from the [default] class (see musiconhold.conf)

The strategy we’ll employ is rrmemory, which stands for round robin with memory. The rrmemory strategy works by rotating through the agents in the queue in sequential order, keeping track of which agent got the last call, and presenting the next call to the next agent. When it gets to the last agent, it goes back to the top (as agents log in, they are added to the end of the list).

We’ve set joinempty to no since it is generally bad form to put callers into a queue where there are no agents available to take their calls.

Note

You could set this to yes for ease of testing, but we would not recommend putting it into production unless you are using the queue for some function that is not about getting your callers to your agents. Nobody wants to wait in a line that is not going anywhere.

The leavewhenempty option is used to control whether callers should fall out of the Queue() application and continue on in the dialplan if no members are available to take their calls. We’ve set this to yes because you won’t normally want callers waiting in a queue with no logged-in agents.

Note

From a business perspective, you should be telling your agents to clear all calls out of the queue before logging off for the day. If you find that there are a lot of calls queued up at the end of the day, you might want to consider extending someone’s shift to deal with them. Otherwise, they’ll just add to your stress when they call back the next day, in a worse mood.

You can use GotoIfTime() near the end of the day to redirect callers to voicemail, or some other appropriate location in your dialplan, while your agents clear out any remaining calls in the queue.

We’ll want ringinuse to be no, which tells Asterisk not to ring members when their devices are already ringing. The purpose of setting ringinuse to no is to avoid multiple calls to the same member from one or more queues.

Note

It should be mentioned that joinempty and leavewhenempty are looking for either no members logged into the queue, or all members unavailable. Agents that are Ringing or InUse are not considered unavailable, so will not block callers from joining the queue or cause them to be kicked out when joinempty=no and/or leavewhenempty=yes.

The autofill option tells the queue to distribute all waiting callers to all available members immediately. Previous versions of Asterisk would only distribute one caller at a time, which meant that while Asterisk was signaling an agent, all other calls were held (even if other agents were available) until the first caller in line had been connected to an agent (which obviously led to bottlenecks in older versions of Asterisk where large, busy queues were being used). Unless you have a particular need for backward compatibility, this option should always be set to yes.

Verify that your /etc/asterisk/extconfig file contains the following lines:

queues => odbc,asterisk,queues
queue_members => odbc,asterisk,queue_members

Save and reload your queue configuration from the Asterisk CLI:

*CLI> queues reload

Verify that your queues were loaded into memory (don’t forget to ensure an empty agents.conf file exists):

localhost*CLI> queue show
support      has 0 calls (max unlimited) in 'rrmemory' strategy 
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
   No Members
   No Callers

sales        has 0 calls (max unlimited) in 'rrmemory' strategy 
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
   No Members
   No Callers

The output of queue show provides various pieces of information, including those parts detailed in Table 12-2.

Table 12-2. Output of queue show CLI command
FieldDescription
W:Queue weight
C:Number of calls presented to this queue
A:Number of calls that have been answered by a member
SL:Service level

Now that you’ve created the queues, you need to configure your dialplan to allow calls to enter the queue.

Add the following dialplan logic to the extensions.conf file (somewhere in the [sets] context):

exten => 610,1,Noop()
  same => n,Progress()
  same => n,Queue(sales)
  same => n,Hangup()

exten => 611,1,Noop()
  same => n,Progress()
  same => n,Queue(support)
  same => n,Hangup()

Save the changes to your extensions.conf file, and reload the dialplan with the dialplan reload CLI command.

If you dial extension 610 or 611 at this point, you will end up with output like the following:

  == Setting global variable 'SIPDOMAIN' to '172.29.1.178'
  -- Executing [610@sets:1] NoOp("PJSIP/SOFTPHONE_A-00000004", "") in new stack
  -- Executing [610@sets:2] Progress("PJSIP/SOFTPHONE_A-00000004", "") in new stack
  -- Executing [610@sets:3] Queue("PJSIP/SOFTPHONE_A-00000004", "test") in new stack
     > 0x7facc801ed60 -- Strict RTP learning after remote  set to: 172.29.1.166:4022
  -- Started music on hold, class 'testmoh', on channel 'PJSIP/SOFTPHONE_A-00000004'
     > 0x7facc801ed60 -- Strict RTP switching to RTP target 172.29.1.166:4022 as source
     > 0x7facc801ed60 -- Strict RTP learning complete - Locking on 172.29.1.166:4022
  -- Stopped music on hold on PJSIP/SOFTPHONE_A-00000004
== Spawn extension (sets, 610, 3) exited non-zero on 'PJSIP/SOFTPHONE_A-00000004'

Note that you won’t join the queue at this point because there are no agents in the queue to answer calls. We have joinempty=no and leavewhenempty=yes configured, so callers will not be placed into the queue. (This would be a good opportunity to experiment with the joinempty and leavewhenempty options in queues.conf to better understand their impact on queues.)

In the next section, we’ll demonstrate how to add members to your queue (as well as other member interactions with the queue, such as pause/unpause).

Queue Members

Queues aren’t very useful without someone to answer the calls that come into them, so we need a method for allowing agents to be logged into the queues to answer calls. There are various ways of going about this, so we’ll show you how to add members to the queue both manually (as an administrator, via either the CLI or hardcoded in the queue_members table) and dynamically (as the agent, through an extension defined in the dialplan). We’ll start with the Asterisk CLI method, which allows you to easily add members to the queue for testing, with minimal dialplan changes. Next we’ll show how you can define members in the queue_members table. Finally, we’ll show you how to add dialplan logic that allows agents to log themselves into and out of the queues and to pause and unpause themselves in queues they are logged into (this is likely the best method for production).

Controlling Queue Members via the CLI

We can add queue members to any available queue through the Asterisk CLI command queue add. The format of the queue add command is (all on one line):

*CLI> queue add member channel to queue [[[penalty penalty] as 
membername] state_interface interface]

The channel is the channel we want to add to the queue, such as SIP/0000FFFF0003, and the queue name will be something like support or sales—any queue name that exists in /etc/asterisk/queues.conf. For now we’ll ignore the penalty option, but we’ll discuss it in “Advanced Queues” (penalty is used to control the rank of a member within a queue, which can be important for agents who are logged into multiple queues, or have differing skills). We can define the membername to provide details to the queue-logging engine.

The state_interface option informs the queue of the device state to be monitored for this agent. The details of how to work with device states are discussed in Chapter 13. Go ahead and work through that chapter, and then come back here and continue on. Don’t worry, we’ll wait.

Now that you’ve added callcounter=yes to sip.conf (we’ll be using SIP channels throughout the rest of our examples), let’s see how to add members to our queues from the Asterisk CLI.

Adding a queue member to the support queue can be done with the queue add member command:

*CLI> queue add member PJSIP/SOFTPHONE_B to support 

Added interface 'PJSIP/SOFTPHONE_B' to queue 'support'

A query of the queue will verify that our new member has been added:

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy (0s holdtime, 0s talktime), 
W:0, C:0, A:0, SL:0.0%, SL2:0.0% within 0s
   Members:
      PJSIP/SOFTPHONE_B (ringinuse disabled) (dynamic) (Not in use) has taken no calls yet
   No Callers

To remove a queue member, you would use the queue remove member command:

*CLI> queue remove member PJSIP/SOFTPHONE_B from support

Removed interface PJSIP/SOFTPHONE_B from queue 'support'

Of course, you can use the queue show command again to verify that your member has been removed from the queue:

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy (0s holdtime, 0s talktime), 
W:0, C:0, A:0, SL:0.0%, SL2:0.0% within 0s
   Members:
      PJSIP/SOFTPHONE_B (ringinuse disabled) (dynamic) (Not in use) has taken no calls yet
   No Callers

We can also pause and unpause members in a queue from the Asterisk console, with the queue pause member and queue unpause member commands. They take a similar format to the previous commands we’ve been using:

*CLI> queue pause member PJSIP/SOFTPHONE_B queue support reason Callbacks

paused interface 'PJSIP/SOFTPHONE_B' in queue 'support' for reason 'Callbacks'

*CLI> queue show support
support      has 0 calls (max unlimited) in 'rrmemory' strategy 
(0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 0s
   Members: 
      SIP/0000FFFF0001 (dynamic) (paused) (Not in use) has taken no calls yet
   No Callers
*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy (0s holdtime, 0s talktime), 
 W:0, C:0, A:0, SL:0.0%, SL2:0.0% within 0s
   Members:
      PJSIP/SOFTPHONE_B (ringinuse disabled) (dynamic) (paused:Callbacks) (Not in use) 
has taken no calls yet
   No Callers

By adding a reason for pausing the queue member, such as lunchtime, you ensure that your queue logs will contain some additional information that may be useful. Here’s how to unpause the member:

*CLI> queue unpause member PJSIP/SOFTPHONE_B queue support reason FinishedCallBacks 

unpaused interface 'PJSIP/SOFTPHONE_B' in queue 'support' for reason 'FinishedCallbacks'

In a production environment, the CLI would not normally be the best way to control the state of agents in a queue. Instead, there are dialplan applications that allow agents to inform the queue as to their availability.

Defining Queue Members in the queue_members Table

If you define a queue member in the asterisk.queue_members table of the database, that member will always be logged into the queue. This usually doesn’t work well if your members are human beings, since humans tend to get up and move about.

Within each queue definition, you simply define the members thus:

MySQL> insert into `asterisk`.`queue_members`
(queue_name,interface,penalty)

VALUES
'hotline','PJSIP/SOME_NON_HUMAN','0');

In a typical queue (one in which you have a group of people responsible for answering calls), you will find that defining the members in the queue_members table might not serve you well. Human agents usually need to be able to log in and out (and not be automatically logged in whenever the queue is reloaded). We do not recommend defining members in the queue_members table unless they have some other purpose (such as a bank of devices that answer calls, where you want to use the queue to load-balance calls to the device pool, or a ring group, where all phones ring for all calls all the time regardless of whether anyone is sitting near the phone).

Controlling Queue Members with Dialplan Logic

In a call center staffed by live agents, it is most common to have the agents themselves log in and log out at the start and end of their shifts (or whenever they go for lunch, or to the bathroom, or are otherwise not available to the queue).

To enable this, we will make use of the following dialplan applications:

  • AddQueueMember()

  • RemoveQueueMember()

While logged into a queue, it may be that an agent needs to put themself into a state where they are temporarily unavailable to take calls. The following applications will allow this:

  • PauseQueueMember()

  • UnpauseQueueMember()

The Add/Remove applications are used to log in and log out, and Pause/Unpause are used for short periods of agent unavailability. The difference is simply that Pause and Unpause set the member as unavailable/available without actually removing them from the queue. This is mostly useful for reporting purposes (if a member is paused, the queue supervisor can see that they are logged into the queue, but simply not available to take calls at that moment). If you’re not sure which one to use, we recommend that the agents use Add/Remove whenever they are not physically at their phone, and Pause/Unpause when they are at their desk, but temporarily not available.

If in doubt, it’s usually better to have your agents log out.

Let’s build some simple dialplan logic that will allow our agents to indicate their availability to the queue. We are going to use the CUT() dialplan function to extract the name of our channel from our call to the system, so that the queue will know which channel to log into the queue.

We have built this dialplan to show a simple process for logging into and out of a queue, and changing the paused status of a member in a queue. We are doing this only for a single queue that we previously defined in the queues.conf file. The status channel variables that the AddQueueMember(), RemoveQueueMember(), PauseQueueMember(), and UnpauseQueueMember() applications set might be used to Playback() announcements to the queue members after they’ve performed certain functions to let them know whether they have successfully logged in/out or paused/unpaused:

exten => *731,1,Page(${PAGELIST},i,120)

exten => *732,1,Verbose(2,Logging In Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,AddQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${AQMSTATUS}) ; ADDED, MEMBERALREADY, NOSUCHQUEUE
   same => n,Playback(agent-loginok)
   same => n,Hangup()

exten => *733,1,Verbose(2,Logging Out Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,RemoveQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${RQMSTATUS}) ; REMOVED, NOTINQUEUE, NOSUCHQUEUE
   same => n,Playback(agent-loggedoff)
   same => n,Hangup()

exten => *734,1,Verbose(2,Pause Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,PauseQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${PQMSTATUS}) ; PAUSED, NOTFOUND
   same => n,Playback(dictate/paused)
   same => n,Hangup()

exten => *735,1,Verbose(2,Unpause Queue Member)
   same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(endpoint)})
   same => n,UnpauseQueueMember(support,${MemberChannel})
   same => n,Verbose(1,${UPQMSTATUS}) ; UNPAUSED, NOTFOUND
   same => n,Playback(agent-loginok)
   same => n,Hangup()

exten => *98,1,NoOp(Access voicemail retrieval.)

Automatically Logging Into and Out of Multiple Queues

It is quite common for an agent to be a member of more than one queue. Rather than having a separate extension for logging into each queue (or demanding information from the agents about which queues they want to log into), this code uses the Asterisk database (astdb) to store queue membership information for each agent, and then loops through each queue the agents are a member of, logging them into each one in turn.

In order for this code to work, an entry similar to the following will need to be added to the AstDB via the Asterisk CLI. For example, the following would store the member SOFTPHONE_A as being in both the support and sales queues:3

*CLI> database put queue_agent SOFTPHONE_A/available_queues support^sales

You will need to do this once for each agent, regardless of how many queues they are members of.

If you then query the Asterisk database, you should get a result similar to the following:

pbx*CLI> database show queue_agent
/queue_agent/SOFTPHONE_A/available_queues         : support^sales

The following dialplan code is an example of how to allow this queue member to be automatically added to both the support and sales queues. We’ve defined a subroutine that is used to set up three channel variables (MemberChannel, MemberChanType, AvailableQueues). These channel variables are then used by the login (*736), logout (*737), pause (*738), and unpause (*739) extensions. Each of the extensions uses the subSetupAvailableQueues subroutine to set these channel variables and to verify that the AstDB contains a list of one or more queues for the device the queue member is calling from.

Near the end of your extensions.conf file, where you’ve put your subroutines, add the following:

[subSetupAvailableQueues]
; This subroutine is used by the various login/logout/pausing/unpausing routines
; in our multiple queue login example.
;
exten => start,1,Verbose(2,Checking for available queues)
; Get the current channel's peer name
   same => n,Set(MemberChannel=${CHANNEL(endpoint)})
; Get the current channel's technology type
   same => n,Set(MemberChanType=${CHANNEL(channeltype)})
; Get the list of queues available for this agent
   same => n,Set(AvailableQueues=${DB(queue_agent/${MemberChannel}/available_queues)})
; if there are no queues assigned to this agent we'll handle it in the
; no_queues_available extension
   same => n,GotoIf($[${ISNULL(${AvailableQueues})}]?no_queues_available,1)
   same => n,Return()

exten => no_queues_available,1,Verbose(2,No queues available for agent ${MemberChannel})
; playback a message stating the channel has not yet been assigned
   same => n,Playback(silence/1&channel&not-yet-assigned)
   same => n,Hangup()

Next, in your [sets] context, add the following:

; Logging into multiple queues via the AstDB system
exten => *736,1,Verbose(2,Logging into multiple queues per the database values)
; get the available queues for this channel
   same => n,GoSub(subSetupAvailableQueues,start,1())
   same => n,Set(QueueCounter=1)  ; setup a counter variable
; using CUT(), get the first listed queue returned from the AstDB
; Note that we've used '^' as our delimiter
   same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
; While the WorkingQueue channel variable contains a value, loop
   same => n,While($[${EXISTS(${WorkingQueue})}])
; AddQueueMember(queuename[,interface[,penalty[,options[,membername
;  [,stateinterface]]]]])
; Add the channel to a queue, setting the interface for calling
; and the interface for monitoring of device state
; *** This should all be on a single line
     same => n,AddQueueMember(
     ${WorkingQueue},${MemberChanType}/${MemberChannel},,,${MemberChanType}/${MemberChannel})
     same => n,Set(QueueCounter=$[${QueueCounter} + 1])    ; increase our counter
; get the next available queue; if it is null our loop will end
     same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
     same => n,EndWhile()
; let the agent know they were logged in okay
   same => n,Playback(silence/1&agent-loginok)
   same => n,Hangup()

exten => no_queues_available,1,Verbose(2,No queues available for ${MemberChannel})
   same => n,Playback(silence/1&channel&not-yet-assigned)
   same => n,Hangup()

; Used for logging agents out of all configured queues per the AstDB
exten => *737,1,Verbose(2,Logging out of multiple queues)
; Because we reused some code, we've placed the duplicate code into a subroutine
   same => n,GoSub(subSetupAvailableQueues,start,1())
   same => n,Set(QueueCounter=1)
   same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
   same => n,While($[${EXISTS(${WorkingQueue})}])
   same => n,RemoveQueueMember(${WorkingQueue},${MemberChanType}/${MemberChannel})
   same => n,Set(QueueCounter=$[${QueueCounter} + 1])
   same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
   same => n,EndWhile()
   same => n,Playback(silence/1&agent-loggedoff)
   same => n,Hangup()

; Used for pausing agents in all available queues
exten => *738,1,Verbose(2,Pausing member in all queues)
   same => n,GoSub(subSetupAvailableQueues,start,1())
   ; if we don't define a queue, the member is paused in all queues
   same => n,PauseQueueMember(,${MemberChanType}/${MemberChannel})
   same => n,GotoIf($[${PQMSTATUS} = PAUSED]?agent_paused,1:agent_not_found,1)

exten => agent_paused,1,Verbose(2,Agent paused successfully)
   same => n,Playback(dictate/paused)
   same => n,Hangup()

; Used for unpausing agents in all available queues
exten => *739,1,Verbose(2,UnPausing member in all queues)
   same => n,GoSub(subSetupAvailableQueues,start,1())
   ; if we don't define a queue, then the member is unpaused from all queues
   same => n,UnPauseQueueMember(,${MemberChanType}/${MemberChannel})
   same => n,GotoIf($[${UPQMSTATUS} = UNPAUSED]?agent_unpaused,1:agent_not_found,1)

exten => agent_unpaused,1,Verbose(2,Agent paused successfully)
   same => n,Playback(silence/1&available)

; Used by both pausing and unpausing dialplan functionality
exten => agent_not_found,1,Verbose(2,Agent was not found)
   same => n,Playback(silence/1&cannot-complete-as-dialed)

You could further refine these login and logout routines to take into account that the AQMSTATUS and RQMSTATUS channel variables are set each time AddQueueMember() and RemoveQueueMember() are used. For example, you could set a flag that lets the queue member know they have not been added to a queue, or even add recordings or text-to-speech systems to play back the particular queue that is producing the problem. Or, if you’re monitoring this via the Asterisk Manager Interface, you could have a screen pop, or use JabberSend() to inform the queue member via instant messaging, or…(ain’t Asterisk fun?).

Advanced Queues

In this section we’ll take a look at some of the finer-grained queue controls, such as options for controlling announcements and when callers should be placed into (or removed from) the queue. We’ll also look at penalties and priorities, exploring how we can control the agents in our queue by giving preference to a pool of agents, and then increasing that pool dynamically based on the wait times in the queue. Finally, we’ll look at using local channels as queue members, which gives us the ability to perform dialplan tricks prior to connecting the caller to an agent.

Priority Queue (Queue Weighting)

Sometimes you need to add people to a queue at a higher priority than that given to other callers. Perhaps the caller has already spent time waiting in a queue, and an agent has taken some information but realized the caller needed to be transferred to another queue. In this case, to minimize the caller’s overall wait time, it might be desirable to transfer the call to a priority queue that has a higher weight (and thus a higher preference), so it will be answered quickly.

Setting a higher priority on a queue is done with the weight option. If you have two queues with differing weights (e.g., support and support-priority), agents assigned to both queues will be passed calls from the higher-priority queue in preference to calls from the lower-priority queue. Those agents will not take any calls from the lower-priority queue until the higher-priority queue is cleared. (Normally, there will be some agents who are assigned only to the lower-priority queue, to ensure that those calls are dealt with in a timely manner.) For example, if we place queue member James Shaw into both the support and support-priority queues, callers in the support-priority queue will have a preferred standing with James over callers in the support queue.

Let’s take a look at how we could make this work. First, we need to create a new queue that’s similar to the support queue except for the weight option.

MySQL> INSERT INTO `asterisk`.`queues` 
(name,strategy,joinempty,leavewhenempty,ringinuse,autofill,musiconhold,monitor_format,
monitor_type,weight) 

VALUES
('support-priority','rrmemory','unavailable,invalid,unknown','unavailable,invalid,unknown',
'no','yes','default','wav','MixMonitor','10');

With our new queue configured, we can now create two extensions to transfer callers to. This can be done wherever you would normally place your dialplan logic to perform transfers. We’re going to use the LocalSets context, which we’ve previously enabled as the starting context for our devices:

exten => 611,1,Noop()
  same => n,Progress()
  same => n,Queue(support)
  same => n,Hangup()

exten => 612,1,Noop()
  same => n,Progress()
  same => n,Queue(support-priority)
  same => n,Hangup()

exten => *724,1,Noop(Page)

The only other configuration left to do is to make sure some or all of your queue members are placed in both queues.

Queue Member Priority

Within a queue, we can apply a penalty to members in order to lower their preference for being called when there are people waiting in a particular queue. For example, we may penalize queue members when we want them to be a member of a queue, but only receive calls when the queue gets full enough that all our preferred agents are unavailable. By defining different penalties for each member of the queue,4 we can help control the preference for where callers are delivered, but still ensure that other queue members will be available to answer calls if the preferred member is unavailable.

Penalties can also be defined using AddQueueMember(). We’ll modify our multiple queue login to provide the required penalties.

First, let’s update our AstDB to include penalties for a member:

*CLI> database put queue_agent SOFTPHONE_A/penalty 0^2 

*CLI> database show queue agent 

/queue_agent/SOFTPHONE_A/available_queues         : support^sales
/queue_agent/SOFTPHONE_A/penalty                  : 0^2

Next, a few tweaks to our dialplan.

The subroutine needs a new line (some code has been removed for brevity, replaced with ;...):

[subSetupAvailableQueues]
; ...
; Get the list of queues available for this agent
   same => n,Set(AvailableQueues=${DB(queue_agent/${MemberChannel}/available_queues)})
   same => n,Set(MemberPenalties=${DB(queue_agent/${MemberChannel}/penalty)})
; if there are no queues assigned ...

The [sets] context requires a couple of new lines as well (some code has been removed for brevity, replaced with ;...). Only insert/change the code written in bold.

exten => *736,1,Verbose(2,Logging into multiple queues per the database values)
; ...
   same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
   same => n,Set(WorkingPenalty=${CUT(MemberPenalties,^,${QueueCounter})}) 
; While the WorkingQueue ...
; ...
     same => n,Set(WorkingQueue=${CUT(AvailableQueues,^,${QueueCounter})})
     same => n,Set(WorkingPenalty=${CUT(MemberPenalties,^,${QueueCounter})})
     same => n,EndWhile()
; ...

These examples are probably not suitable for a production environment (we’d use purpose-built MySQL tables for this sort of thing rather than AstDB), but it gives you an idea of how the dialplan can be used to apply dynamic logic to more complex configuration scenarios.

Changing Penalties Dynamically (queuerules)

Using the asterisk.queuerules table, it is possible to define rules that change the values of the QUEUE_MIN_PENALTY and QUEUE_MAX_PENALTY channel variables. The QUEUE_MIN_PENALTY and QUEUE_MAX_PENALTY channel variables are used to control which members of a queue are preferred for servicing callers. Let’s say we have a queue called support, and we have five queue members with various penalties ranging from 1 through 5. If, prior to a caller entering the queue, the QUEUE_MIN_PENALTY channel variable is set to a value of 2 and the QUEUE_MAX_PENALTY is set to a value of 4, only queue members whose penalties are set to values ranging from 2 through 4 will be considered available to answer that call:

   same => n,Set(QUEUE_MIN_PENALTY=2)   ; set minimum member penalty
   same => n,Set(QUEUE_MAX_PENALTY=4)   ; set maximum member penalty
   same => n,Queue(support)             ; entering the queue with min and max 
                                        ; member penalties to be used

What’s more, during the caller’s stay in the queue, we can dynamically change the values of QUEUE_MIN_PENALTY and QUEUE_MAX_PENALTY for that caller. This allows either more or a different set of queue members to be used, depending on how long the caller waits in the queue. For instance, in the previous example, we could modify the minimum penalty to 1 and the maximum penalty to 5 if the caller has to wait more than 60 seconds in the queue.

The sample file ~/src/asterisk-15.<TAB>/configs/samples/queuerules.conf.sample contains an excellent reference for how queue rules work.

The rules are defined using the asterisk.queuerules table. Multiple rules can be created in order to facilitate different penalty changes throughout the call. Let’s take a look at how we might choose to define a rule:

MySQL> insert into `asterisk`.`queue_rules`
(rule_name,time,min_penalty,max_penalty)

VALUES
('more_members',60,5,1);
Note

New rules will affect only new callers entering the queue, not existing callers already holding.

We’ve named the rule more_members and defined the following values:

60

The number of seconds to wait before changing the penalty values.

5

The new QUEUE_MAX_PENALTY.

1

The new QUEUE_MIN_PENALTY.

We can now tell our queues to make use of it.

MySQL> update `asterisk`.`queues`

set defaultrule='more_members' where `name` in ('sales','support')

The queuerules.conf.sample file shows that these rules are quite flexible. If you want fine-grained control over call prioritization, some additional lab work may be worth your while.

Announcement Control

Asterisk has the ability to play several announcements to callers waiting in the queue. For example, you might want to announce the caller’s position in the queue, announce the average wait time, or periodically thank your callers for waiting (or whatever your audio files say). It’s important to carefully tune the values that control when these announcements are played to the callers, because announcing their position, thanking them for waiting, and informing them of the average hold time too frequently is going to tend to annoy them, which is not the goal of these things.

There are several options in the queues table that you can use to fine-tune what and when announcements are played to your callers. The full list of queue options is available in the ~/src/asterisk-15.<TAB>/configs/samples/queues.conf.sample file. Table 12-3 reviews a few of the more useful ones.

Table 12-3. Options related to prompt control timing within a queue
OptionAvailable valuesDescription
announce-frequencyValue in secondsDefines how often we should announce the caller’s position and/or estimated hold time in the queue. Set this value to zero to disable.
min-announce-frequencyValue in secondsIndicates the minimum amount of time that must pass before we announce the caller’s position in the queue again. This is used when the caller’s position may change frequently, to prevent the caller hearing multiple updates in a short period of time.
periodic-announce-frequencyValue in secondsSpecifies how often to make periodic announcements to the caller.
random-periodic-announceyes, noIf set to yes, will play the defined periodic announcements in a random order. See periodic-announce.
relative-periodic-announceyes, noIf set to yes, the periodic-announce-frequency timer will start when the end of the file being played back is reached, instead of from the beginning. Defaults to no.
announce-holdtimeyes, no, onceDefines whether the estimated hold time should be played along with the periodic announcements. Can be set to yes, no, or only once.
announce-positionyes, no, limit, moreDefines whether the caller’s position in the queue should be announced to them. If set to no, the position will never be announced. If set to yes, the caller’s position will always be announced. If the value is set to limit, the caller will hear their position in the queue only if it is within the limit defined by announce-position-limit. If the value is set to more, the caller will hear their position only if it is beyond the number defined by announce-position-limit.
announce-position-limitNumber of zero or greaterUsed if you’ve defined announce-position as either limit or more.
announce-round-secondsValue in secondsIf this value is nonzero, the number of seconds is announced as well, and rounded to the value defined.

Table 12-4 defines the files that will be used when announcements are played to the caller.

Table 12-4. Options for controlling the playback of prompts within a queue
OptionAvailable valuesDescription
musicclassMusic class as defined by musiconhold.confSets the music class to be used by a particular queue. You can also override this value with the CHANNEL(musicclass) channel variable.
queue-thankyouFilename of prompt to playIf not defined, plays the default value (“Thank you for your patience”). If set to an empty value, prompt will not be played at all.
queue-youarenextFilename of prompt to playIf not defined, plays the default value (“You are now first in line”). If set to an empty value, prompt will not be played at all.
queue-thereareFilename of prompt to playIf not defined, plays the default value (“There are”). If set to an empty value, prompt will not be played at all.
queue-callswaitingFilename of prompt to playIf not defined, plays the default value (“calls waiting”). If set to an empty value, prompt will not be played at all.
queue-holdtimeFilename of prompt to playIf not defined, plays the default value (“The current estimated hold time is”). If set to an empty value, prompt will not be played at all.
queue-minutesFilename of prompt to playIf not defined, plays the default value (“minutes”). If set to an empty value, prompt will not be played at all.
queue-secondsFilename of prompt to playIf not defined, plays the default value (“seconds”). If set to an empty value, prompt will not be played at all.
queue-reportholdFilename of prompt to playIf not defined, plays the default value (“hold time”). If set to an empty value, prompt will not be played at all.
periodic-announceA set of periodic announcements to be played, separated by commasPrompts are played in the order they are defined. Defaults to queue-periodic-announce (“All representatives are currently busy assisting other callers. Please wait for the next available representative”).

There’s a ton of flexibility possible when designing a caller’s experience while they’re waiting, but please don’t forget that your callers will never be happy to be waiting in the queue. Also, if you’ve found some half-decent hold music, and your callers are enjoying it, an interruption to play yet another message runs the risk of really setting their blood boiling. When they are finally answered, your poor agents will get the brunt of their anger, even though it is actually your fault.5

So keep your on-hold tweaking simple. Callers know they’re waiting, and they aren’t going to be happy about it. Get them to an agent as quickly as possible, with the bare minimum amount of silliness while they’re holding, and don’t succumb to the temptation of making the queue more important to your callers than it actually is.

Overflow

Unfortunately, your queue will not always get your callers to an agent in a timely manner. When various conditions cause the queue to reject incoming callers, we have an overflow situation. Overflowing out of the queue is done either with a timeout value or when no queue members are available (as defined by joinempty or leavewhenempty). In this section we’ll discuss how to control when overflow happens.

Controlling timeouts

The Queue() application supports two kinds of timeout: one defines the maximum period of time a caller stays in the queue, and the other specifies how long to ring a device when attempting to connect a caller to a queue member. The two are unrelated but can affect each other. In this section we’ll be talking about the maximum period of time a caller stays in the Queue() application before the call overflows to the next step in the dialplan, which could be something like VoiceMail(), or even another queue. Once the call has fallen out of the queue, it can go anywhere that a call could normally go when controlled by the dialplan.

The timeouts are specified in two locations. The timeout that indicates how long to ring queue members for is specified in the queues table. The absolute timeout (how long the caller stays in the queue) is controlled via the Queue() application. To set a maximum amount of time for callers to stay in a queue, simply specify it after the queue name in the Queue() application:

; Queue
exten => 610,1,Noop()
  same => n,Progress()
  same => n,Queue(sales,120)
  same => n,Voicemail(${EXTEN}@queues,u)
  same => n,Hangup()

exten => 611,1,Noop()
  same => n,Progress()
  same => n,Queue(support,120)
  same => n,Voicemail(${EXTEN}@queues,u)
  same => n,Hangup()

exten => 612,1,Noop()
  same => n,Progress()
  same => n,Queue(support-priority,120)
  same => n,Voicemail(${EXTEN}@queues,u)
  same => n,Hangup()

Since we’re sending the calls to voicemail, we’ll need some mailboxes:

MySQL> INSERT INTO `asterisk`.`voicemail` 
(context,mailbox,password,fullname,email) 

VALUES
('queues','610','192837','Queue sales','[email protected]'), 
('queues','611','192837','Queue support','[email protected]'), 
('queues','612','192837','Queue support-priority','[email protected]');

Of course, we could define a different destination, but the VoiceMail() application is a common overflow destination for a queue. Obviously, sending callers to voicemail is not ideal (they were hoping to speak to someone live), so make sure someone checks it regularly and calls your customers back.

Now, let’s say we have set our absolute timeout to 10 seconds, our timeout value for ringing queue members to 5 seconds, and our retry timeout value to 4 seconds. In this scenario, we would ring the queue member for 5 seconds, then wait 4 seconds before attempting another queue member. That brings us up to 9 seconds of our absolute timeout of 10 seconds. At this point, should we ring the second queue member for 1 second and then exit the queue, or should we ring this member for the full 5 seconds before exiting?

We control which timeout value has priority with the timeoutpriority option in the queues table. The available values are app (the default) and conf. If we want the application timeout (the absolute timeout) to take priority, which would cause our caller to be kicked out after exactly 10 seconds (even though it was just starting to ring an agent), we should set the timeoutpriority value to app. If we want the configuration file timeout to take priority and finish ringing the queue member, which will cause the caller to stay in the queue a little longer, we should set timeoutpriority to conf. The default value is app (which is the default behavior in previous versions of Asterisk). Probably in most cases you’ll want to use conf (especially if you want your caller experience to be as non-weird as possible).

MySQL> update `asterisk`.`queues` set timeoutpriority='conf' 
  where name in ('sales','support','support-priority');

The goal is to get callers to agents, yes?

Controlling when to join and leave a queue

Asterisk provides two options that control when callers can join and are forced to leave queues, both based on the statuses of the queue members. The first option, joinempty, is used to control whether callers can enter a queue in the first place. The second option, leavewhenempty, is used to control events that will cause callers already in a queue to be removed from that queue (i.e., if all of the queue members become unavailable). Both options allow for a comma-separated list of values to control this behavior, as listed in Table 12-5.

Table 12-5. Options that can be set for joinempty or leavewhenempty
ValueDescription
pausedMembers are considered unavailable if they are paused.
penaltyMembers are considered unavailable if their penalties are less than QUEUE_MAX_PENALTY.
inuseMembers are considered unavailable if their device status is InUse.
ringingMembers are considered unavailable if their device status is Ringing.
unavailableApplies primarily to agent channels; if the agent is not logged in but is a member of the queue, the channel is considered unavailable.
invalidMembers are considered unavailable if their device status is Invalid. This is typically an error condition.
unknownMembers are considered unavailable if device status is unknown.
wrapupMembers are considered unavailable if they are currently in the wrapup time after the completion of a call.

For joinempty, prior to placing a caller into the queue, all the members are checked for availability using the factors you list as criteria. If all members are deemed to be unavailable, the caller will not be permitted to enter the queue, and dialplan execution will continue at the next priority.6 For the leavewhenempty option, the members’ statuses are checked periodically against the listed conditions; if it is determined that no members are available to take calls, the caller is removed from the queue, with dialplan execution continuing at the next priority.

An example use of joinempty could be:

joinempty=unavailable,invalid,unknown

With this configuration, prior to a caller entering the queue the statuses of all queue members will be checked, and the caller will not be permitted to enter the queue unless at least one queue member is found to have a status that is not unavailable, invalid, or unknown.

The leavewhenempty example could be something like:

leavewhenempty=unavailable,invalid,unknown

In this case, the queue members’ statuses will be checked periodically, and callers will be removed from the queue if no queue members can be found who do not have a status of unavailable, invalid, or unknown.

Previous versions of Asterisk used the values yes, no, strict, and loose as the available values to be assigned. The mapping of those values is shown in Table 12-6.

Table 12-6. Mapping between old and new values for controlling when callers join and leave queues
ValueMapping (joinempty)Mapping (leavewhenempty)
yes(empty)penalty,paused,invalid
nopenalty,paused,invalid(empty)
strictpenalty,paused,invalid,unavailablepenalty,paused,invalid,unavailable
loosepenalty,invalidpenalty,invalid

Using Local Channels

The use of local channels as queue members is a powerful way of executing dialplan code prior to dialing the actual agent’s device. When Queue() decides to present a call to an agent, using local channels allows us to define custom channel variables, write to a logfile, set some limit on call length (e.g., if it is a paid service), send messages of all sorts all over the place, perform database transactions, and perform many of the other actions we might wish to do at that exact moment. Normally, we have no control over when the Queue() application has decided to present a caller to a specific member, but with local channels, we get one final kick at the can, and can even return Congestion(), which will have the effect of returning the caller to the queue, since the queue will not consider this call to have been successfully delivered to an agent (this can be very handy, since some external condition can be evaluated before the call is just fired off to an endpoint).

When using local channels for queues, they are added just like any other channels, typically dynamically through the AddQueueMember() dialplan application.

We’ll need to define the local channel where all the magic happens, and since local channels are typically used in a manner similar to subroutines, we like to name and locate them in the dialplan with the subroutines, with a context name starting with local (akin to how subroutines start with sub). If you’ve been building out your dialplan along with the book, you’ll notice you already have a local channel [localDialDelay]. Add this code somewhere in that part of the dialplan.

[localMemberConnector] 
exten => _[A-Za-z0-9].,1,Verbose(2,Connect ${CALLERID(all)} to Agent at ${EXTEN})
   ; filter out any bad characters, allow alphanumeric chars and hyphen
   same => n,Set(QueueMember=${FILTER(A-Za-z0-9-,${EXTEN})})
   ; assign the first field of QueueMember to Technology; hyphen as separator
   same => n,Set(Technology=${CUT(QueueMember,-,1)})
   ; assign the second field of QueueMember to Device using the hyphen separator
   same => n,Set(Device=${CUT(QueueMember,-,2)})
   ; dial the agent
   same => n,Dial(${Technology}/${Device})
   same => n,Hangup()

This code might not make total sense just yet, but what it’s doing is taking the ${EXTEN} (which is a complex alphanumeric string at this point), and slicing and dicing it to extract the actual channel to be called (i.e., we pass as part of the local channel all the information needed to dial the actual channel).

Let’s look at the AddQueueMember code and see if we can make more sense of this:

exten => *740,1,Noop(Logging in device ${CHANNEL(endpoint)} into the support queue)
   same => n,Set(MemberTech=${CHANNEL(channeltype)})
   same => n,Set(MemberIdent=${CHANNEL(endpoint)})
   same => n,Set(Interface=${MemberTech}/${MemberIdent})
   ;;; THE FOLLOWING SHOULD ALL BE ON ONE LINE
same => n,AddQueueMember(support,Local/${MemberTech}-${MemberIdent}@localMemberConnector
,,,${IF($[${MemberTech} = PJSIP]?${Interface})})
   same => n,Playback(silence/1)
   same => n,Playback(${IF($[${AQMSTATUS} = ADDED]?agent-loginok:agent-incorrect)})
   same => n,Hangup()

Once you’ve input all this and reloaded your dialplan, log into the queue by dialing *740, and let’s see what we’ve got.

*CLI> queue show support

support has 0 calls (max unlimited) in 'rrmemory' strategy (1s holdtime, 0s talktime), 
W:0, C:1, A:1, SL:0.0%, SL2:0.0% within 0s
   Members:
      PJSIP/SOFTPHONE_A (Local/PJSIP-SOFTPHONE_A@localMemberConnector) 
(ringinuse disabled) (dynamic) (Not in use)
   No Callers

The member is now identified to the queue as a local channel named PJSIP-SOFTPHONE_A in the [localMemberConnector] context. (The PJSIP/SOFTPHONE_A channel will be monitored for actual status of the endpoint.) When Queue() decides to send a call to the member, the call will end up in the [localMemberConnector] context, where the EXTEN (PJSIP-SOFTPHONE_A) will be sliced and diced in order to yield our channel type and endpoint,7 which is what will actually be called.

At this point, the purpose of all this extra complexity is not immediately clear. So far we don’t get anything useful out of all this extra code.

So now that we can add devices to the queue using local channels, let’s look at how this might be useful.

Let’s say we have a customer who just can’t stand our best agent. They’re a good customer, so we don’t want to lose them, but it’s our best agent, so we’re not going to fire them.

To set this up, we’re going to assign a caller ID to SOFTPHONE_B, so we have something to match against.

MySQL> UPDATE `asterisk`.`ps_endpoints` SET callerid='SOFTPHONE_B <103>' 
WHERE id='SOFTPHONE_B';

We’re going to build a little trick into our dialplan that will reject the call to the agent if the caller ID matches our sensitive customer.

[localMemberConnector]
exten => _[A-Za-z0-9].,1,Verbose(2,Connect ${CALLERID(all)} to Agent at ${EXTEN})
   same => n,Wait(0.1) ; Prevent loop from completely hogging CPU
   same => n,Set(QueueMember=${FILTER(A-Za-z0-9-_,${EXTEN})}) ; allow alphanum, - , _
   same => n,Set(Technology=${CUT(QueueMember,-,1)}) ; first field, hyphen is separator
   same => n,Set(Device=${CUT(QueueMember,-,2)})     ; second field, hypen separator
   ; is this our mismatched pair?
   same => n,DumpChan()
   same => n,Noop(${CALLERID(all)} : ${Device})
same=>n,GotoIf($["${CALLERID(num)}"="103"&"${Device}"="SOFTPHONE_A"]?rejectcall:ringagent)
   ; dial the agent
   same => n(ringagent),Dial(${Technology}/${Device})
   same => n,Hangup()
   ; send it back!
   same => n(rejectcall),Congestion()
   same => n,Hangup()

The passing back of Congestion() will cause the caller to be returned to the queue (while this is happening, the caller gets no indication that anything is amiss and keeps hearing music until their call is answered by a channel of some sort).8 Ideally, your queue is programmed to try another agent; however, you need to keep in mind that if app_queue determines that this member is still its first choice to present the call to, the call will simply be reconnected to the same agent (and get congestion again, and thus potentially create a CPU-hogging logic loop). To avoid this, you will need to ensure your queue is using a distribution strategy such as round_robin, random, or any strategy that ensures the same member is not tried over and over. This is also why we toss a tiny little delay into our [localMemberConnector] context, so if a loop like this does happen, there’s at least a small throttle on it.

Let’s just sanity check our code. Set the caller ID number to something other than 103, and the call should go through.

MySQL> UPDATE `asterisk`.`ps_endpoints` SET callerid='SOFTPHONE_B <123>' 
WHERE id='SOFTPHONE_B';

The use of local channels for your member channels will not make queue design and debugging easier, but it does give you far more power over your queues than just using app_queue on its own, so if you have a complex queue requirement, the use of local channels will give you a level of control you would not have otherwise.

Queue Statistics: The queue_log File

The queue_log file (commonly located in /var/log/asterisk) contains cumulative event information for the queues defined in your system (such as when a queue is reloaded, when queue members are added or removed, pause/unpause events, and so forth) as well as some call details (e.g., their status and which channels the callers were connected to). The queue log is enabled by default, but it can be controlled via the /etc/asterisk/logger.conf file. There are three options related to the queue_log file specifically:

queue_log

Controls whether the queue log is enabled or not. Valid values are yes or no (defaults to yes).

queue_log_to_file

Controls whether the queue log should be written to a file even when a real-time backend is present. Valid values are yes or no (defaults to no).

queue_log_name

Controls the name of the queue log. The default is queue_log.

The queue log is a pipe-separated list of events. The fields in the queue_log file are as follows:

  • UNIX Epoch timestamp of the event

  • Unique ID of the call

  • Name of the queue

  • Name of bridged channel

  • Type of event

  • Zero or more event parameters

The information contained in the event parameters depends on the type of event. A typical queue_log file will look something like the following:

1530389309|NONE|NONE|NONE|QUEUESTART|
1530409313|CLI|support|PJSIP/SOFTPHONE_B|ADDMEMBER|
1530409467|CLI|support|PJSIP/SOFTPHONE_B|REMOVEMEMBER|
1530409666|NONE|support|PJSIP/SOFTPHONE_B|PAUSE|Callbacks
1530411108|NONE|support|PJSIP/SOFTPHONE_B|UNPAUSE|FinishedCallbacks
1530440239|1530440239.10|support|PJSIP/SOFTPHONE_A|ADDMEMBER|
1530440303|1530440303.16|support|PJSIP/SOFTPHONE_A|REMOVEMEMBER|
1530497165|1530497165.54|support|Local/PJSIP-SOFTPHONE_A@MemberConnector|ADDMEMBER|
1530497388|CLI|support|Local/PJSIP-SOFTPHONE_A@MemberConnector|REMOVEMEMBER|
1530497408|1530497408.60|support|Local/PJSIP-SOFTPHONE_A@localMemberConnector|ADDMEMBER|
1530497506|1530497506.71|support|NONE|ENTERQUEUE||SOFTPHONE_B|1
1530497511|1530497506.71|support|PJSIP/SOFTPHONE_A|CONNECT|5|1530497506.72|4
1530497517|1530497506.71|support|PJSIP/SOFTPHONE_A|COMPLETEAGENT|5|6|1
1530509861|1530509861.134|support|NONE|ENTERQUEUE||SOFTPHONE_B|1
1530509864|1530509861.134|support|PJSIP/SOFTPHONE_A|RINGCANCELED|2224
1530509864|1530509861.134|support|NONE|ABANDON|1|1|3
1530510503|1530510503.156|support|NONE|ENTERQUEUE||103|1
1530510503|1530510503.156|support|PJSIP/SOFTPHONE_A|RINGNOANSWER|0
1530510511|1530510503.156|support|NONE|ABANDON|1|1|8
1530510738|1530510738.163|support|NONE|ENTERQUEUE||123|1
1530510742|1530510738.163|support|PJSIP/SOFTPHONE_A|CONNECT|4|1530510738.164|4
1530510752|1530510738.163|support|PJSIP/SOFTPHONE_A|COMPLETECALLER|4|10|1

As you can see from this example, there may not always be a unique ID for the event. External services, such as the Asterisk CLI, can perform actions on the queue, and in these cases you’ll see something like CLI in the Unique ID field.

The available events and the information they provide are described in Table 12-7.

Table 12-7. Events in the Asterisk queue log
EventInformation provided
ABANDONWritten when a caller in a queue hangs up before his call is answered by an agent. Three parameters are provided for ABANDON: the position of the caller at hangup, the original position of the caller when entering the queue, and the amount of time the caller waited prior to hanging up.
ADDMEMBERWritten when a member is added to the queue. The bridged channel name will be populated with the name of the channel added to the queue.
AGENTDUMPIndicates that the agent hung up on the caller while the queue announcement was being played, prior to them being bridged together.
AGENTLOGINRecorded when an agent logs in. The bridged channel field will contain something like Agent/9994 if logging in with chan_agent, and the first parameter field will contain the channel logging in (e.g., SIP/0000FFFF0001).
AGENTLOGOFFLogged when an agent logs off, along with a parameter indicating how long the agent was logged in for. Note that since you will often use RemoveQueueMember() for agent log off, this parameter may not be written. See the REMOVEMEMBER event instead.
COMPLETEAGENTRecorded when a call is bridged to an agent and the agent hangs up, along with parameters indicating the amount of time the caller was held in the queue, the length of the call with the agent, and the original position at which the caller entered the queue.
COMPLETECALLERSame as COMPLETEAGENT, except the caller hung up and not the agent.
CONFIGURELOADIndicates that the queue configuration was reloaded (e.g., via module reload app_queue.so).
CONNECTWritten when the caller and the agent are bridged together. Three parameters are also written: the amount of time the caller waited in the queue, the unique ID of the queue member’s channel to which the caller was bridged, and the amount of time the queue member’s phone rang prior to being answered.
ENTERQUEUEWritten when a caller enters the queue. Two parameters are also written: the URL (if specified) and the caller ID of the caller.
EXITEMPTYWritten when the caller is removed from the queue due to a lack of agents available to answer the call (as specified by the leavewhenempty parameter). Three parameters are also written: the position of the caller in the queue, the original position at which the caller entered the queue, and the amount of time the caller was held in the queue.
EXITWITHKEYWritten when the caller exits the queue by pressing a single DTMF key on his phone to exit the queue and continue in the dialplan (as enabled by the context parameter in queues.conf). Four parameters are recorded: the key used to exit the queue, the position of the caller in the queue upon exit, the original position the caller entered the queue at, and the amount of time the caller was waiting in the queue.
EXITWITHTIMEOUTWritten when the caller is removed from the queue due to timeout, as specified by the timeout parameter to Queue(). Three parameters are also recorded: the position the caller was in when exiting the queue, the original position of the caller when entering the queue, and the amount of time the caller waited in the queue.
PAUSEWritten when a queue member is paused.
PAUSEALLWritten when all members of a queue are paused.
UNPAUSEWritten when a queue member is unpaused.
UNPAUSEALLWritten when all members of a queue are unpaused.
PENALTYWritten when a member’s penalty is modified. The penalty can be changed through several means, such as the QUEUE_MEMBER_PENALTY() function, the Asterisk Manager Interface, or the Asterisk CLI commands.
REMOVEMEMBERWritten when a queue member is removed from the queue. The bridge channel field will contain the name of the member removed from the queue.
RINGNOANSWERLogged when a queue member is rung for a period of time, and the timeout value for ringing the queue member is exceeded. A single parameter will also be written indicating the amount of time the member’s extension rang.
TRANSFERWritten when a caller is transferred to another extension. Additional parameters are also written, which include the extension and context the caller was transferred to, the hold time of the caller in the queue, the amount of time the caller was speaking to a member of the queue, and the original position of the caller when he entered the queue.a
SYSCOMPATRecorded if an agent attempts to answer a call, but the call cannot be set up due to incompatibilities in the media setup.

a Please note that when the caller is transferred using SIP transfers (rather than the built-in transfers triggered by DTMF and configured in features.conf), the TRANSFER event may not be written.

Conclusion

We started this chapter with a look at basic call queues, discussing what they are, how they work, and when you might want to use one. After building a simple queue, we explored how to control queue members through various means (including the use of local channels, which provide the ability to perform some dialplan logic just prior to connecting to a queue member). Of course, we need the ability to monitor what our queues are doing, so we had a quick look at the queue_log file, and the various fields written as a result of events happening in our queues.

With the information provided in this chapter, you have most of the foundational knowledge required to implement queues in Asterisk.

1 It is a common misconception that a queue can allow you to handle more calls. This is not strictly true: your callers will still want to speak to a live person, and they will only be willing to wait for so long. In other words, if you are short-staffed, your queue could end up being nothing more than an obstacle to your callers. This is the same whether you’re on the phone or at the Walmart checkout. Nobody likes to wait in line. The ideal queue is invisible to the callers, since their calls get answered immediately without them having to wait.

2 There are several books available that discuss call center metrics and available queuing strategies, such as James C. Abbott’s The Executive Guide to Call Center Metrics (Robert Houston Smith).

3 We’re going to use the ^ character as a delimiter. You could probably use another character instead, just so long as it’s not one the Asterisk parser would see as a normal delimiter (and thus get confused by). So avoid commas, semicolons, and so forth.

4 Similar to adding ballast to a jockey or racing car.

5 Just sayin’.

6 If the priority n+1 (from where the Queue() application was called) is not defined, the call will be hung up. In other words, don’t use this functionality unless your dialplan does something useful at the step immediately following Queue().

7 Perhaps we could have used / instead of - as a delimiter, giving us Local/PJSIP/SOFTPHONE_A@localMemberConnector, but we felt that would be more prone to strange syntax errors, and awkward to filter and parse, so we went with -.

8 Obviously, don’t use any dialplan code in your local channel that will answer, such as Answer(), Playback(), and so forth.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset