# Using Twilio to build an automated contact center
An important feature to many businesses is the ability to contact a list of customers in a manner that all the resources involved are used in the best way possible. Imagine that your business has a number of debtors and your team needs to reach out all of them in order to collect the debt while retaining a customer relationship and getting the optimal use of both technical and human resources. For achieving that, there are two important parts: an automated orchestrator to initiate the contacts and the use of different channels. With Twilio platform, both are possible and we will build this together throughout this article.
## Multiple Channels
With Twilio, you can contact a person using many different channels such as Voice, SMS, WhatsApp, Facebook Messager and so on. This gives you the ability to build a contact flow using one channel or another depending on, for example, the type of customer, the customer's answer from previous contacts and the type of request.
Let's imagine that your company tries a phone call contact with a customer and he does not answer it. The next step would probably be to call the customer again later or in another day. However, using Twilio, we could build any type of contact flow that will act depending on the customer's answer. For example, we could build a contact flow that identifies when a call is not answered and sends a message to the client using WhatsApp, telling that your company tried to call, but that it would be possible to continue the contact through a WhatsApp conversation if the client prefers it. Moreover, you could save the user's preference to be contacted through WhatsApp and use this knowledge in another time.
## Orchestrator
Twilio platform offers a range of runtime tools that help developers create a variaty of contact flows. Let's start with Twilio TaskRouter that will be at the core of our automated contact center implementation along with Twilio Functions and Twilio Flex.
The Twilio TaskRouter is a attribute-driven router for tasks, that can be from any available channel, and are redirected in a programmable manner to multi-tasking workers. For more information about TaskRouter, please refer to [TaskRouter Docs](https://www.twilio.com/taskrouter).
In our automated contact center, it will be important to notice that at Twilio you can even create your own task types that will not be necessarily related to a communication channel. We will use these custom task types to divide contact center flow (queues, agents etc) from the automated attempt to contact a person. The idea is that a human resource will be used just when the contact is confirmed.
On the other hand, Twilio Functions is a serverless environment which empowers developers to quickly and easily create production-grade, event-driven Twilio applications that scale with their businesses. For more information about Functions, please refer to [Functions Docs](https://www.twilio.com/docs/runtime/functions).
In our automated contact center, Functions will be used on the contact attempt part, using the Twilio SDK to start conversations in any channel. However, if you prefer, you can implement the logic of each function (that we will build on the next steps of our tutorial) in any platform that accepts HTTP requests. For this approach you can either use the Twilio API directly or use one of the available SDKS.
For agents to engage in conversations with customers in multiple channels, Twilio offers Flex that is a programmable contact center platform that gives businesses complete control over customer engagement with the speed and flexibility of the cloud. For more information about Flex, please refer to [Flex Docs](https://www.twilio.com/docs/flex). In our application, we will be using Flex after the automated contact attempt is successful, redirecting to Flex the task where the agent would take over the conversation.
## Automated Contact Center
First of all, let's start with the contact attempt part of the TaskRouter setup using just voice channel (phone call). To achieve that, we will create the following:
1. Task to represent what to do with the contact attempt;
2. Worker to be assigned to a task;
3. TaskQueue to queue each contact attempt to be made;
4. Workflow for routing the tasks;
5. Functions to be called by worker assignment's event and effectively execute the contact attempt using Twilio SDK and the task's attributes.
To give you a broader view of what we are going to build, please check out the following sequence flow diagram:

### Task
The tasks will have attributes to inform a Function what to do when a worker is assigned to them. These attributes are something programmable, related to the logic that you want to build. For our task, let's use the following attributes:
* dialer: *true* of *false*. In this case, it will be *true*;
* queue: the name of the queue to connect the call if the attempt is successful;
* attempts: the number of times already attempted to make a contact;
* callAfterTime: the time in the format HHMM to make the next contact attempt;
* retries: the number of times to try the contact if it fails;
* numbers: array of possible numbers to be called
* currentNumberIdx: the array's position of the current number to be called.
Notice that the numbers attribute is an array and has its current element represented by the currentNumberIdx attribute. The idea here is that each round of contact attempt is not limited to just one phone number but for a set of them. This means that each round will be in fact a contact attempt of each task's numbers. If all the numbers do not answer the call, the task is than sent to retry after a certain time of the day represented by the callAfterTime attribute.
In order to send a Task to Twilio, let's use the Twilio TaskRouter Node.js SDK, using a queue called "leads" as example:
```
const task = await client.taskrouter.workspaces(<workspace sid>)
.tasks
.create({attributes: JSON.stringify({
queue: "leads",
dialer: true,
attempts: 0,
callAfterTime: 0,
retries: 3,
numbers: <array of numbers>,
currentNumberIdx: 0
}), workflowSid: <workflow sid>});
```
### Worker
A Worker represents an entity that is able to perform tasks, such as an agent working in a call center or, in our case, the execution of a portion of code. The Worker also has got to have some attributes to inform TaskQueues which Worker is part of each queue. The following are the automated worker's attributes we are going to use:
* bot: *true* or *false*. In this case, it is *true* because this worker is not related to a human resource;
* queue: which queue our automated worker will redirect the tasks to. Let's set as "leads".
Go to `TaskRouter > Workspaces > Flex Task Assignment > Workers` and add a new worker called "Automated Worker - Leads" with the above attributes. Moreover, it is important to notice that it is possible to set the worker capacity for each channel. The capacity means how many tasks can be assigned simultaneously to that worker. The tasks we are going to create are from the default type. Therefore, let's set its capacity to 1 for now. Later, we will see how to change this capacity automatically depending on the agent's availability.
For human workers, normally they are added using some Single Sign-On (SSO) strategy. For more information about this procedure, please refer to [Configuring SSO and IdP in Flex Tutorial](https://www.twilio.com/docs/flex/sso-configuration). After an agent is added to Flex through SSO, the Worker will appear at `TaskRouter > Workspaces> Flex Task Assignment > Workers` where you need to add the following attribute:
* queue: array of queue names which the worker is on.
### TaskQueue
Our automated contact center uses queue as a center point. This means that each queue will have one automated worker that will control how tasks are redirected to agents. Let's create a TaskQueue example called "To Call - Leads". Go to `TaskRouter > Workspaces> Flex Task Assignment > TaskQueues` and click to add a new queue:
1. Add a new TaskQueue called "To Call - Leads"
2. Add `bot == true AND queue == "leads"`to Queue expression. This means that the queue's matching workers would have the bot attribute set to `true` and `queue` to "leads". On the bottom of the page, you will see that the worker we created before is now listed at "Matching Workers" section.
### Workflow
A Workflow defines the rules for distributing Tasks to Workers. A Workflow's configuration matches Tasks into Task Queues, assigns each Task's priority, and configures time-based rules for escalating the Task. Let's create a workflow that will assign our tasks to our bot worker, using the example queue "leads":
1. Create a new Workflow called "Automated Contact Center";
2. Create a filter called "To Call - Leads";
3. Add the following "Matching Tasks" expression: `(dialer==true) AND (queue=="leads")`;
4. In "Matching Workers", set *Queue* to "To Call - Leads" and *Expression* to `(dialer == true) AND (queue == "leads")`.
### Functions
This part will be the one with more work to do. Using Functions or HTTP endpoints, we will receive the tasks and use the Twilio API/SDK to initiate the conversation with a person, to check if this person answered and then transfer the task for a human agent. So, in our example, let's start with the Function to initiate the call. A possible implementation would be:
```
exports.handler = async function(context, event, callback) {
//Get Twilio Client from context given by Twilio Runtime
const client = context.getTwilioClient();
//Get Task SID
const taskSid = event.TaskSid;
// Extract the Task's attributes
const attributes = event.TaskAttributes;
const { numbers, currentNumberIdx, queue } = JSON.parse(attributes);
// Extract the Worker's attributes
const workerAttributes = event.WorkerAttributes;
const { bot } = JSON.parse(workerAttributes);
//Check if the Task is assigned to an automated Worker
if(bot) {
//Make the call using the Task's attributes and machine detection feature (Twilio AMD)
const call = await client.calls.create({
url: `<path>/evaluate-call?taskSid=${taskSid}&queue=${queue}`,
statusCallback: `<path>/evaluate-complete-call?taskSid=${taskSid}`,
to: numbers[currentNumberIdx],
from: "<Twilio Number>",
machineDetection: 'Enable'
});
//Callback instructing worker to accept the task
callback(null, { 'instruction' : 'accept' });
} else {
callback(null);
}
}
```
After creating the Function, you need to get its path and add to our Workflow Assignment Callback. Go to `TaskRouter > Workspaces > Flex Task Assignment > "Automated Contact Center" Workflow`. After that, click the Assignment Callback section and add the function path to assignment url field. By doing that, every time a task is assigned to a worker in this workflow, the assignment url will receive a POST request with a number of information such as task and worker attributes and so on. With these information, our function will be able to execute its logic.
Notice that when creating the call, along with other parameters, we are passing both "url" and "statusCallback" URLs. The "url" is a callback to when the call is picked up. On the other hand, the "statusCallback" is called when the call finishes.
Let's go through the first callback, called *url*. Notice that when calling the *url*, we pass two query strings: taskSid and queue. Both will be needed inside the Function. This Function's code would be:
```
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
let reason;
let twiml;
if(event.AnsweredBy === "human") {
twiml = new Twilio.twiml.VoiceResponse();
//Transfer call to Flex. This will create another Task with some default attributes
// and the ones passed to the enqueue function.
twiml.enqueue({
workflowSid: context.WORKFLOW_SID,
}).task({}, JSON.stringify({
dialer: false,
queue: event.queue
}));
reason = "Call sent to Queue";
//Set automated worker task as completed
await client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(event.taskSid)
.update({
assignmentStatus: 'completed',
reason
});
} else {
//Terminate the call
twiml = new Twilio.twiml.VoiceResponse();
twiml.hangup();
reason = "Call failed";
}
callback(null, twiml);
};
```
At the above code, the idea is to check if the call was aswered by a human and if so, redirect the call to a human agent. Otherwise, the call will be dropped. When this endpoint is called, one attribute passed on by the callback is the AnsweredBy, added by the Twilio AMD that we activated when creating the call.
If the call is answered by a human, we can use TwiML to redirect the voice task to Flex. This can be done by using the *Enqueue* instruction. Also, it is possible to add or update the task's attributes when creating the *Enqueue* instruction which we do by changing the *dialer* attribute to `false` and *queue* to the value we receive through the query string. Moreover, we use the TaskRouter SDK to change the current task (the one used by the automated worker) from `assigned` to `completed`. This is important because after redirecting the call, we need to release the automated worker to be assigned to the next task.
Bellow, there is a possible implementation for the *statusCallback*. This callback is called when the call is finished. Here, we will use this event to define if a retry will be made or if the next task number will be called or if nothing will be done at all.
```
const moment = require("moment");
const recalculateCallAfterTime = function () {
return moment().add(2, "minutes").format("HHmm");
}
const finishDialerTask = async function (client, workspaceSid, taskSid) {
return client.taskrouter.workspaces(workspaceSid)
.tasks(taskSid)
.update({
assignmentStatus: 'completed',
});
}
const updateCurrentNumberIdx = async function(client, workspaceSid, task, newValue) {
const { sid, attributes } = task;
const parsedAttributes = JSON.parse(attributes);
const newAttributes = {...parsedAttributes, currentNumberIdx: newValue };
return client.taskrouter.workspaces(workspaceSid)
.tasks(sid)
.update({
attributes: JSON.stringify(newAttributes)
});
}
const makeCall = async function(client, task, currentNumberIdx) {
const { sid, attributes } = task;
const { queue, numbers } = JSON.parse(attributes);
const call = await client.calls.create({
url: `<path>/evaluate-call?taskSid=${sid}&queue=${queue}`,
statusCallback: `<path>/evaluate-complete-call?taskSid=${sid}`,
to: numbers[currentNumberIdx],
from: "<Twilio Number>",
machineDetection: 'Enable'
});
}
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
if(event.AnsweredBy !== "human") {
const task = await
client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(event.taskSid)
.fetch();
const attributes = JSON.parse(task.attributes);
if(attributes.retries > (attributes.attempts + 1)) {
if(attributes.numbers.length > (attributes.currentNumberIdx + 1)) {
await updateCurrentNumberIdx(client, context.WORKSPACE_SID, task, attributes.currentNumberIdx + 1);
await makeCall(client, task, attributes.currentNumberIdx + 1);
} else {
await finishDialerTask(client, context.WORKSPACE_SID, event.taskSid)
await client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks
.create({ attributes: JSON.stringify({
...attributes,
attempts: attributes.attempts + 1,
callAfterTime: parseInt(recalculateCallAfterTime()),
currentNumberIdx: 0
}), workflowSid: process.env.WORKFLOW_SID });
}
} else {
await finishDialerTask(client, context.WORKSPACE_SID, event.taskSid);
}
}
callback(null);
};
```
At the code above, if a retry is made, the current dialer task is set to `completed` and a new task, with the same attributes is created. The only difference between the old task and this new one are some values of the following attributes: attempts, callAfterTime and currentNumberIdx. The *attempts* will be incremented, the *callAfterTime* will be updated to the time of the day (using th HHMM format) that will be the minimal start time for execution of this task and *currentNumberIdx* will be 0 because the retry task will start to call the first number from the numbers array.
Next, the last block of code is related to a small fix we need to do related to the incoming/outbound nature of tasks going to Flex. When we do the *Enqueue*, the attributes from the automated worker task are all copied to this new task in a manner that Flex thinks it is an outbound call and tries to validate the *to* number as a Twilio valid number. We need to change that, transforming the attributes in a manner they represent an incoming call. In order to do that, the above code changes the *to* attribute to a valid Twilio number and the task name to the customer's number. By doing that, when the agent receives the call, it will have the number the automated worker called for the first place, representing the customer. Notice that you could change the name of the task the way you want. For example, you could add a customer's name attribute to the automated worker task and use this attribute to rename the voice task going to an agent on Flex.
```
const changeOutboundCallTaskToBeAcceptedByFlex = function(client, context, event, attributes){
return client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(event.TaskSid)
.update({
attributes: JSON.stringify({ ...attributes, to: "<Twilio Number>", name: attributes.to })
});
}
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
const attributes = event.TaskAttributes && JSON.parse(event.TaskAttributes);
//outbound call redirected to Flex
if(event.EventType === "task.created" && attributes.dialer == false) {
await changeOutboundCallTaskToBeAcceptedByFlex(client, context, event, attributes);
}
callback(null);
};
```
## Configuring your Human Contact Center
After configuring the attempt part of our contact center, we need to create the real queues and connect tasks to human agents using TaskRouter and Flex. First of all, your agents need to be in Flex already as explained at "Workers" section.
In TaskRouter, we need to create a TaskQueue called "Leads" and add `queue HAS "leads" AND bot != true` to the "Queue Expression" field. This means that your workers from Flex will need to have an attribute called "queue" and its value will be an array of queue names. After that, we create a filter on our Workflow called "Leads" adding `(dialer == false) AND (queue == "leads")` to the "Matching Tasks" field. This means that tasks that are targeting humans and the queue "leads" will be routed to the "Leads" queue.
After this configuration, your agents will start to receive calls sent to Flex from our automated contact center. In Flex, the agents will have a number of great features related to call and other channels such as mute, pause, transfer and so on. Please refer to [Flex Docs](https://www.twilio.com/docs/flex) to get the most of our platform.
## Using another channel
The previous sections showed us how to create an automated contact center using the voice channel. However, you might be thinking: you said I can use multiple channels, not just voice! Yep, you are absolutely right. We are now going to mix it a little by using voice and Whatsapp. Remember, you have all the tools you need to create any flow you want.
For our simple example, we will be building a contact flow that accepts voice or Whatsapp for each task. To do that, let's use all the code and configuration we did on the last example and just expand it with the Whatsapp channel. This is great to see how easy it is with Twilio to customize any communication feature you might want.
To make it clear how we are going to build the automated contact center using Whatsapp, please check out the sequence flow diagram:

Next, a complete sequence flow diagram gathering both Voice and Whatsapp channels:
To configure the integration between Whatsapp and Flex, please refer to this [Blog Post](https://www.twilio.com/blog/whatsapp-and-flex-in-minutes-html). Remember, this configuration is related to the "human" part of the contact center.
### Worker
First of all, our automated worker is currently receiving tasks to create voice calls that will be redirected to Flex. This worker has a capacity that can be changed depending on, for example, the human agents' availability (we will discuss this on the next sections). However, depending on the type of channel, one agent will have the ability of answering one or more people. For example, an agent could be part of, let's say, 10 Whatsapp conversations at the same time but just 1 voice conversation. Therefore, it makes sense to change our automated worker's capacity for Whatsapp, which is part of the chat channel, to 10, for now.
### Task
Now, to create a Whatsapp task, we will use the same attributes from the previous sections but with some other additional attributes:
* msg: a string with the message to be sent;
* type: the type of contact - "voice" or "whatsapp";
In order to send a Task to Twilio, let's use the Twilio's TaskRouter Node.js SDK, using a queue called "leads" as example:
```
const task = await client.taskrouter.workspaces(process.env.WORKSPACE_SID)
.tasks
.create({
taskChannel: "chat",
attributes: JSON.stringify({
queue: "leads",
dialer: true,
attempts: 0,
callAfterTime: 0,
retries: 1,
numbers: <array of numbers>,
currentNumberIdx: 0,
type: "whatsapp",
msg: <message to be sent>
}
), workflowSid: process.env.WORKFLOW_SID });
```
It is really important to notice that besides the attributes, we need to set "taskChannel" parameter to "chat".
### Functions
For Whatsapp channel to work in our automated contact center, we need to change some existing functions and create some more. Let's start with the Function that will initiate the contact.
```
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
const taskSid = event.TaskSid;
const attributes = event.TaskAttributes;
const { numbers, currentNumberIdx, queue, type, msg } = JSON.parse(attributes);
const workerAttributes = event.WorkerAttributes;
const { bot } = JSON.parse(workerAttributes);
if(bot) {
if(type == "voice"){
try {
const call = await client.calls.create({
url: `<path>/evaluate-call?taskSid=${taskSid}&queue=${queue}`,
statusCallback: `<path>/evaluate-complete-call?taskSid=${taskSid}&queue=${queue}`,
to: numbers[currentNumberIdx],
from: '<Twilio Number>',
machineDetection: 'Enable'
});
} catch (err) {
console.log(err);
}
}
if(type == "whatsapp") {
const whats = await client.messages
.create({
from: 'whatsapp:+<Twilio Number>',
body: msg,
to: `whatsapp:${numbers[currentNumberIdx]}`,
statusCallback: `<path>/evaluate-whatsapp?taskSid=${taskSid}`
});
}
callback(null, { 'instruction' : 'accept' });
} else {
callback(null);
}
}
```
Notice that the function is similar to the one presented on the previous sections, however, now we check which type of communication the automated worker will initiate. For "Whatsapp", we add a block of code using the SDK to send the task's message to the respective "to" number.
Similar to the call endpoint, when intiating a conversation through Whatsapp, you can add a statusCallback url to the request. This statusCallback endpoint could have the following implementation:
```
const moment = require("moment");
const recalculateCallAfterTime = function () {
return moment().add(2, "minutes").format("HHmm");
}
const createRetryTask = async function (client, context, attributes, msgSid) {
await client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks
.create({
taskChannel: "chat",
attributes: JSON.stringify({
...attributes,
attempts: attributes.attempts + 1,
callAfterTime: parseInt(recalculateCallAfterTime()),
currentNumberIdx: 0,
msgSid
}
), workflowSid: process.env.WORKFLOW_SID });
}
const getTaskAttributes = async function (client, context, taskSid) {
const task = await
client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(taskSid)
.fetch();
return JSON.parse(task.attributes);
}
exports.handler = async function(context, event, callback) {
const { SmsStatus, MessageSid, taskSid } = event;
const client = context.getTwilioClient();
if(SmsStatus === "failed" || SmsStatus === "sent") {
await client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(taskSid)
.update({
assignmentStatus: 'completed',
reason: SmsStatus
});
const attributes = await getTaskAttributes(client, context, taskSid);
if(attributes.retries >= (attributes.attempts + 1)) {
await createRetryTask(client, context, attributes, MessageSid);
}
}
callback(null);
};
```
Check that if the status is "failed" or "sent", the automated worker task is set to "completed" and a retry task is created. Different from the voice task, it is not possible to connect directly a person with an agent because it depends on the moment that the person will answer the message. Therefore, even when the message is sent with success, a retry task is created. The idea here is that if an answer is given then we remove all the current retry tasks. Otherwise, when the time threshold passes, a new message will be sent. To create this logic of retry tasks removal, the following code could be used:
```
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
const tasks = await client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks
.list({
evaluateTaskAttributes: `numbers has "${event.from.replace("whatsapp:", "")}"`,
limit: 10
});
let queue;
if(tasks) {
await Promise.all(tasks.map( task => {
const { attributes, assignmentStatus } = task;
if(assignmentStatus === "pending") {
({ queue } = JSON.parse(attributes));
return client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(task.sid)
.remove();
}
}));
}
callback(null, { queue });
};
```
Notice that we use an "event.from" parameter which is a query string passed to this endpoint. This endpoint will be called from the "Messaging Flow" at Studio that you configured at the beginning of this part. The following image shows where you could add an "EvaluateRetry" Widget that calls the above endpoint:

Using this flow, when an incoming message is received, first we check if there are any retry tasks related to that "from" number and if so, they are removed. After this checking, the message is redirected to Flex as usual.
## Advanced Features
At this point, your automated contact center should be up and running. All tasks created initiate a contact attempt and if the contact is successful it is redirected to a human worker at Flex. However, imagine the situation where you send 10 phone calls contact attempts and all of them are successful. This means that 10 tasks will be redirected to human workers. Now suposed that we just have 4 agents available at this moment, it will result in 6 people waiting in a queue while listening to some boring music. This scenario has a huge negative impact on our customer relationship. The good news is that we can solve this problem with a litle more of code and some tweaks on our automated worker's capacity using the Twilio SDK.
The following example will not be considering the Whatsapp channel, just voice, however, it is pretty straightforward how to change the example's codes to contemplate any type of channels combination.
First of all, let's create an endpoint with all the logic to check the available workers and then set the automated worker capacity to this value:
```
const queueMap = {
leads: {
sid: "<leads queue SID>",
dialer: "<Automated Worker SID for queue leads>"
},
collections: {
sid: "<collections queue SID>",
dialer: "<Automated Worker SID for queue collections>"
}
};
const getAvailableWorkersFromQueue = async function (client, context, sid) {
const {
totalAvailableWorkers,
tasksByStatus: {
assigned,
wrapping,
reserved
}
} = await client.taskrouter.workspaces(context.WORKSPACE_SID)
.taskQueues(sid)
.realTimeStatistics()
.fetch();
return totalAvailableWorkers - (assigned + wrapping + reserved);
}
exports.changeDialerCapacity = async function (client, context, queues, params ) {
try {
if(!queues) {
queues = Object.keys(queueMap).map(key => key);
}
return await Promise.all(queues.map(async (currentQueue) => {
const { sid, dialer } = queueMap[currentQueue];
let capacity = (params && params.forceCapacity);
if(capacity == null) {
capacity = await getAvailableWorkersFromQueue(client, context, sid);
}
return await client.taskrouter.workspaces(context.WORKSPACE_SID)
.workers(dialer)
.workerChannels("default")
.update({ capacity: (capacity > 0) ? capacity : 0 });
}));
} catch(ex) {
console.log(ex);
}
};
```
In the begining there is an object that maps each queue's worker and sid with the queue's name. These information will be used to change the correct worker capacity depending on the queue we want to update. Also, an agent could be logged in more than one queue which is why the code changes the capacity for each queue the agent is on.
Moreover, notice that it is possible to set a "forceCapacity" value in the place of checking the available agents. This is necessary to mitigate a problem with concurrent tasks.
When you set the automated capacity to, let's say 10, it is possible that many calls will be made at the same time which will be not totally sincronized with the available agents because of some *ms* of difference between status changes etc. This will create a number of orphan calls. One way of mitigating this problem is setting the automated worker capacity to 0 (through the "forceCapacity" param) everytime you initiate a call. This way, if the capacity is 10, a number of parallel calls will be made and the rest will wait for some event that changes the automated worker capacity to happen. Normally an automated contact center will have a lot of events happening so this should not be a problem.
To call the "changeDialerCapacity" method on other Functions, you can use the following in the Function:
```
let path = Runtime.getFunctions()["<endpoint's relative path>"].path;
let assets = require(path);
...
await assets.changeDialerCapacity(client, context, <array of queue names>);
...
```
Now, we need to call this method in some endpoints and events. Let's start with the endpoint that initiates a call:
```
let path = Runtime.getFunctions()['dialer-assets'].path;
let assets = require(path);
const concurrencyBarrier = function(client, context, queue) {
return assets.changeDialerCapacity(client, context, [queue], { forceCapacity: 0 });
}
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
const taskSid = event.TaskSid;
const attributes = event.TaskAttributes;
const { numbers, currentNumberIdx, queue, channel, msg } = JSON.parse(attributes);
const workerAttributes = event.WorkerAttributes;
const { bot } = JSON.parse(workerAttributes);
if(bot) {
if(channel == "voice"){
try {
const call = await client.calls.create({
url: `<path>/evaluate-call?taskSid=${taskSid}&queue=${queue}`,
statusCallback: `<path>/evaluate-complete-call?taskSid=${taskSid}&queue=${queue}`,
to: numbers[currentNumberIdx],
from: '<Twilio Number>',
machineDetection: 'Enable'
});
await concurrencyBarrier(client, context, queue);
} catch (err) {
console.log(err);
}
}
if(channel == "whatsapp") {
const whats = await client.messages
.create({
from: 'whatsapp:+<Twilio Whatsapp Number>',
body: msg,
to: `whatsapp:${numbers[currentNumberIdx]}`,
statusCallback: `<path>/evaluate-whatsapp?taskSid=${taskSid}`
});
}
callback(null, { 'instruction' : 'accept' });
} else {
callback(null);
}
}
```
Next is the endpoint to evaluate a call:
```
let path = Runtime.getFunctions()['dialer-assets'].path;
let assets = require(path);
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
let reason;
let twiml;
if(event.AnsweredBy === "human") {
twiml = new Twilio.twiml.VoiceResponse();
twiml.enqueue({
workflowSid: context.WORKFLOW_SID,
}).task({}, JSON.stringify({
dialer: false,
queue: event.queue
}));
reason = "Call sent to Queue";
await client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(event.taskSid)
.update({
assignmentStatus: 'completed',
reason
});
} else {
await assets.changeDialerCapacity(client, context, [event.queue]);
twiml = new Twilio.twiml.VoiceResponse();
twiml.hangup();
reason = "Call failed";
}
callback(null, twiml);
};
```
Next is the endpoint to evaluate a complete call:
```
const moment = require("moment");
let path = Runtime.getFunctions()['dialer-assets'].path;
let assets = require(path);
const recalculateCallAfterTime = function () {
return moment().add(2, "minutes").format("HHmm");
}
const finishDialerTask = async function (client, workspaceSid, taskSid) {
return client.taskrouter.workspaces(workspaceSid)
.tasks(taskSid)
.update({
assignmentStatus: 'completed',
});
}
const updateCurrentNumberIdx = async function(client, workspaceSid, task, newValue) {
const { sid, attributes } = task;
const parsedAttributes = JSON.parse(attributes);
const newAttributes = {...parsedAttributes, currentNumberIdx: newValue };
return client.taskrouter.workspaces(workspaceSid)
.tasks(sid)
.update({
attributes: JSON.stringify(newAttributes)
});
}
const makeCall = async function(client, task, currentNumberIdx) {
const { sid, attributes } = task;
const { queue, numbers } = JSON.parse(attributes);
const call = await client.calls.create({
url: `<path>/evaluate-call?taskSid=${sid}&queue=${queue}`,
statusCallback: `<path>/evaluate-complete-call?taskSid=${sid}`,
to: numbers[currentNumberIdx],
from: '<Twilio Number>',
machineDetection: 'Enable'
});
}
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
console.log(event);
if(event.AnsweredBy !== "human") {
await assets.changeDialerCapacity(client, context, [event.queue]);
const task = await
client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(event.taskSid)
.fetch();
const attributes = JSON.parse(task.attributes);
if(attributes.retries > (attributes.attempts + 1)) {
if(attributes.numbers.length > (attributes.currentNumberIdx + 1)) {
await updateCurrentNumberIdx(client, context.WORKSPACE_SID, task, attributes.currentNumberIdx + 1);
await makeCall(client, task, attributes.currentNumberIdx + 1);
} else {
await finishDialerTask(client, context.WORKSPACE_SID, event.taskSid)
await client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks
.create({ attributes: JSON.stringify({
...attributes,
attempts: attributes.attempts + 1,
callAfterTime: parseInt(recalculateCallAfterTime()),
currentNumberIdx: 0
}), workflowSid: process.env.WORKFLOW_SID });
}
} else {
await finishDialerTask(client, context.WORKSPACE_SID, event.taskSid);
}
}
callback(null);
};
```
Lastly, the event handler endpoint:
```
let path = Runtime.getFunctions()['dialer-assets'].path;
let assets = require(path);
const changeOutboundCallTaskToBeAcceptedByFlex = function(client, context, event, attributes){
return client.taskrouter.workspaces(context.WORKSPACE_SID)
.tasks(event.TaskSid)
.update({
attributes: JSON.stringify({ ...attributes, to: "<Twilio Number>", name: attributes.to })
});
}
exports.handler = async function(context, event, callback) {
const client = context.getTwilioClient();
const attributes = event.TaskAttributes && JSON.parse(event.TaskAttributes);
const workerAttributes = event.WorkerAttributes && JSON.parse(event.WorkerAttributes)
const queues = attributes ? [attributes.queue] : workerAttributes.queue;
//outbound call redirected to Flex
if(event.EventType === "task.created" && attributes.dialer == false) {
await changeOutboundCallTaskToBeAcceptedByFlex(client, context, event, attributes);
}
//events that could change dialer behaviour
if(
event.EventType === "worker.activity.update" ||
((event.EventType === "reservation.accepted" || event.EventType === "task.completed") && attributes.dialer == false) ||
event.EventType === "reservation.canceled"
) {
await assets.changeDialerCapacity(client, context, queues);
}
callback(null);
};
```
### Limitations
It is easy to notice that using the "changeDialerCapacity" in all of these endpoints and events will create a lot of requests to the Twilio API which in turn will probably answer with "429" error as soon as your operation starts to scale. This means that the API request limit were reached. There are some ways to mitigate this, however, unfortunately it is possible that some orphan calls will be made in the process. But remember, in a considerable operation, some orphan calls will not be significant.
One possible solution is to create some sort of cache. So, before requesting the available agents, you can check a cache. If the time for a new request is reached, you can request the API and add this new value to the cache.
Other possibility is to use some sort of "deboucing" function that will just make a request after a time interval every cycle of requests.
## Conclusion
In this tutorial, we learned how to use a group of Twilio tools to create an automated contact center with omnichannel features. Moreover, the idea was to show how programmable and simple the Twilio platform is and how you can customize all communication flows depending on your need. If you have any question or need further assistance, please feel free to contact us.