Building an IVR with no code by using TaskRouter as a state machine

January 12, 2017
Written by
Al Cook
Twilion

TaskRouter

The other day, a customer showed me their Twilio-powered IVR. Specifically, they showed the code that tracks a caller’s progress through the IVR. They built an IVR state machine that solved some of the common challenges many run into when building a complex, multi-stage IVR:

  • They wanted a generic, re-usable solution to keep track of where each caller is within the overall IVR experience each time you get a webhook, rather than hard-coding the state tracking to the current configuration of the IVR
  • They wanted for people to be able to change the configuration of the IVR  without making code changes.
  • They wanted a JSON based syntax for defining an IVR workflow so that they could tie it to a visual IVR flow builder which automatically creates the right JSON.

Their demo sparked this thought – at its heart, TaskRouter is a state machine.

I built an IVR abstraction on top of TaskRouter to function as a backbone, and solve the typical challenges of tracking state.

I found you can build an IVR with nothing but TaskRouter and TwiML Bins.

What I built uses the TaskRouter workflow syntax for defining the flow between different states, and TwiML Bins for hosting the XML associated with each state. This post shows you the architecture of the backbone hack. Grab it here on GitHub

How it works

Each call coming in to the IVR is represented as a task within TaskRouter – the task is created the first time the call hits my application server, and from then on the same Task is used for each subsequent webhook, by looking up the Task from the CallSid.

Each TaskQueue represents a state within the IVR – i.e. a spoken menu and a DTMF <Gather>  request for the caller to specify where they want to go next. As the caller navigates the IVR, the Task is updated with what state they just left, and what DTMF digits they entered – and then the TaskRouter workflow expression dictates which TaskQueue state the call moves to next – and therefore what TwiML should be returned to Twilio.

app.post('/initiateivr', function(request, response) {
    var attributesJson = {};
    checkForExistingTask(request.body['CallSid'], function(returnedTask) {
        if (!returnedTask) {
            attributesJson['CallSid'] = request.body['CallSid'];
            attributesJson['From'] = request.body['From'];
            attributesJson['To'] = request.body['To'];
            createTask(attributesJson, function(returnedTask){
                response.send(getTwimlfromTwimlBin(returnedTask));
            });
        }
        else {
            attributesJson['exited_node'] = returnedTask.task_queue_friendly_name.split(':')[0];
            attributesJson[returnedTask.task_queue_friendly_name.split(':')[0] + '_entered_digits'] = request.body['Digits'];
            updateTask(attributesJson, returnedTask, function(updatedTask){
                response.send(getTwimlfromTwimlBin(updatedTask));
            });
        }
    });
});

In order to return the right TwiML, each IVR State (TaskQueue) is correlated with a TwiML Bin which hosts the TwiML for that state. When Twilio webhooks to my application server seeking instructions for what to do with the call, my application server looks up what TaskQueue the Task is currently in, and then returns the TwiML from the TwiML Bin associated with that TaskQueue. It does this based on the name of each TaskQueue being of the form <Friendly_name>:<TwiMLBin Sid> .

When a caller enters digits from a <Gather>  in that TwiML, before looking up the TaskQueue, my code first updates the task with an attribute containing the digits entered, and the last state left – so that TaskRouter re-routes the Task to the new TaskQueue based on this new information. So for example if a <Gather>  in the state “first_state” of an IVR led to digits being entered, those digits will be available within an attribute on the Task as first_state_entered_digits. 

attributesJson['exited_state'] = returnedTask.task_queue_friendly_name.split(':')[0];

attributesJson[returnedTask.task_queue_friendly_name.split(':')[0] + '_entered_digits'] = request.body['Digits'];

updateTask(attributesJson, returnedTask, function(updatedTask){

   response.send(getTwimlfromTwimlBin(updatedTask));

});

In addition, TaskRouter will pass all of the Task’s current attributes to the TwiML Bin, so any of them can be read aloud by the TwiML by simply including the attribute name in the form {{task_<attributename>}} . E.g:

<?xml version="1.0" encoding="UTF-8"?>

<Response>

 <Say>Thank you for confirming your ZIP code. You entered {{task_first_state_entered_digits}}</Say>

</Response>

My application server will also automatically insert spaces between any sequence of numbers, or an E164 number before including it in the parameters to the TwiML Bin, in order to have Twilio pronounce it correctly. This is why all attributes are referenced with a task_ prefix from the TwiML Bin, so you can take advantage of this automatic number formatting.

An example flow

So to piece together the different parts, let’s walk through a basic IVR example where a caller dials in, hears a menu, presses 1, and then hears a different menu. In this scenario:

  • When the call first comes in, the application server receives a webhook from Twilio. It verifies this is a new call and creates a task associated with it
  • When the task is created, Twilio returns the TaskQueue it has been routed to based on the workflow. My application server then fetches the TwiML from the TwiML Bin associated with that TaskQueue
  • The application server returns that TwiML to Twilio in response to the initial webhook. This TwiML includes a <Gather>  requesting DTMF digits, which once fulfilled will webhook to my application server again.
  • When the webhook for the completed <Gather>  comes in, my application server finds the correlating Task based on CallSid. It then updates the attributes of the task with the dialed digits and the state the Task just exited.
  • Twilio responds to that task update with the new TaskQueue (state) which the task has been routed to based on the new attributes. My application server retrieves the TwiML Bin associated with that new TaskQueue, and responds to the webhook for the completed <Gather>  with that TwiML.

 

Building IVR workflows in TaskRouter

So now we have the framework for our IVR flow builder, everything else can be configured with no code, using only the workflow.

So for example, moving from the first state to the second state if the digit ‘1’ is pressed is as simple as:

"filters": [
     {
       "targets": [
         {
           "queue": "WQ77fc8f0cc8346e5ff37ac82dc944e141"
         }
       ],
       "filter_friendly_name": "Send calls from the first state to the second state if they entered 1",
       "expression": "exited_state=='first_state' AND first_state_entered_digits ==1"
     }
   ],

And of course because it’s built on TaskRouter all the pre-defined attributes can also be used, so to have a different IVR menu for inside business hours is as simple as:

"filters": [
           {
               "targets": [
                   {
                       "queue": "WQ57cab415732dec475f600c75eab44cc9"
                   }
               ],
               "filter_friendly_name": "Business Hours Menu",
               "expression": "(taskrouter.dayOfWeek IN ['Mon', 'Tue', 'Wed','Thu', 'Fri']
                               AND taskrouter.currentTime > 800
                               AND taskrouter.currentTime < 1730)
            }
       ],

After the IVR

Once the caller has reached a ‘leaf state’ in the IVR where it is ready to be assigned to an agent, it is simply a case of using the workflow to move that to a TaskQueue which has workers matched to it.

Alternatively if you wanted to move the task to a different workspace in order to keep TaskQueues and metrics separate, you could maintain an attribute within the task which is a JSON blob of all task attributes, and then <Enqueue>  the task into a new workflow and include those attributes.

A foundation for building complex IVRs?

I’m excited about the potential of this, and keen to hear your thoughts as to whether this approach would be worth us investing in productizing. You can email me at al@twilio.com. To get started with TaskRouter, read the docs. You can also check out the IVR tutorial page to see how to build IVR in many languages and frameworks.

The back end for this is all Node, making heavy use of callbacks for dealing with the asynchronous nature of this. 

Disclaimer: In no way is this production quality code. Pull Requests welcome!