
Setting up Call Queues -- A Tutorial

Pardon, but the dialplan in this tutorial will be expressed
in AEL, the new Asterisk Extension Language. If you are 
not used to its syntax, we hope you will find it to some
degree intuitive. If not, there are documents explaining
its syntax and constructs.


======  Configuring Call Queues

First of all, set up call queues in queue.conf

Here is an example:

   =========== queues.conf ===========
   | ; Cool Digium Queues            |
   | [general]                       |
   | persistentmembers = yes         |
   |                                 |
   | ; General sales queue           |
   | [sales-general]                 |
   | music=default                   |
   | context=sales                   |
   | strategy=ringall                |
   | joinempty=strict                |
   | leavewhenempty=strict           |
   |                                 |
   | ; Customer service queue        |
   | [customerservice]               |
   | music=default                   |
   | context=customerservice         |
   | strategy=ringall                |
   | joinempty=strict                |
   | leavewhenempty=strict           |
   |                                 |
   | ; Support dispatch queue        |
   | [support-dispatch]              |
   | music=default                   |
   | context=dispatch                |
   | strategy=ringall                |
   | joinempty=strict                |
   | leavewhenempty=strict           |
   ===================================

In the above, we have defined 3 separate calling queues: 
sales-general, customerservice, and support-dispatch.

Please note that the sales-general queue specifies a
context of "sales", and that customerservice specifies the
context of "customerservice", and the support-dispatch
queue specifies the context "dispatch". These three
contexts must be defined somewhere in your dialplan.
We will show them after the main menu below.

<verbage explaining options above>
In the [general] section, specifying the persistentmembers=yes,
will cause the agent lists to be stored in astdb, and 
recalled on startup.

The strategy=ringall will cause all agents to be dialed
together, the first to answer is then assigned the incoming
call. 

"joinempty" set to "strict" will keep incoming callers from
being placed in queues where there are no agents to take calls.
The Queue() application will return, and the dial plan can 
determine what to do next.

If there are calls queued, and the last agent logs out, the
remaining incoming callers will immediately be removed from
the queue, and the Queue() call will return, IF the "leavewhenempty" is
set to "strict".


=====================================
|  Routing incoming Calls to Queues |
=====================================


Then in extensions.ael, you can do these things:

================ The Main Menu 

At Digium, incoming callers are sent to the "mainmenu" context, where they
are greeted, and directed to the numbers they choose...

context mainmenu {

	includes {
		digium;
		queues-loginout;
	}

        0 => goto dispatch|s|1;
        2 => goto sales|s|1;
        3 => goto customerservice|s|1;
        4 => goto dispatch|s|1;

        s => {
                Ringing();
                Wait(1);
                Set(attempts=0);
                Answer();
                Wait(1);
                Background(digium/ThankYouForCallingDigium);
                Background(digium/YourOpenSourceTelecommunicationsSupplier);
                WaitExten(0.3);
        repeat:
                Set(attempts=$[${attempts} + 1]);
                Background(digium/IfYouKnowYourPartysExtensionYouMayDialItAtAnyTime);
                WaitExten(0.1);
                Background(digium/Otherwise);
                WaitExten(0.1);
                Background(digium/ForSalesPleasePress2);
                WaitExten(0.2);
                Background(digium/ForCustomerServicePleasePress3);
                WaitExten(0.2);
                Background(digium/ForAllOtherDepartmentsPleasePress4);
                WaitExten(0.2);
                Background(digium/ToSpeakWithAnOperatorPleasePress0AtAnyTime);
                if( ${attempts} < 2 ) {
                        WaitExten(0.3);
                        Background(digium/ToHearTheseOptionsRepeatedPleaseHold);
                }
                WaitExten(5);
                if( ${attempts} < 2 ) goto repeat;
                Background(digium/YouHaveMadeNoSelection);
                Background(digium/ThisCallWillBeEnded);
                Background(goodbye);
                Hangup();
        }
}


============= The Contexts referenced from the queues.conf file



context sales {

        0 => goto dispatch|s|1;
        8 => Voicemail(${SALESVM});

        s => {
                Ringing();
                Wait(2);
                Background(digium/ThankYouForContactingTheDigiumSalesDepartment);
                WaitExten(0.3);
                Background(digium/PleaseHoldAndYourCallWillBeAnsweredByOurNextAvailableSalesRepresentative);
                WaitExten(0.3);
                Background(digium/AtAnyTimeYouMayPress0ToSpeakWithAnOperatorOr8ToLeaveAMessage);
                Set(CALLERID(name)=Sales);
                Queue(sales-general|t);
                Set(CALLERID(name)=EmptySalQ);
                goto dispatch|s|1;
                Playback(goodbye);
                Hangup();
        }
}

Please note that there is only one attempt to queue a call in the sales queue. All sales agents that
are logged in will be rung.


context customerservice {

        0 => {
                SetCIDName(CSVTrans);
                goto dispatch|s|1;
        }
        8 => Voicemail(${CUSTSERVVM});

        s => {
                Ringing();
                Wait(2);
                Background(digium/ThankYouForCallingDigiumCustomerService);
                WaitExten(0.3);
        notracking:
                Background(digium/PleaseWaitForTheNextAvailableCustomerServiceRepresentative);
                WaitExten(0.3);
                Background(digium/AtAnyTimeYouMayPress0ToSpeakWithAnOperatorOr8ToLeaveAMessage);
                Set(CALLERID(name)=Cust Svc);
                Set(QUEUE_MAX_PENALTY=10);
                Queue(customerservice|t);
                Set(QUEUE_MAX_PENALTY=0);
                Queue(customerservice|t);
                Set(CALLERID(name)=EmptyCSVQ);
                goto dispatch|s|1;
                Background(digium/NoCustomerServiceRepresentativesAreAvailableAtThisTime);
                Background(digium/PleaseLeaveAMessageInTheCustomerServiceVoiceMailBox);
                Voicemail(${CUSTSERVVM});
                Playback(goodbye);
                Hangup();
        }
}

Note that calls coming into customerservice will first be try to queue
calls to those agents with a QUEUE_MAX_PENALTY of 10, and if none are available,
then all agents are rung.


context dispatch
{

        s => {
                Ringing();
                Wait(2);
                Background(digium/ThankYouForCallingDigium);
                WaitExten(0.3);
                Background(digium/YourCallWillBeAnsweredByOurNextAvailableOperator);
                Background(digium/PleaseHold);
                Set(QUEUE_MAX_PENALTY=10);
                Queue(dispatch|t);
                Set(QUEUE_MAX_PENALTY=20);
                Queue(dispatch|t);
                Set(QUEUE_MAX_PENALTY=0);
                Queue(dispatch|t);
                Background(digium/NoOneIsAvailableToTakeYourCall);
                Background(digium/PleaseLeaveAMessageInOurGeneralVoiceMailBox);
                Voicemail(${DISPATCHVM});
                Playback(goodbye);
                Hangup();
        }
}

And in the dispatch context, first agents of priority 10 are tried, then
20, and if none are available, all agents are tried. 

Notice that a common pattern is followed in each of the three queue contexts:

First, you set QUEUE_MAX_PENALTY to a value, then you call
Queue(<queue-name>,option,... (see the documentation for the Queue application));

In the above, note that the "t" option is specified, and this allows the 
agent picking up the incoming call the luxury of transferring the call to 
other parties.

The purpose of specifying the QUEUE_MAX_PENALTY is to develop a set of priorities
amongst agents. By the above usage, agents with lower number priorities will 
be given the calls first, and then, if no-one picks up the call, the QUEUE_MAX_PENALTY
will be incremented, and the queue tried again. Hopefully, along the line, someone
will pick up the call, and the Queue application will end with a hangup.

The final attempt to queue in most of our examples sets the QUEUE_MAX_PENALTY
to zero, which means to try all available agents.


=========================================
|      Assigning agents to Queues       |
=========================================

In this example dialplan, we want to be able to add and remove agents to 
handle incoming calls, as they feel they are available. As they log in,
they are added to the queue's agent list, and as they log out, they are
removed. If no agents are available, the queue command will terminate, and
it is the duty of the dialplan to do something appropriate, be it sending
the incoming caller to voicemail, or trying the queue again with a higher 
QUEUE_MAX_PENALTY.

Because a single agent can make themselves available to more than one queue,
the process of joining multiple queues can be handled automatically by the
dialplan.


================= Agents Log In and Out 


context queues-loginout
{
        6092 => {
                        Answer();
                        Read(AGENT_NUMBER,agent-enternum);
                        VMAuthenticate(${AGENT_NUMBER}@default,s);
                        Set(queue-announce-success=1);
                        goto queues-manip,I${AGENT_NUMBER},1;
                }

        6093 => {
                        Answer();
                        Read(AGENT_NUMBER,agent-enternum);
                        Set(queue-announce-success=1);
                        goto queues-manip,O${AGENT_NUMBER},1;
                }
}


In the above contexts, the agents dial 6092 to log into their queues,
and they dial 6093 to log out of their queues. The agent is prompted
for their agent number, and if they are logging in, their passcode, 
and then they are transferred to the proper extension in the 
queues-manip context.  The queues-manip context does all the 
actual work:


context queues-manip {

        // Raquel Squelch
        _[IO]6121 => {
                &queue-addremove(dispatch,10);
                &queue-success();
        }

        // Brittanica Spears
        _[IO]6165 => {
                &queue-addremove(dispatch,20);
                &queue-success();
        }

        // Rock Hudson
        _[IO]6170 => {
                &queue-addremove(sales-general,10);
                &queue-addremove(customerservice,20);
                &queue-addremove(dispatch,30);
                &queue-success();
        }

        // Saline Dye-on
        _[IO]6070 => {
                &queue-addremove(sales-general,20);
                &queue-addremove(customerservice,30);
                &queue-addremove(dispatch,30);
                &queue-success();
        }
}

In the above extensions, note that the queue-addremove macro is used
to actually add or remove the agent from the applicable queue,
with the applicable priority level. Note that agents with a 
priority level of 10 will be called before agents with levels
of 20 or 30.

In the above example, Raquel will be dialed first in the dispatch
queue, if she has logged in. If she is not, then the second call of
Queue() with priority of 20 will dial Brittanica if she is present,
otherwise the third call of Queue() with MAX_PENALTY of 0 will 
dial Rock and Saline simultaneously.

Also note that Rock will be among the first to be called in the sales-general 
queue, and among the last in the dispatch queue. As you can see in
main menu, the callerID is set in the main menu so they can tell 
which queue incoming calls are coming from.

The call to queue-success() gives some feedback to the agent
as they log in and out, that the process has completed.

macro queue-success()
{
        if( ${queue-announce-success} > 0 )
        {
                switch(${MACRO_EXTEN:0:1})
                {
                case I:
                        Playback(agent-loginok);
                        Hangup();
						break;
                case O:
                        Playback(agent-loggedoff);
                        Hangup();
						break;
                }
        }
}


The queue-addremove macro is defined in this manner:

macro queue-addremove(queuename,penalty)
{
        switch(${MACRO_EXTEN:0:1})
        {
        case I:  // Login
                AddQueueMember(${queuename},Local/${MACRO_EXTEN:1}@agents,${penalty});
				break;
        case O:  // Logout
                RemoveQueueMember(${queuename},Local/${MACRO_EXTEN:1}@agents);
				break;
        case P:  // Pause
                PauseQueueMember(${queuename},Local/${MACRO_EXTEN:1}@agents);
				break;
        case U:  // Unpause
                UnpauseQueueMember(${queuename},Local/${MACRO_EXTEN:1}@agents);
				break;
        default: // Invalid
                Playback(invalid);
				break;
        }
}

Basically, it uses the first character of the MACRO_EXTEN variable, to determine the
proper actions to take. In the above dial plan code, only the cases I or O are used,
which correspond to the Login and Logout actions.


=======================================================
|    Controlling The Way Queues Call the Agents       |
=======================================================

Notice in the above, that the commands to manipulate agents in queues have
"@agents" in their arguments. This is a reference to the agents context:

context agents
{
	// General sales queue
	8010 =>
	{
		Set(QUEUE_MAX_PENALTY=10);
		Queue(sales-general|t);
		Set(QUEUE_MAX_PENALTY=0);
		Queue(sales-general|t);
		Set(CALLERID(name)=EmptySalQ);
		goto dispatch|s|1;
	}
	// Customer Service queue
	8011 =>
	{
		Set(QUEUE_MAX_PENALTY=10);
		Queue(customerservice|t);
		Set(QUEUE_MAX_PENALTY=0);
		Queue(customerservice|t);
		Set(CALLERID(name)=EMptyCSVQ);
		goto dispatch|s|1;
	}
	8013 =>
	{
		Dial(iax2/sweatshop/9456@from-ecstacy);

		Set(CALLERID(name)=EmptySupQ);
		Set(QUEUE_MAX_PENALTY=10);
		Queue(support-dispatch,t);
		Set(QUEUE_MAX_PENALTY=20);
		Queue(support-dispatch,t);
		Set(QUEUE_MAX_PENALTY=0); // means no max
		Queue(support-dispatch,t);
		goto dispatch|s|1;
	}
	6121 => &callagent(${RAQUEL});
	6165 => &callagent(${SPEARS});
	6170 => &callagent(${ROCK});
	6070 => &callagent(${SALINE});
}

In the above, the variables ${RAQUEL}, etc stand for
actual devices to ring that person's
phone (like Zap/37).

The 8010, 8011, and 8013 extensions are purely for transferring
incoming callers to queues. For instance, a customer service 
agent might want to transfer the caller to talk to sales. The 
agent only has to transfer to extension 8010, in this case.

Here is the callagent macro, note that if a person in the
queue is called, but does not answer, then they are automatically
removed from the queue.

macro callagent(device)
{
	if( ${GROUP_COUNT(${MACRO_EXTEN}@agents)}=0 )
	{
		Set(OUTBOUND_GROUP=${MACRO_EXTEN}@agents);
		Dial(${device}|300|t);
		switch(${DIALSTATUS})
		{
		case BUSY:
			Busy();
			break;
		case NOANSWER:
			Set(queue-announce-success=0);
			goto queues-manip|O${MACRO_EXTEN}|1;
		default:
			Hangup();
			break;
		}
	}
	else
	{
		Busy();
	}
}

In the callagent macro above, the ${MACRO_EXTEN} will
be 6121, or 6165, etc, which is the extension of the agent.

The use of the GROUP_COUNT, and OUTBOUND_GROUP follow this line
of thinking. Incoming calls can be queued to ring all agents in the
current priority. If some of those agents are already talking, they
would get bothersome call-waiting tones. To avoid this inconvenience,
when an agent gets a call, the OUTBOUND_GROUP assigns that 
conversation to the group specified, for instance 6171@agents.
The ${GROUP_COUNT()} variable on a subsequent call should return
"1" for that group. If GROUP_COUNT returns 1, then the busy() 
is returned without actually trying to dial the agent.

================ Pre Acknowledgement Message

If you would like to have a pre acknowledge message with option to reject the message
you can use the following dialplan Macro as a base with the 'M' dial argument.

[macro-screen]
exten=>s,1,Wait(.25)
exten=>s,2,Read(ACCEPT|screen-callee-options|1)
exten=>s,3,Gotoif($[${ACCEPT} = 1] ?50)
exten=>s,4,Gotoif($[${ACCEPT} = 2] ?30)
exten=>s,5,Gotoif($[${ACCEPT} = 3] ?40)
exten=>s,6,Gotoif($[${ACCEPT} = 4] ?30:30)
exten=>s,30,Set(MACRO_RESULT=CONTINUE)
exten=>s,40,Read(TEXTEN|custom/screen-exten|)
exten=>s,41,Gotoif($[${LEN(${TEXTEN})} = 3]?42:45)
exten=>s,42,Set(MACRO_RESULT=GOTO:from-internal^${TEXTEN}^1)
exten=>s,45,Gotoif($[${TEXTEN} = 0] ?46:4)
exten=>s,46,Set(MACRO_RESULT=CONTINUE)
exten=>s,50,Playback(after-the-tone)
exten=>s,51,Playback(connected)
exten=>s,52,Playback(beep)

================ Caveats 

In the above examples, some of the possible error checking has been omitted,
to reduce clutter and make the examples clearer.

