twiliotwilio-functionstwilio-flextwilio-taskrouter

Forwarding an incoming call after task reservation timeout


I am using Twilio Flex to support a call center. I have a TaskRouter workflow set up where Task Reservation Timeout is set to 120 seconds. In its filter, I've created two routing steps. The first one finds matching workers in the main queue and has a timeout of 120 seconds. After 120 seconds, it should move to Call Forward Queue. In the call forward queue, no workers exist (target worker expression: 1==2). I'm catching all these events with a "trEventListener" function. Once a task is moved into the Call Forward queue, I call the "callForward" function which uses twiml.dial() to connect the call to an external number. I also change this task's status to "canceled" with a custom reason so I can track it in flex insights. I am using the guide in this link to form my logic: https://support.twilio.com/hc/en-us/articles/360021082934-Implementing-Voicemail-with-Twilio-Flex-TaskRouter-and-WFO.

Call forwarding is working fine but according to Flex insights, there are some calls that get handled after 120 seconds (between 120 - 300 seconds). Ideally, these should be forwarded as well. There is also no error logged for me to track down why this is happening to only a handful of calls.

Furthermore, in some cases, when I try to change the task status to cancel with my custom reason, it spits out the following error: Cannot cancel task because it is not pending or reserved. In other cases, it works fine. It's again hard to figure out why it's selectively working and not consistent in its behavior.

Here is the function code.

trEventListener.js:

exports.handler = function(context, event, callback) {
    
    const client = context.getTwilioClient();
    let task = '';
    let workspace = '';
    console.log(`__[trEventStream]__: Event recieved of type: ${event.EventType}`);
    
    // setup an empty success response
    let response = new Twilio.Response();
    response.setStatusCode(204);
    
    // switch on the event type
    switch(event.EventType) {
        case 'task-queue.entered':
            // ignore events that are not entering the 'Call Forward' TaskQueue 
            if (event.TaskQueueName !== 'Call Forward') {
                console.log(`__[trEventStream]__: Entered ${event.TaskQueueName} queue - no forwarding required!`);
                return callback(null, response);
            }
    
            console.log(`__[trEventStream]__: entered ${event.TaskQueueName} queue - forwarding call!`);
            task = event.TaskSid;
            workspace = event.WorkspaceSid;
            const ta = JSON.parse(event.TaskAttributes);
            const callSid = ta.call_sid;
    
            let url = `https://${context.DOMAIN_NAME}/forwardCall`;
    
            // redirect call to forwardCall function
            client.calls(callSid).update({
                method: 'POST',
                url: encodeURI(url),
            }).then(() => { 
                console.log(`__[trEventStream]__: [SUCCESS] ~> Task with id ${task} forwarded to external DID`);
                // change task status to canceled so it doesn't appear in flex or show up as a pending task
                client.taskrouter.workspaces(workspace)
                 .tasks(task)
                 .update({
                    assignmentStatus: 'canceled',
                    reason: 'Call forwarded'
                  })
             .then(task => {
                    console.log(`__[trEventStream]__: [SUCCESS] ~> Task canceled`);
                    return callback(null, response); 
                 }).catch(err => { 
                    console.log(`__[trEventStream]__: [ERROR] ~> Task not marked complete: `, err);
                    // doesn't warrant reponse 500 since call still forwarded :)
                    return callback(null, response);
                 });
            }).catch(err => {
                console.log(`__[trEventStream]__: [ERROR] ~> Task failed to forward to external DID: `, err);
                response.setStatusCode(500);
                return callback(err, response);
            });
    break;
    default:
        return callback(null, response);
    }
};

callForward.js:

exports.handler = function(context, event, callback) {
    console.log(`forwarding call`);
    // set-up the variables that this Function will use to forward a phone call using TwiML
    // REQUIRED - you must set this
    let phoneNumber = event.PhoneNumber || context.NUMBER;
    // OPTIONAL
    let callerId =  event.CallerId || null;
    // OPTIONAL
    let timeout = event.Timeout || null;
    // OPTIONAL
    let allowedCallers = event.allowedCallers || [];

    let allowedThrough = true;
    if (allowedCallers.length > 0) {
      if (allowedCallers.indexOf(event.From) === -1) {
        allowedThrough = false;    
      }
    }
    
    // generate the TwiML to tell Twilio how to forward this call
    let twiml = new Twilio.twiml.VoiceResponse();

    let dialParams = {};
    if (callerId) {
      dialParams.callerId = callerId;
    }
    if (timeout) {
      dialParams.timeout = timeout;
    }
    
    if (allowedThrough) {
      twiml.dial(dialParams, phoneNumber); // making call :)
    }
    else {
      twiml.say('Sorry, you are calling from a restricted number. Good bye.');
    }
    
    // return the TwiML
    callback(null, twiml);
        
};

Any kind of help and/or guidance will be appreciated.


Solution

  • Twilio developer evangelist here.

    When you redirect a call from a task, its task is cancelled with the reason "redirected" so you don't need to cancel it yourself.

    Your code was failing to update the task occasionally because of a race condition between your code and the task getting cancelled by Twilio.