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 and assigns each call a rank that 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 service their customers and to temporarily handle situations where there are more callers than there are agents.[126]
There are two types of call centers: inbound and outbound. ACD refers to the technology that handles inbound call centers, whereas the term 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.[127]
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.
In this chapter, we may flip back and forth
between the usage of the terms queue
members and agents. Unless we are talking about agents logged in via
chan_agent
(using AgentLogin()
), we’re almost certainly talking
about queue members as added via AddQueueMember()
or the CLI commands (which
we’ll discuss in this chapter). Just know that there is a difference in
Asterisk between an agent and a queue
member, but that we’ll use the term
agent loosely to simply describe an endpoint as
called by a 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.
In Asterisk, the term
member refers to a 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 your queues.conf file in the /etc/asterisk configuration directory:
$
cd /etc/asterisk/
$
touch queues.conf
Populate it with the following configuration,
which will create 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:
[general] autofill=yes ; distribute all waiting callers to available members shared_lastcall=yes ; respect the wrapup time for members logged into more ; than one queue [StandardQueue](!) ; template to provide common features musicclass=default ; play [default] music strategy=rrmemory ; use the Round Robin Memory strategy joinempty=no ; do not join the queue when no members available leavewhenempty=yes ; leave the queue when no members available ringinuse=no ; don't ring members when already InUse (prevents ; multiple calls to an agent) [sales](StandardQueue) ; create the sales queue using the parameters in the ; StandardQueue template [support](StandardQueue) ; create the support queue using the parameters in the ; StandardQueue template
The [general]
section defines the default behavior and global options. We’ve only
specified two options in the [general]
section, since the built-in defaults
are sufficient for our needs at this point.
The first option is
autofill
, which 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
.
The second option in the [general]
section of queues.conf is
shared_lastcall
. When we enable shared_lastcall
, the
last call to an agent who is logged into multiple queues will be the
call that is counted for wrapup time[128] in order to avoid sending a call to an agent from another
queue during the wrap period. If this option is set to no
, the wrap timer will only apply to the
queue the last call came from, which means an agent who was wrapping up
a call from the support queue might still get a call from the sales
queue. This option should also always be set to yes
(the default).
The next section, [StandardQueue]
is the template we’ll apply to
our sales and support queues (we declared it a template by adding
(!)
). We’ve defined the
musicclass
to be the default
music on hold, as configured in the musiconhold.conf file. 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.
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
it makes no sense to wait in a line that’s not going anywhere.
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.
The alternative is to use GotoIfTime()
near the end of the day to
redirect callers to voicemail, or some other appropriate location in
your dialplan.
Finally, we’ve set
ringinuse
to 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.
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
and/or
joinempty
=no
.leavewhenempty
=yes
Once you’ve finished configuring your queues.conf file, you can save it and reload the app_queue.so module from your Asterisk CLI:
$asterisk -r
*CLI>module reload app_queue.so
-- Reloading module 'app_queue.so' (True Call Queueing)
Then verify that your queues were loaded into memory:
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
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:
[Queues] exten => 7001,1,Verbose(2,${CALLERID(all)} entering the support queue) same => n,Queue(support) same => n,Hangup() exten => 7002,1,Verbose(2,${CALLERID(all)} entering the sales queue) same => n,Queue(sales) same => n,Hangup() [LocalSets] include => Queues ; allow phones to call queues
We’ve included the Queues
context in the LocalSets
context so that our telephones can
call the queues we’ve set up. In Chapter 15, we’ll
define menu items that go to these queues. Save the changes to your
extensions.conf file, and reload
the dialplan with the dialplan reload
CLI command.
If you dial extension 7001
or
7002
at this point, you will end up
with output like the following:
-- Executing [7001@LocalSets:1] Verbose("SIP/0000FFFF0003-00000001", "2,"Leif Madsen" <100> entering the support queue") in new stack == "Leif Madsen" <1--> entering the support queue -- Executing [7001@LocalSets:2] Queue("SIP/0000FFFF0003-00000001", "support") in new stack [2011-02-14 08:59:39] WARNING[13981]: app_queue.c:5738 queue_exec: Unable to join queue 'support' -- Executing [7001@LocalSets:3] Hangup("SIP/0000FFFF0003-00000001", "") in new stack == Spawn extension (LocalSets, 7001, 3) exited non-zero on 'SIP/0000FFFF0003-00000001'
You don’t join the queue at this point, as there are no agents in
the queue to answer calls. Because we have joinempty=no
and leavewhenempty=yes
configured in
queues.conf, 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).
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, and we’ll show you how to add members to the queue both manually (as an administrator) and dynamically (as the agent). We’ll start with the Asterisk CLI method, which allows you to easily add members to the queue for testing and minimal dialplan changes. We’ll then expand upon that, showing you how to add dialplan logic allowing agents to log themselves into and out of the queues and to pause and unpause themselves in queues they are logged into.
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). We can define the
<membername> to provide details to the
queue-logging engine. The state_interface
option is something that we
should delve a bit more into at this junction. Because it is so
important for all aspects of queues and their members in Asterisk,
we’ve written a little section about it, so go ahead and read An Introduction to Device State. Once you’ve set that up, 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 SIP/0000FFFF0001 to support
Added interface 'SIP/0000FFFF0001' 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% within 0s
Members:
SIP/0000FFFF0001 (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 SIP/0000FFFF0001 from support
Removed interface 'SIP/0000FFFF0001' from queue 'support'
Of course, you can use the queue show command again to verify that your member has been removed from the queue.
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 SIP/0000FFFF0001 queue support reason DoingCallbacks
paused interface 'SIP/0000FFFF0001' in queue 'support' for reason 'DoingCallBacks' *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
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 SIP/0000FFFF0001 queue support reason off-break
unpaused interface 'SIP/0000FFFF0001' in queue 'support' for reason 'off-break' *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) (Not in use) has taken no calls yet No Callers
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.
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:
While logged into a queue, it may be that an agent needs to put herself into a state where she is temporarily unavailable to take calls. The following applications will allow this:
It may be easier to think of these applications in the following manner: the add and remove applications are used to log in and log out, and the pause/unpause pair are used for short periods of agent unavailability. The difference is simply that pause/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 she is 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 going to be available to take calls.
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 Unpause
Queue
Member()
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):
[QueueMemberFunctions] exten => *54,1,Verbose(2,Logging In Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,AddQueueMember(support,${MemberChannel}) ; ${AQMSTATUS} ; ADDED ; MEMBERALREADY ; NOSUCHQUEUE exten => *56,1,Verbose(2,Logging Out Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,RemoveQueueMember(support,${MemberChannel}) ; ${RQMSTATUS}: ; REMOVED ; NOTINQUEUE ; NOSUCHQUEUE exten => *72,1,Verbose(2,Pause Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,PauseQueueMember(support,${MemberChannel}) ; ${PQMSTATUS}: ; PAUSED ; NOTFOUND exten => *87,1,Verbose(2,Unpause Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) same => n,UnpauseQueueMember(support,${MemberChannel}) ; ${UPQMSTATUS}: ; UNPAUSED ; NOTFOUND
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 to 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
0000FFFF0001
as being in both the
support
and sales
queues:
*CLI> database put queue_agent 0000FFFF0001/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/0000FFFF0001/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 (*54
), logout (*56
), pause (*72
), and unpause (*87
) extensions. Each of the extensions uses
the subSetup
Available
Queues
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:
[subSetupAvailableQueues] ; ; This subroutine is used by the various login/logout/pausing/unpausing routines ; in the [ACD] context. The purpose of the subroutine is centralize the retrieval ; of information easier. ; exten => start,1,Verbose(2,Checking for available queues) ; Get the current channel's peer name (0000FFFF0001) same => n,Set(MemberChannel=${CHANNEL(peername)}) ; Get the current channel's technology type (SIP, IAX, etc) 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)}) ; *** This should all be on a single line ; 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}) ; *** This should all be on a single line ; playback a message stating the channel has not yet been assigned same => n,Playback(silence/1&channel¬-yet-assigned) same => n,Hangup() [ACD] ; ; Used for logging agents into all configured queues per the AstDB ; ; ; Logging into multiple queues via the AstDB system exten => *54,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 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¬-yet-assigned) same => n,Hangup() ; ------------------------- ; Used for logging agents out of all configured queues per the AstDB exten => *56,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 => *72,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(silence/1&unavailable) same => n,Hangup() ; ------------------------- ; Used for unpausing agents in all available queues exten => *87,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) same => n,Hangup() ; ------------------------- ; 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 he has not been
added to a queue by setting a flag, 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. (Sorry, sometimes our brains run away with
us.)
Device states in Asterisk are used to inform various
applications as to whether your device is currently in use or not.
This is especially important for queues, as we don’t want to send
callers to an agent who is already on the phone. Device states are
controlled by the channel module, and in Asterisk only chan_sip
has the appropriate handling. When
the queue asks for the state of a device, it first queries the channel
driver (e.g., chan_sip
). If the
channel cannot provide the device state directly (as is the case with
chan_iax2
), it asks the Asterisk
core to determine it, which it does by searching through channels
currently in progress.
Unfortunately, simply asking the core to
search through active channels isn’t accurate, so getting device state
from channels other than chan_sip
is less reliable when working with queues. We’ll explore some methods
of controlling calls to other channel types in Advanced Queues, but for now we’ll focus on SIP channels,
which do not have complex device state requirements. For more
information about device states, see Chapter 14.
In order to correctly determine the state of a
device in Asterisk, we need to enable call counters in sip.conf. By enabling call counters, we’re
telling Asterisk to track the active calls for a device so that this
information can be reported back to the channel module and the state
can be accurately reflected in our queues. First, let’s see what
happens to our queue without the callcounter
option:
*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) (Not in use) has taken no calls yet
No Callers
Now suppose we have an extension in our
dialplan, 555
, that calls
MusicOnHold()
. If we dial that
extension without having enabled call counters, a query of the
support
queue (of which SIP/0000FFFF0001
is a member) from the
Asterisk CLI will show something similar to the following:
-- Executing [555@LocalSets:1] MusicOnHold("SIP/0000FFFF0001-00000000", "") in new stack -- Started music on hold, class 'default', on SIP/0000FFFF0001-00000000 *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) (Not in use
) has taken no calls yet No Callers
Notice that even though our phone should be
marked as In Use
because it is on a
call, it does not show up that way when we look at the queue status.
This is obviously a problem since the queue will consider this device
as available, even though it is already on a call.
To correct this problem, we need to add
callcounter=yes
to the [general]
section of our sip.conf file. We can also specifically
configure this for any peer (since it is a peer-level configuration
option); however, this is really something you’ll want to set for all
peers that might ever be part of a queue, so it’s normally going to be
best to put this option in the [general]
section (it could also be assigned
to a template that would be used with all peers in the queue).
Edit your sip.conf file so it looks similar to the following:
[general]
context=unauthenticated ; default context for incoming calls
allowguest=no ; disable unauthenticated calls
srvlookup=yes ; enabled DNS SRV record lookup on outbound calls
udpbindaddr=0.0.0.0 ; listen for UDP request on all interfaces
tcpenable=no ; disable TCP support
callcounter=yes ; enable device states for SIP devices
Then reload the chan_sip
module and perform the same test
again:
*CLI> sip reload
Reloading SIP
== Parsing '/etc/asterisk/sip.conf': == Found
The device should now show In use
when a call is in progress from that
device:
== Parsing '/etc/asterisk/sip.conf': == Found
== Using SIP RTP CoS mark 5
-- Executing [555@LocalSets:1] MusicOnHold("SIP/0000FFFF0001-00000001",
"") in new stack
-- Started music on hold, class 'default', on SIP/0000FFFF0001-00000001
*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) (In use) has taken no calls yet
No Callers
In short, Queue()
needs to know the
state of a device in order to properly manage call distribution. The
callcounter
option in sip.conf is an essential component of a
properly functioning queue.
We’ve mentioned the queues.conf file already, but there are many options in this file, and we figured it would be right and proper for us to go over some of them with you.
Table 13-1
contains the options available in the [general]
section of queues.conf.
Table 13-1. Available options for [general] section of queues.conf
Table 13-2 describes the options available for configuring queue contexts.
Table 13-2. Available options for defined queues in queues.conf
Options | Available values | Description |
---|---|---|
musicclass | Music class as defined by musiconhold.conf | Sets the music class to be used by a particular queue.
You can also override this value with the CHANNEL (musicclass)
channel variable. |
announce | Filename of the announcement | Used for playing an announcement to the agent that answered the call, typically to let him know what queue the caller is coming from. Useful when the agent is in multiple queues, especially when set to auto-answer the queue. |
strategy | ringall , least recent , fewest calls , random , rrmemory , linear , wrandom |
|
servicelevel | Value in seconds | Used in statistics to determine the service level of the queue (calls answered within the service level time frame). |
context | Dialplan context | Allows a caller to exit the queue by pressing a single DTMF digit. If a context is specified and the caller enters a number, that digit will attempt to be matched in the context specified, and dialplan execution will continue there. |
penaltymemberslimit | Value of 0 or
greater | Used to disregard penalty values if the number of members in the queue is lower than the value specified. |
timeout | Value in seconds | Specifies the number of seconds to ring a member’s
device. Also see timeoutpriority . |
retry | Value in seconds | Specifies the number of seconds to wait before attempting
the next member in the queue if the timeout value is exhausted while
attempting to ring a member of the queue. |
timeoutpriority | app , conf | Used to control the priority of the two possible timeout options specified for a queue.
The Queue() application has a
timeout value that can be specified to control the absolute time
a caller can be in the queue. The timeout value in queues.conf controls the amount of
time (along with retry ) to
ring a member for. Sometime these values conflict, so you can
control which value takes precedence. The default is app , as this is the way it works in
previous versions. |
weight | Value of 0 or
higher | Defines the weight of a queue. A queue with a higher weight defined will get first priority when members are associated with multiple queues. |
wrapuptime | Value in seconds | The number of seconds to keep a member unavailable in a queue after completing a call. |
autofill | yes , no | Same as defined in the [general] section. This value can be
defined per queue. |
autopause | yes , no , all | Enables/disables the automatic pausing of members who
fail to answer a call. A value of all causes this member to be paused in
all queues she is a member of. |
maxlen | Value of 0 or
higher | Specifies the maximum number of callers allowed to be waiting in a queue. A value of zero means an unlimited number of callers are allowed in the queue. |
setinterfacevar | yes , no | If set to yes , the
following channel variables will be set just prior to connecting
the caller with the queue member: |
setqueueentryvar | yes , no | If set to yes , the
following channel variables will be set just prior to the call
being bridged: |
setqueuevar | yes , no | If set to yes , the
following channel variables will be set just prior to the call
being bridged: |
membermacro | Name of a macro defined in the dialplan | Defines a macro to be executed just prior to bridging the caller and the queue member. |
announce-frequency | Value in seconds | Defines 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-frequency | Value in seconds | Specifies 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-frequency | Value in seconds | Indicates how often we should make periodic announcements to the caller. |
random-periodic-announce | yes , no | If set to yes , will
play the defined periodic announcements in a random order. See
periodic-announce . |
relative-periodic-announce | yes , no | If set to yes , the
periodic-announce-frequency
timer will start from when the end of the file being played back
is reached, instead of from the beginning. Defaults to no . |
announce-holdtime | yes , no , once | Defines whether the estimated hold time should be played
along with the periodic announcements. Can be set to yes , no , or only once . |
announce-position | yes , no , limit , more | Defines whether the caller’s position in the queue should
be announced to her. 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 her 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 her position if it is beyond the number defined by
announce-position-limit . |
announce-position-limit | Number of zero or greater | Used if you’ve defined announce-position as either limit or more . |
announce-round-seconds | Value in seconds | If this value is nonzero, we’ll announce the number of seconds as well, and round them to the value defined. |
queue-thankyou | Filename of prompt to play | If not defined, will play the default value (“Thank you for your patience”). If set to an empty value, the prompt will not be played at all. |
queue-youarenext | Filename of prompt to play | If not defined, will play the default value (“You are now first in line”). If set to an empty value, the prompt will not be played at all. |
queue-thereare | Filename of prompt to play | If not defined, will play the default value (“There are”). If set to an empty value, the prompt will not be played at all. |
queue-callswaiting | Filename of prompt to play | If not defined, will play the default value (“calls waiting”). If set to an empty value, the prompt will not be played at all. |
queue-holdtime | Filename of prompt to play | If not defined, will play the default value (“The current estimated hold time is”). If set to an empty value, the prompt will not be played at all. |
queue-minutes | Filename of prompt to play | If not defined, will play the default value (“minutes”). If set to an empty value, the prompt will not be played at all. |
queue-seconds | Filename of prompt to play | If not defined, will play the default value (“seconds”). If set to an empty value, the prompt will not be played at all. |
queue-reporthold | Filename of prompt to play | If not defined, will play the default value (“Hold time”). If set to an empty value, the prompt will not be played at all. |
periodic-announce | A set of periodic announcements to be played, separated by commas | Prompts 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”). |
monitor-format | gsm , wav , wav49 , <any valid file
format > | Specifies the file format to use when recording. If
monitor-format is commented out,
calls will not be recorded. |
monitor-type | MixMonitor ,
<unspecified > | Same as monitor-type
as defined in the [general] section, but on a
per-queue basis. |
joinempty | paused , penalty , inuse , ringing , unavailable , invalid , unknown , wrapup | Controls whether a caller is added to the queue when no
members are available. Comma-separated options can be included
to define how this option determines whether members are
available. The definitions for the values are:
|
leavewhenempty | paused , penalty , inuse , ringing , unavailable , invalid , unknown , wrapup | Used to control whether callers are kicked out of the
queue when members are no longer available to take calls. See
joinempty for more
information on the assignable values. |
eventwhencalled | yes , no , vars | If set to yes , the
following manager events will be sent to the Asterisk Manager
Interface (AMI):If set to |
eventmemberstatus | yes , no | If set to yes , the
QueueMemberStatus event will
be sent to AMI. Note that this may generate a lot of manager
events. |
reportholdtime | yes , no | Enables reporting of the caller’s hold time to the queue member prior to bridging. |
ringinuse | yes , no | Used to avoid sending calls to members whose status is
In Use . Recall from our
discussion in the preceding section that only the SIP channel
driver is currently able to accurately report this
status. |
memberdelay | Value in seconds | Used if you want there to be a delay prior to the caller and queue member being connected to each other. |
timeoutrestart | yes , no | If set to yes , resets
the timeout for an agent to answer if either a BUSY or CONGESTION status is received from the
channel. This can be useful if the agent is allowed to reject or
cancel a call. |
defaultrule | Rule as defined in queuerules.conf | Associates a queue rule as defined in queuerules.conf to this queue, which is used to dynamically change the minimum and maximum penalties, which are then used to select an available agent. See Changing Penalties Dynamically (queuerules.conf). |
member | Device | Used to define static members in a queue. To define a
static member, you supply its
Technology /Device_ID
(e.g., Agent/1234 , SIP/0000FFFF0001 , DAHDI/g0/14165551212 ). |
If you’ve browsed through the samples in the ~/src/asterisk-complete/1.8/configs/ directory, you may have noticed the agents.conf file. It may seem tempting, and it has its places, but overall the best way to implement queues is through the use of SIP channels. There are two reasons for this. The first is that SIP channels are the only type that provide true device state information. The other reason is that agents are always logged in when using the agent channel, and if you’re using remote agents, the bandwidth requirements may be greater than you wish. However, in busy call centers it may be desirable to force agents to answer calls immediately rather than having them press the answer button on the phone.
The agents.conf file is use to define agents for
queues using the agents channel. This channel is similar in nature to
the other channel types in Asterisk (local, SIP, IAX2, etc.), but it is
more of a pseudo-channel in that it is used to connect callers to agents
who have logged into the system using other types of transport channel.
For example, suppose we use our SIP-enabled phone to log in to Asterisk
using the AgentLogin()
dialplan
application. Once we’re logged in, the channel remains online the entire
time it is available (logged on), and calls are then passed to it
through the agent channel.
Let’s take a look at the various options
available to us in the agents.conf
file to get a better idea of what it provides us. Table 13-3 shows the single option available in
the [general]
section of agents.conf. Table 13-4 shows the available options under the
[agents]
header.
Table 13-4. Options available under the [agents] header in agents.conf
Options | Available values | Description |
---|---|---|
maxloginretries | Integer value | Specifies the maximum number of tries an agent has to log in before the system considers it a failed attempt and ends the call. Defaults to 3. |
autologoff | Value in seconds | Specifies the number of seconds for which an agent’s device should ring before the agent is automatically logged off. |
autologoffunavail | yes , no | If set to yes , the
agent is automatically logged off when the device being called
returns a status of CHANUNAVAIL. |
ackcall | yes , no | If set to yes , the
agent must enter a single DTMF digit to accept the call. To be
used in conjunction with acceptdtmf . Defaults to no . |
acceptdtmf | Single DTMF character | Used in conjunction with ackcall , this option defines the DTMF
character to be used to accept a call. Defaults to # . |
endcall | yes , no | If set to yes , allows
an agent to end a call with a single DTMF digit. To be used in
conjunction with enddtmf .
Defaults to yes . |
enddtmf | Single DTMF character | Used in conjunction with endcall , this option defines the DTMF
character to be used to end a call. Defaults to * . |
wrapuptime | Value in milliseconds | Specifies the amount of time after disconnection of a caller from an agent for which the agent will not be available to accept another call. Used in situations where agents must perform a function after each call (such as entering call details into a log). |
musiconhold | Music class as defined in musiconhold.conf | Defines the default music class agents listen to when logged in. |
goodbye | Name of file (relative to /var/lib/asterisk/sounds/<lang>/) | Defines the default goodbye sound played to agents. Defaults to vm-goodbye. |
updatecdr | yes , no | Used in call detail records to change the source channel
field to the agent/agent_id . |
group | Integer value | Allows you to define groups for sets of agents.
The use of agent groups is essentially deprecated
functionality that we do not recommend you use. If
you define group1 , you can
use Agent/@1 in queues.conf to call that group of
agents. The call will be connected arbitrarily to one of those
agents. If no agents are available, it will return back to the
queue like any other unanswered call. If you use Agent/:1 , it will wait for a member of
the group to become available. The use of strategies
has no effect on agent groups. Do not use
these. |
recordagentcalls | yes , no | Enables/disables the recording of agent calls. Disabled by default. |
recordformat | File format (gsm , wav , etc.) | Defines the format to be used when recording agent calls.
Default is wav . |
urlprefix | String (URL) | Accepts a string as its argument. The string can be formed as a URL and is appended to the start of the text to be added to the name of the recording. |
savecallsin | Filesystem path (e.g., /var/calls/) | Accepts a filesystem path as its argument. Allows you to override the default path of /var/spool/asterisk/monitor/ with one of your choosing.[a] |
custom_beep | Name of file (relative to /var/lib/asterisk/sounds/<lang>/) | Accepts a filename as its argument. Can be used to define a custom notification tone to signal to an always-connected agent that there is an incoming call. |
agent | Agent definition (see description) | Defines an agent for use by Queue() and AgentLogin() . These are agents that
will log in and stay connected to the system, waiting for calls
to be delivered by the Queue() dialplan application. Agents
are defined like so:agent => An example of a defined agent would be: agent => 1000,1234,Danielle Roberts |
[a] Since the storage of calls will require a large amount of hard drive space, you will want to define a strategy to handle storing and managing these recordings. This location should probably reside on a separate volume, one with very high performance characteristics. |
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 to answer the call and increase 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 functionality prior to connecting the caller to an agent.
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 two queues that are identical except
for the weight
option. We can use a
template for this to ensure that the two queues remain identical if
anything should need to change in the future:
[support_template](!) musicclass=default strategy=rrmemory joinempty=no leavewhenempty=yes ringinuse=no [support](support_template) weight=0 [support-priority](support_template) weight=10
With our queues configured (and subsequently
reloaded using module reload
app_queue.so from the Asterisk console), 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:
[LocalSets] include => Queue ; allow direct transfer of calls to queues [Queues] exten => 7000,1,Verbose(2,Entering the support queue) same => n,Queue(support) ; standard support queue available ; at extension 7000 same => n,VoiceMail(7000@queues,u) ; if there are no members in the queue, ; we exit and send the caller to voicemail same => n,Hangup() exten => 8000,1,Verbose(2,Entering the priority support queue) same => n,Queue(support-priority) ; priority queue available at ; extension 8000 same => n,VoiceMail(7000@queues,u) ; if there are no members in the queue, ; we exit and send the caller to voicemail same => n,Hangup()
There you have it: two queues defined with
different weights. We’ve configured our standard queues to start at
extension 7000
, and our priority
queues to start at 8000
. We can
mirror this for several queues by simply matching between the 7XXX
and 8XXX
ranges. So, for example, if we have our
sales
queue at extension 7004
, our priority-sales
queue (for returning
customers, perhaps?) could be placed in the mirrored queue at 8004
, which has a higher weight.
The only other configuration left to do is
to make sure some or all of your queue members are placed in both
queues. If you have more callers in your 7XXXX
queues, you may want to have more
queue members logged into that queue, with a percentage of your queue
members logged into both queues. Exactly how you wish to configure
your queues will depend on your local policy and circumstances.
Within a queue, we can penalize
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 to be used only when the queue gets full enough that all our
preferred agents are unavailable. This means we can have three queues
(say, support
, sales
, and billing
), each containing the same three
queue members: James Shaw, Kay Madsen, and Danielle Roberts.
Suppose, however, that we want James Shaw to
be the preferred contact in the support
queue, Kay Madsen preferred in
sales
, and Danielle Roberts
preferred in billing
. By penalizing Kay Madsen and
Danielle Roberts in support
, we
ensure that James Shaw will be the preferred queue member called.
Similarly, we can penalize James Shaw and Danielle Roberts in the
sales
queue so Kay Madsen is
preferred, and penalize James Shaw and Kay Madsen in the billing
queue so Danielle Roberts is
preferred.
Penalizing queue members can be done either
in the queues.conf file, if
you’re specifying queue members statically, or through the AddQueueMember()
dialplan application. Let’s
look at how our queues would be set up with static members in
queues.conf. We’ll be using the
StandardQueue
template we defined
earlier in this chapter:
[support](StandardQueue) member => SIP/0000FFFF0001,0,James Shaw ; preferred member => SIP/0000FFFF0002,10,Kay Madsen ; second preferred member => SIP/0000FFFF0003,20,Danielle Roberts ; least preferred [sales](StandardQueue) member => SIP/0000FFFF0002,0,Kay Madsen member => SIP/0000FFFF0003,10,Danielle Roberts member => SIP/0000FFFF0001,20,James Shaw [billing](StandardQueue) member => SIP/0000FFFF0003,0,Danielle Roberts member => SIP/0000FFFF0001,10,James Shaw member => SIP/0000FFFF0002,20,Kay Madsen
By defining different penalties for each
member of the queue, 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()
, as the following example
demonstrates:
exten => *54,1,Verbose(2,Logging In Queue Member) same => n,Set(MemberChannel=${CHANNEL(channeltype)}/${CHANNEL(peername)}) ; *CLI> database put queue support/0000FFFF0001/penalty 0 same => n,Set(QueuePenalty=${DB(queue/support/${CHANNEL(peername)}/penalty)}) ; *CLI> database put queue support/0000FFFF0001/membername "James Shaw" same => n,Set(MemberName=${DB(queue/support/${CHANNEL(peername)}/membername)}) ; AddQueueMember(queuename[,interface[,penalty[,options[,membername ; [,stateinterface]]]]]) same => n,AddQueueMember(support,${MemberChannel},${QueuePenalty},,${MemberName})
Using AddQueueMember()
, we’ve shown how you could retrieve the penalty
associated with a given member name for a particular queue and assign
that value to the member when she logs into the queue. Some additional
abstraction would need to be done to make this work for multiple
queues; for more information see Automatically Logging Into and Out of Multiple Queues.
Using the queuerules.conf file, it is possible to
specify rules to 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 to be used 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:
[Queues] exten => 7000,1,Verbose(2,Entering the support queue) same => n,Set(QUEUE_MIN_PENALTY=2) ; set minimum queue member penalty to be used same => n,Set(QUEUE_MAX_PENALTY=4) ; set maximum queue member penalty we'll use same => n,Queue(support) ; entering the queue with minimum and maximum ; 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 rules are defined using the queuerules.conf file. Multiple rules can be created in order to facilitate different penalty changes throughout the call. Let’s take a look at how we’d define the changes described in the previous paragraph:
[more_members] penaltychange => 60,5,1
If you make changes to the queuerules.conf file and reload app_queue.so, the new rules will affect only new callers in the queue, not existing callers.
We’ve defined the rule more_members
in queuerules.conf and passed the following
values to penaltychange:
60
is the number of seconds to wait before
changing the penalty values, 5
is
the new QUEUE_MAX_PENALTY
, and
1
is the new QUEUE_MIN_PENALTY
. With our new rule
defined, we must reload app_queue.so to make it available to us for
use:
*CLI> module reload app_queue.so
-- Reloading module 'app_queue.so' (True Call Queueing)
== Parsing '/etc/asterisk/queuerules.conf': == Found
We can also verify our rules at the console with queue show rules:
*CLI> queue show rules
Rule: more_members
After 60 seconds, adjust QUEUE_MAX_PENALTY to 5 and adjust QUEUE_MIN_PENALTY to 1
With our rule now loaded into memory, we can modify our dialplan
to make use of it. Just modify the Queue()
line to include the new rule, like
so:
[Queues]
exten => 7000,1,Verbose(2,Entering the support queue)
same => n,Set(QUEUE_MIN_PENALTY=2) ; set minimum queue member penalty
same => n,Set(QUEUE_MAX_PENALTY=4) ; set maximum queue member penalty
; Queue(queuename[,options[,URL[,announceoverride[,timeout[,AGI[,macro
; [,gosub[,rule[,position]]]]]]]]])
same => n,Queue(support,,,,,,,,more_members
) ; entering queue with minimum and
; maximum member penalties
The queuerules.conf file is quite flexible. We can define our rule using relative instead of absolute penalty values, and we can define multiple rules:
[more_members] penaltychange => 30,+1 penaltychange => 45,,-1 penaltychange => 60,+1 penaltychange => 120,+2
Here, we’ve modified our more_members
rule to use relative values.
After 30 seconds, we increase the maximum penalty by 1
(which would take us to 5
using our sample dialplan). After 45 seconds, we
decrease the minimum penalty by 1
,
and so on. We can verify our new rule changes after a module reload app_queue.so at the Asterisk console:
*CLI> queue show rules
Rule: more_members
After 30 seconds, adjust QUEUE_MAX_PENALTY by 1 and adjust QUEUE_MIN_PENALTY by 0
After 45 seconds, adjust QUEUE_MAX_PENALTY by 0 and adjust QUEUE_MIN_PENALTY by -1
After 60 seconds, adjust QUEUE_MAX_PENALTY by 1 and adjust QUEUE_MIN_PENALTY by 0
After 120 seconds, adjust QUEUE_MAX_PENALTY by 2 and adjust QUEUE_MIN_PENALTY by 0
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, the average wait time, or make periodic announcements thanking your callers for waiting (or whatever your audio files say). It’s important to tune the values that control when these announcements are played to the callers, because announcing their position, thanking them for waiting, and telling them the average hold time too often may annoy them, causing them to either hang up or take it out on your agents.
There are several options in the queues.conf file 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 queues.conf File, but we’ll review the relevant ones here.
Table 13-5 lists the options you can use to control when announcements are played to the caller.
Table 13-5. Options related to prompt control timing within a queue
Table 13-6 shows what files will be used when announcements are played to the caller.
Table 13-6. Options for controlling the playback of prompts within a queue
If the number of options devoted to playing announcements to callers is any indication of their importance, it’s probably in our best interest to use them to their fullest potential. The options in Table 13-5 help us define when we’ll play announcements to callers, and the options in Table 13-6 help us control what we play to our callers. With those tables in hand, let’s take a look at an example queue where we’ve defined some values. We’ll use our basic queue template as a starting point:
[general] autofill=yes ; distribute all waiting callers to available members shared_lastcall=yes ; respect the wrapup time for members logged into more ; than one queue [StandardQueue](!) ; template to provide common features musicclass=default ; play [default] music strategy=rrmemory ; use the Round Robin Memory strategy joinempty=yes ; do not join the queue when no members available leavewhenempty=no ; leave the queue when no members available ringinuse=no ; don't ring members when already InUse (prevents ; multiple calls to an agent) [sales](StandardQueue) ; create the sales queue using the parameters in the ; StandardQueue template [support](StandardQueue) ; create the support queue using the parameters in the ; StandardQueue template
We’ll now modify the StardardQueue
template to control our
announcements:
[StandardQueue](!) ; template to provide common features musicclass=default ; play [default] music strategy=rrmemory ; use the Round Robin Memory strategy joinempty=yes ; do not join the queue when no members available leavewhenempty=no ; leave the queue when no members available ringinuse=no ; don't ring members when already InUse (prevents ; multiple calls to an agent) ; -------- Announcement Control -------- announce-frequency=30 ; announces caller's hold time and position every 30 ; seconds min-announce-frequency=30 ; minimum amount of time that must pass before the ; caller's position is announced periodic-announce-frequency=45 ; defines how often to play a periodic announcement to ; caller random-periodic-announce=no ; defines whether to play periodic announcements in ; a random order, or serially relative-periodic-announce=yes ; defines whether the timer starts at the end of ; file playback (yes) or the beginning (no) announce-holdtime=once ; defines whether the estimated hold time should be ; played along with the periodic announcement announce-position=limit ; defines if we should announce the caller's position ; in the queue announce-position-limit=10 ; defines the limit value where we announce the ; caller's position (when announce-position is set to ; limit or more) announce-round-seconds=30 ; rounds the hold time announcement to the nearest ; 30-second value
Let’s describe what we’ve just set in our
StandardQueue
template.
We’ll announce the caller’s hold time and
position every 30 seconds (announce
-
frequency
),[129] and make sure the minimum amount of time that passes
before we announce it again is at least 30 seconds (min-announce-frequency
). We do this to limit
how often our announcements are played to the callers, in order to
avoid the updates becoming annoying. Periodically, we’ll play an
announcement to the callers that thanks them for holding and assures
them that an agent will be with them shortly. (The announcement is
defined by the periodic-announcement
setting. We’re using
the default announcement, but you can define one or more announcements
yourself using periodic
-
announce
.)
These periodic announcements will be played
every 45 seconds (periodic-announce-frequency
), in the order
they were defined (random-period-announce
). To determine when
the periodic-announce-frequency
timer should start, we use relative-periodic-announce
. The yes
setting means the timer will start after
the announcement has finished playing, rather than when it starts to
play. The problem you could run into if you set this to no
is that if your periodic announcement
runs for any significant length of time (lets say 30 seconds), it will
appear as if it is being played every 15 seconds, rather than every 45
seconds as may be intended.
How many times we announce the hold time to
the caller is controlled via the announce-holdtime
option, which we’ve set to
once
. Setting the value to yes
will announce it every time, and setting
to no
will disable it.
We configure how and when we announce the
caller’s estimated remaining hold time via announce-position
, which we’ve set to
limit
. Using the value of limit
for announce-position
lets us announce the
caller’s position only if it is within the limit defined by announce-position-limit
. So, in this case
we’re only announcing the callers’ positions if they are in the first
10 positions of the queue. We could also use yes
to announce the position every time the
periodic announcement is played, set it to no
to never announce it, or use the value
more
if we want to announce the
position only when it is greater than the value set for announce-position-limit
.
Our last option, announce-round-seconds
, controls the value
to round to when we announce the caller’s hold time. In this case,
instead of saying “1 minute and 23 seconds,” the value would be
rounded to the nearest 30-second value, which would result in a prompt
of “1 minute and 30 seconds.”
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.
The Queue()
application supports two kinds of timeout: one is for the maximum
period of time a caller stays in the queue, and the other is how
long to ring a device when attempting to connect a caller to a queue
member. We’ll be talking about the maximum period of time a caller
stays in the queue before the call overflows to another location,
such as VoiceMail()
. 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.conf file. 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:
[Queues]
exten => 7000,1,Verbose(2,Joining the support queue for a maximum of 2 minutes)
same => n,Queue(support,120
)
same => n,VoiceMail(support@queues,u)
same => n,Hangup()
Of course, we could define a
different destination, but the VoiceMail()
application is as good as any. Just make sure that if you’re going
to send callers to voicemail someone checks it regularly and calls
your customers back.
Now let’s say we have the scenario where 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 queues.conf.
The available values are app
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, 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).
Asterisk provides two options that control when
callers can join and are forced to leave queues, based on the
statuses of the queue members. The first option, joinempty
, is used to control whether callers can enter a
queue. The leavewhenempty
option
is used to control when callers already in a queue
should be removed from that queue (i.e., if all of the queue members
become unavailable). Both options take a comma-separated list of
values that control this behavior. The factors are listed in Table 13-7.
Table 13-7. Options that can be set for joinempty or leavewhenempty
Value | Description |
---|---|
paused | Members are considered unavailable if they are paused. |
penalty | Members are considered unavailable if their penalties
are less than QUEUE_MAX_PENALTY . |
inuse | Members are considered unavailable if their device
status is In Use . |
ringing | Members are considered unavailable if their device
status is Ringing . |
unavailable | Applies primarily to agent channels; if the agent is not logged in but is a member of the queue it is considered unavailable. |
invalid | Members are considered unavailable if their device
status is Invalid . This
is typically an error condition. |
unknown | Members are considered unavailable if device status is unknown. |
wrapup | Members 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.[130] For the leavewhempty
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=paused,inuse,invalid
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 paused
, inuse
, or invalid
.
The leavewhenempty
example could be something
like:
leavewhenempty=inuse,ringing
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 either inuse
or ringing
.
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 13-8.
Table 13-8. Mapping between old and new values for controlling when callers join and leave queues
Value | Mapping (joinempty) | Mapping (leavewhenempty) |
---|---|---|
yes | (empty) | penalty,paused,invalid |
no | penalty,paused,invalid | (empty) |
strict | penalty,paused,invalid,unavailable | penalty,paused,invalid,unavailable |
loose | penalty,invalid | penalty,invalid |
The use of Local channels as queue members is a popular way of executing parts of the dialplan and performing checks prior to dialing the actual agent’s device. For example, it allows us to do things like start recording the call, set up channel variables, write to a log file, set a limit on the call length (e.g., if it is a paid service), or do any of the other things we might need to do once we know which location we’re going to call.
When using Local channels for queues, they are added just like any other channels. In the queues.conf file, adding a Local channel would look like this:
; queues.conf [support](StandardQueue) member => Local/SIP-0000FFFF0001@MemberConnector ; pass the technology to dial over ; and the device identifier, ; separated by a hyphen. We'll ; break it apart inside the ; MemberConnector context.
Notice how we passed the type of
technology we want to call along with the device identifier to the
MemberConnector
context. We’ve
simply used a hyphen (although we could have used nearly anything
as a separator argument) as the field marker. We’ll use the
CUT()
function inside the
MemberConnector
context and
assign the first field (SIP
) to
one channel variable and the second field (0000FFFF0001
) to another channel
variable, which will then be used to call the endpoint.
Passing information to be later
“exploded” in the context used by the Local channel is a common
and useful technique
(kind of
like the explode()
function in
PHP).
Of course, we’ll need the MemberConnector
context to actually connect
the caller to the agent:
[MemberConnector] exten => _[A-Za-z0-9].,1,Verbose(2,Connecting ${CALLERID(all)} to Agent at ${EXTEN}) ; filter out any bad characters, allowing alphanumeric characters and the hyphen same => n,Set(QueueMember=${FILTER(A-Za-z0-9-,${EXTEN}) ; assign the first field of QueueMember to Technology using the hyphen 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()
So, now we’ve passed our queue member
to the context, and we can dial the device. However, because we’re
using the Local channel as the queue member, the Queue()
won’t necessarily know the state the call is in,
especially when the Local channel is optimized out of the path (see
https://wiki.asterisk.org/wiki/display/AST/Local+Channel+Modifiers
for information about the /n
modifier, which causes the Local channel to not be optimized out of
the path). The queue will be monitoring the state of the Local
channel, and not that of the device we really want to monitor.
Luckily, we can give the Queue()
the actual device to monitor and
associate that with the Local channel, so that the Local channel’s
state is always that of the device we’ll end up calling. Our queue
member would be modified in the queues.conf file like so:
; queues.conf [support](StandardQueue) member => Local/SIP-0000FFFF0001@MemberConnector,,,SIP/0000FFFF0001
Only SIP channels are capable of sending back reliable device state information, so it is highly recommended that you use only these channels when using Local channels as queue members.
You can also use the AddQueueMember()
and RemoveQueueMember()
applications to add members to and remove members from a queue, just
like with any other channel. AddQueueMember()
also has the ability to set
the state interface, which we defined statically in the queues.conf file. An example of how you
might do this follows:
[QueueMemberLogin] exten => 500,1,Verbose(2,Logging in device ${CHANNEL(peername)} into the support queue) ; Save the device's technology to the MemberTech channel variable same => n,Set(MemberTech=${CHANNEL(channeltype)}) ; Save the device's identifier to the MemberIdent channel variable same => n,Set(MemberIdent=${CHANNEL(peername)}) ; Build up the interface name and assign it to the Interface channel variable same => n,Set(Interface=${MemberTech}/${MemberIdent}) ; Add the member to the support queue using a Local channel. We're using the same ; format as before, separating the technology and the device indentifier with ; a hyphen and passing that information to the MemberConnector context. We then ; use the IF() function to determine if the member's technology is SIP and, if so, ; to pass back the contents of the Interface channel variable as the value to the ; state interface field of the AddQueueMember() application. ; ; *** This line should not have any line breaks same => n,AddQueueMember(support,Local/${MemberTech}-${MemberIdent} @MemberConnector,,,${IF($[${MemberTech} = SIP]?${Interface})}) same => n,Playback(silence/1) ; Play back either the agent-loginok or agent-incorrect file, depending on what ; the AQMSTATUS variable is set to. same => n,Playback(${IF($[${AQMSTATUS} = ADDED]?agent-loginok:agent-incorrect)}) same => n,Hangup()
Now that we can add devices to the
queue using Local channels, let’s look at how we might control the
number of calls to either non-SIP channels or devices with more than
one line on them. We can make use of the GROUP()
and GROUP_COUNT()
functions to track call counts
to an endpoint. We’ll modify our MemberConnector
context to take this into
account:
[MemberConnector] exten => _[A-Za-z0-9].,1,Verbose(2,Connecting ${CALLERID(all)} to Agent at ${EXTEN}) ; filter out any bad characters, allowing alphanumeric characters and the hyphen same => n,Set(QueueMember=${FILTER(A-Za-z0-9-,${EXTEN}) ; assign the first field of QueueMember to Technology using the hyphen 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)}) ; Increase the value of the group inside the queue_members category by one same => n,Set(GROUP(queue_members)=${Technology}-${Device}) ; Check if the group@category is greater than 1, and, if so, return Congestion() ; (too many channels) ; ; *** This line should not have any line breaks same => n,ExecIf($[${GROUP_COUNT(${Technology}-${Device}@queue_members)} > 1] ?Congestion()) ; dial the agent same => n,Dial(${Technology}/${Device}) 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 we
actually connect to the device). While this is not an ideal situation
because the queue will keep trying the member over and over again (or
at least include it in the cycle of agents, depending on how many
members you have and their current statuses), it is better than an
agent getting multiple calls at the same time.
We’ve also used this same method to create a
type of reservation process. If you want to call an agent directly
(for example, if the caller needs to follow up with a particular
agent), you could reserve that agent by using the GROUP()
and GROUP_COUNT()
functions to essentially pause
the agent in the queue until the caller can be connected. This is
particularly useful in situations where you need to play some
announcements to the caller prior to connecting her with the agent,
but you don’t want the agent to get connected to another caller while
the announcements are being played.
The queue_log file located in /var/log/asterisk/ contains information about the queues defined in your system (when a queue is reloaded, when queue members are added or removed, etc.) and about calls into the queues (e.g., their status and what channels the callers were connected to). The queue log is enabled by default, but can be controlled via the 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:
The information contained in the event parameters depends on the type of event. A sample queue_log file might look something like the following:
1292281046|psy1-1292281041.87|7100|NONE|ENTERQUEUE||4165551212|1 1292281046|psy1-1292281041.87|7100|Local/9996@MemberConnector|RINGNOANSWER|0 1292281048|psy1-1292281041.87|7100|Local/9990@MemberConnector|CONNECT|2 |psy1-1292281046.90|0 1292284121|psy1-1292281041.87|7100|Local/9990@MemberConnector|COMPLETECALLER|2|3073|1 1292284222|MANAGER|7100|Local/9990@MemberConnector|REMOVEMEMBER| 1292284222|MANAGER|7200|Local/9990@MemberConnector|REMOVEMEMBER| 1292284491|MANAGER|7100|Local/9990@MemberConnector|ADDMEMBER| 1292284491|MANAGER|7200|Local/9990@MemberConnector|ADDMEMBER| 1292284519|psy1-1292284515.93|7100|NONE|ENTERQUEUE||4165551212|1 1292284519|psy1-1292284515.93|7100|Local/9996@MemberConnector|RINGNOANSWER|0 1292284521|psy1-1292284515.93|7100|Local/9990@MemberConnector|CONNECT|2 |psy1-1292284519.96|0 1292284552|MANAGER|7100|Local/9990@MemberConnector|REMOVEMEMBER| 1292284552|MANAGER|7200|Local/9990@MemberConnector|REMOVEMEMBER| 1292284562|psy1-1292284515.93|7100|Local/9990@MemberConnector|COMPLETECALLER|2|41|1
As you can see from this example, there might not always be a
unique ID for the event. In some cases external services, such as the
Asterisk Manager Interface (AMI), perform actions on the queue; in this
case you’ll see something like MANAGER
in the Unique ID field.
The available events and the information they provide are described in Table 13-9.
Table 13-9. Events in the Asterisk queue log
Event | Information provided |
---|---|
ABANDON | Written 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. |
ADDMEMBER | Written 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. |
AGENTDUMP | Indicates that the agent hung up on the caller while the queue announcement was being played, prior to them being bridged together. |
AGENTLOGIN | Recorded 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 ). |
AGENTLOGOFF | Logged when an agent logs off, along with a parameter indicating how long the agent was logged in for. |
COMPLETEAGENT | Recorded 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. |
COMPLETECALLER | Same as COMPLETEAGENT , except the caller
hung up and not the agent. |
CONFIGRELOAD | Indicates that the queue configuration was reloaded (e.g., via module reload app_queue.so). |
CONNECT | Written 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. |
ENTERQUEUE | Written when a caller enters the queue. Two parameters are also written: the URL (if specified) and the caller ID of the caller. |
EXITEMPTY | Written 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. |
EXITWITHKEY | Written 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. |
EXITWITHTIMEOUT | Written 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. |
PAUSE | Written when a queue member is paused. |
PAUSEALL | Written when all members of a queue are paused. |
UNPAUSE | Written when a queue member is unpaused. |
UNPAUSEALL | Written when all members of a queue are unpaused. |
PENALTY | Written when a member’s penalty is modified. The
penalty can be changed through several means, such as the
QUEUE_MEMBER_PENALTY()
function, through using Asterisk Manager Interface, or the
Asterisk CLI commands. |
REMOVEMEMBER | Written when a queue member is removed from the queue. The bridge channel field will contain the name of the member removed from the queue. |
RINGNOANSWER | Logged 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. |
TRANSFER | Written 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] |
SYSCOMPAT | Recorded 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
|
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). We also explored all the options available to us in the queues.conf, agents.conf, and queuerules.conf files, which offer us fine-grained control over any queues we configure. Of course, we need the ability to monitor what our queues are doing, so we looked finally at the queue log and the myriad of events and event parameters written when various things happen in our queues.
With the knowledge provided in this chapter, you should be well on your way to implementing a successful set of queues for your company.
[126] It is a common misconception that a queue can allow you to handle more calls. This is not strictly true, in that 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. The ideal queue is invisible to the callers, since their calls get answered immediately without them having to hold.
[127] 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).
[128] Wrapup time is used for agents who may need to perform some sort of logging or other function once a call is done. It gives them a grace period of several seconds in order to perform this task before taking another call.
[129] Callers’ positions and hold times are only announced if more than one person is holding in the queue.
[130] If the priority n+1 from where the Queue()
application was called is not
defined, the call will be hung up.