Queue Members

Queues aren’t very useful without someone to answer the calls that come into them, so we need a method for allowing agents to be logged into the queues to answer calls. There are various ways of going about this, 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.

Controlling Queue Members via the CLI

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

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

The <channel> is the channel we want to add to the queue, such as SIP/0000FFFF0003, and the <queue> name will be something like support or sales—any queue name that exists in /etc/asterisk/queues.conf. For now we’ll ignore the <penalty> option, but we’ll discuss it in 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.

Controlling Queue Members with Dialplan Logic

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

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

  • AddQueueMember()

  • RemoveQueueMember()

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

  • PauseQueueMember()

  • UnpauseQueueMember()

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 UnpauseQueueMember() applications set might be used to Playback() announcements to the queue members after they’ve performed certain functions to let them know whether they have successfully logged in/out or paused/unpaused):

[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

Automatically Logging Into and Out of Multiple Queues

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

In order 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 subSetupAvailableQueues subroutine to set these channel variables and to verify that the AstDB contains a list of one or more queues for the device the queue member is calling from:

[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&not-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&not-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.)

An Introduction to Device State

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.