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 the section called “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 the section called “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 the section called “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, Device States.
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.