Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Dynamic Call Center with Node.js and Express


In this tutorial we will show how to automate the routing of calls from customers to your support agents. In this example customers would select a product, then be connected to a specialist for that product. If no one is available our customer's number will be saved so that our agent can call them back.


This is what the application does at a high level

this-is-what-the-application-does-at-a-high-level page anchor
  • Configure a workspace using the Twilio TaskRouter REST API .
  • Listen for incoming calls and let the user select a product with the dial pad.
  • Create a Task with the selected product and let TaskRouter handle it.
  • Store missed calls so agents can return the call to customers.
  • Redirect users to a voice mail when no one answers the call.
  • Allow agents to change their status (Available/Offline) via SMS.

In order to instruct TaskRouter to handle the Tasks, we need to configure a Workspace. We can do this in the TaskRouter Console(link takes you to an external page) or programmatically using the TaskRouter REST API.

In this Node.js application we'll do this setup when we start up the app.

A Workspace is the container element for any TaskRouter application. The elements are:

  • Tasks - Represents a customer trying to contact an agent
  • Workers - The agents responsible for handling Tasks
  • Task Queues - Holds Tasks to be consumed by a set of Workers
  • Workflows - Responsible for placing Tasks into Task Queues
  • Activities - Possible states of a Worker. Eg: idle, offline, busy

In order to build a client for this API, we need a TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN which you can find on Twilio Console. The function initClient configures and returns a TaskRouterClient, which is provided by the Twilio Node.js library.

Create, Setup and Configure the Workspace

create-setup-and-configure-the-workspace page anchor

lib/workspace.js


_284
'use strict';
_284
_284
var twilio = require('twilio');
_284
var find = require('lodash/find');
_284
var map = require('lodash/map');
_284
var difference = require('lodash/difference');
_284
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
_284
var HOST = process.env.HOST;
_284
var EVENT_CALLBACK = `${HOST}/events`;
_284
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
_284
var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
_284
_284
module.exports = function() {
_284
function initClient(existingWorkspaceSid) {
_284
if (!existingWorkspaceSid) {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN).taskrouter.v1.workspaces;
_284
} else {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN)
_284
.taskrouter.v1.workspaces(existingWorkspaceSid);
_284
}
_284
}
_284
_284
function createWorker(opts) {
_284
var ctx = this;
_284
_284
return this.client.activities.list({friendlyName: 'Idle'})
_284
.then(function(idleActivity) {
_284
return ctx.client.workers.create({
_284
friendlyName: opts.name,
_284
attributes: JSON.stringify({
_284
'products': opts.products,
_284
'contact_uri': opts.phoneNumber,
_284
}),
_284
activitySid: idleActivity.sid,
_284
});
_284
});
_284
}
_284
_284
function createWorkflow() {
_284
var ctx = this;
_284
var config = this.createWorkflowConfig();
_284
_284
return ctx.client.workflows
_284
.create({
_284
friendlyName: 'Sales',
_284
assignmentCallbackUrl: HOST + '/call/assignment',
_284
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
_284
taskReservationTimeout: 15,
_284
configuration: config,
_284
})
_284
.then(function(workflow) {
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var idleActivity = find(activities, {friendlyName: 'Idle'});
_284
var offlineActivity = find(activities, {friendlyName: 'Offline'});
_284
_284
return {
_284
workflowSid: workflow.sid,
_284
activities: {
_284
idle: idleActivity.sid,
_284
offline: offlineActivity.sid,
_284
},
_284
workspaceSid: ctx.client._solution.sid,
_284
};
_284
});
_284
});
_284
}
_284
_284
function createTaskQueues() {
_284
var ctx = this;
_284
return this.client.activities.list()
_284
.then(function(activities) {
_284
var busyActivity = find(activities, {friendlyName: 'Busy'});
_284
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
_284
_284
return Promise.all([
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'SMS',
_284
targetWorkers: 'products HAS "ProgrammableSMS"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Voice',
_284
targetWorkers: 'products HAS "ProgrammableVoice"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Default',
_284
targetWorkers: '1==1',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
])
_284
.then(function(queues) {
_284
ctx.queues = queues;
_284
});
_284
});
_284
}
_284
_284
function createWorkers() {
_284
var ctx = this;
_284
_284
return Promise.all([
_284
ctx.createWorker({
_284
name: 'Bob',
_284
phoneNumber: process.env.BOB_NUMBER,
_284
products: ['ProgrammableSMS'],
_284
}),
_284
ctx.createWorker({
_284
name: 'Alice',
_284
phoneNumber: process.env.ALICE_NUMBER,
_284
products: ['ProgrammableVoice'],
_284
})
_284
])
_284
.then(function(workers) {
_284
var bobWorker = workers[0];
_284
var aliceWorker = workers[1];
_284
var workerInfo = {};
_284
_284
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
_284
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
_284
_284
return workerInfo;
_284
});
_284
}
_284
_284
function createWorkflowActivities() {
_284
var ctx = this;
_284
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
_284
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var existingActivities = map(activities, 'friendlyName');
_284
_284
var missingActivities = difference(activityNames, existingActivities);
_284
_284
var newActivities = map(missingActivities, function(friendlyName) {
_284
return ctx.client.activities
_284
.create({
_284
friendlyName: friendlyName,
_284
available: 'true'
_284
});
_284
});
_284
_284
return Promise.all(newActivities);
_284
})
_284
.then(function() {
_284
return ctx.client.activities.list();
_284
});
_284
}
_284
_284
function createWorkflowConfig() {
_284
var queues = this.queues;
_284
_284
if (!queues) {
_284
throw new Error('Queues must be initialized.');
_284
}
_284
_284
var defaultTarget = {
_284
queue: find(queues, {friendlyName: 'Default'}).sid,
_284
timeout: 30,
_284
priority: 1,
_284
};
_284
_284
var smsTarget = {
_284
queue: find(queues, {friendlyName: 'SMS'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var voiceTarget = {
_284
queue: find(queues, {friendlyName: 'Voice'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var rules = [
_284
{
_284
expression: 'selected_product=="ProgrammableSMS"',
_284
targets: [smsTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
{
_284
expression: 'selected_product=="ProgrammableVoice"',
_284
targets: [voiceTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
];
_284
_284
var config = {
_284
task_routing: {
_284
filters: rules,
_284
default_filter: defaultTarget,
_284
},
_284
};
_284
_284
return JSON.stringify(config);
_284
}
_284
_284
function setup() {
_284
var ctx = this;
_284
_284
ctx.initClient();
_284
_284
return this.initWorkspace()
_284
.then(createWorkflowActivities.bind(ctx))
_284
.then(createTaskQueues.bind(ctx))
_284
.then(createWorkflow.bind(ctx))
_284
.then(function(workspaceInfo) {
_284
return ctx.createWorkers()
_284
.then(function(workerInfo) {
_284
return [workerInfo, workspaceInfo];
_284
});
_284
});
_284
}
_284
_284
function findByFriendlyName(friendlyName) {
_284
var client = this.client;
_284
_284
return client.list()
_284
.then(function (data) {
_284
return find(data, {friendlyName: friendlyName});
_284
});
_284
}
_284
_284
function deleteByFriendlyName(friendlyName) {
_284
var ctx = this;
_284
_284
return this.findByFriendlyName(friendlyName)
_284
.then(function(workspace) {
_284
if (workspace.remove) {
_284
return workspace.remove();
_284
}
_284
});
_284
}
_284
_284
function createWorkspace() {
_284
return this.client.create({
_284
friendlyName: WORKSPACE_NAME,
_284
EVENT_CALLBACKUrl: EVENT_CALLBACK,
_284
});
_284
}
_284
_284
function initWorkspace() {
_284
var ctx = this;
_284
var client = this.client;
_284
_284
return ctx.findByFriendlyName(WORKSPACE_NAME)
_284
.then(function(workspace) {
_284
var newWorkspace;
_284
_284
if (workspace) {
_284
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
_284
.then(createWorkspace.bind(ctx));
_284
} else {
_284
newWorkspace = ctx.createWorkspace();
_284
}
_284
_284
return newWorkspace;
_284
})
_284
.then(function(workspace) {
_284
ctx.initClient(workspace.sid);
_284
_284
return workspace;
_284
});
_284
}
_284
_284
return {
_284
createTaskQueues: createTaskQueues,
_284
createWorker: createWorker,
_284
createWorkers: createWorkers,
_284
createWorkflow: createWorkflow,
_284
createWorkflowActivities: createWorkflowActivities,
_284
createWorkflowConfig: createWorkflowConfig,
_284
createWorkspace: createWorkspace,
_284
deleteByFriendlyName: deleteByFriendlyName,
_284
findByFriendlyName: findByFriendlyName,
_284
initClient: initClient,
_284
initWorkspace: initWorkspace,
_284
setup: setup,
_284
};
_284
};

Now let's look in more detail at all the steps, starting with the creation of the workspace itself.


Before creating a workspace, we need to delete any others with the same friendlyName as the one we are trying to create. In order to create a workspace we need to provide a friendlyName and a eventCallbackUrl where a request will be made every time an event is triggered in our workspace.

lib/workspace.js


_284
'use strict';
_284
_284
var twilio = require('twilio');
_284
var find = require('lodash/find');
_284
var map = require('lodash/map');
_284
var difference = require('lodash/difference');
_284
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
_284
var HOST = process.env.HOST;
_284
var EVENT_CALLBACK = `${HOST}/events`;
_284
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
_284
var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
_284
_284
module.exports = function() {
_284
function initClient(existingWorkspaceSid) {
_284
if (!existingWorkspaceSid) {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN).taskrouter.v1.workspaces;
_284
} else {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN)
_284
.taskrouter.v1.workspaces(existingWorkspaceSid);
_284
}
_284
}
_284
_284
function createWorker(opts) {
_284
var ctx = this;
_284
_284
return this.client.activities.list({friendlyName: 'Idle'})
_284
.then(function(idleActivity) {
_284
return ctx.client.workers.create({
_284
friendlyName: opts.name,
_284
attributes: JSON.stringify({
_284
'products': opts.products,
_284
'contact_uri': opts.phoneNumber,
_284
}),
_284
activitySid: idleActivity.sid,
_284
});
_284
});
_284
}
_284
_284
function createWorkflow() {
_284
var ctx = this;
_284
var config = this.createWorkflowConfig();
_284
_284
return ctx.client.workflows
_284
.create({
_284
friendlyName: 'Sales',
_284
assignmentCallbackUrl: HOST + '/call/assignment',
_284
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
_284
taskReservationTimeout: 15,
_284
configuration: config,
_284
})
_284
.then(function(workflow) {
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var idleActivity = find(activities, {friendlyName: 'Idle'});
_284
var offlineActivity = find(activities, {friendlyName: 'Offline'});
_284
_284
return {
_284
workflowSid: workflow.sid,
_284
activities: {
_284
idle: idleActivity.sid,
_284
offline: offlineActivity.sid,
_284
},
_284
workspaceSid: ctx.client._solution.sid,
_284
};
_284
});
_284
});
_284
}
_284
_284
function createTaskQueues() {
_284
var ctx = this;
_284
return this.client.activities.list()
_284
.then(function(activities) {
_284
var busyActivity = find(activities, {friendlyName: 'Busy'});
_284
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
_284
_284
return Promise.all([
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'SMS',
_284
targetWorkers: 'products HAS "ProgrammableSMS"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Voice',
_284
targetWorkers: 'products HAS "ProgrammableVoice"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Default',
_284
targetWorkers: '1==1',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
])
_284
.then(function(queues) {
_284
ctx.queues = queues;
_284
});
_284
});
_284
}
_284
_284
function createWorkers() {
_284
var ctx = this;
_284
_284
return Promise.all([
_284
ctx.createWorker({
_284
name: 'Bob',
_284
phoneNumber: process.env.BOB_NUMBER,
_284
products: ['ProgrammableSMS'],
_284
}),
_284
ctx.createWorker({
_284
name: 'Alice',
_284
phoneNumber: process.env.ALICE_NUMBER,
_284
products: ['ProgrammableVoice'],
_284
})
_284
])
_284
.then(function(workers) {
_284
var bobWorker = workers[0];
_284
var aliceWorker = workers[1];
_284
var workerInfo = {};
_284
_284
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
_284
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
_284
_284
return workerInfo;
_284
});
_284
}
_284
_284
function createWorkflowActivities() {
_284
var ctx = this;
_284
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
_284
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var existingActivities = map(activities, 'friendlyName');
_284
_284
var missingActivities = difference(activityNames, existingActivities);
_284
_284
var newActivities = map(missingActivities, function(friendlyName) {
_284
return ctx.client.activities
_284
.create({
_284
friendlyName: friendlyName,
_284
available: 'true'
_284
});
_284
});
_284
_284
return Promise.all(newActivities);
_284
})
_284
.then(function() {
_284
return ctx.client.activities.list();
_284
});
_284
}
_284
_284
function createWorkflowConfig() {
_284
var queues = this.queues;
_284
_284
if (!queues) {
_284
throw new Error('Queues must be initialized.');
_284
}
_284
_284
var defaultTarget = {
_284
queue: find(queues, {friendlyName: 'Default'}).sid,
_284
timeout: 30,
_284
priority: 1,
_284
};
_284
_284
var smsTarget = {
_284
queue: find(queues, {friendlyName: 'SMS'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var voiceTarget = {
_284
queue: find(queues, {friendlyName: 'Voice'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var rules = [
_284
{
_284
expression: 'selected_product=="ProgrammableSMS"',
_284
targets: [smsTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
{
_284
expression: 'selected_product=="ProgrammableVoice"',
_284
targets: [voiceTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
];
_284
_284
var config = {
_284
task_routing: {
_284
filters: rules,
_284
default_filter: defaultTarget,
_284
},
_284
};
_284
_284
return JSON.stringify(config);
_284
}
_284
_284
function setup() {
_284
var ctx = this;
_284
_284
ctx.initClient();
_284
_284
return this.initWorkspace()
_284
.then(createWorkflowActivities.bind(ctx))
_284
.then(createTaskQueues.bind(ctx))
_284
.then(createWorkflow.bind(ctx))
_284
.then(function(workspaceInfo) {
_284
return ctx.createWorkers()
_284
.then(function(workerInfo) {
_284
return [workerInfo, workspaceInfo];
_284
});
_284
});
_284
}
_284
_284
function findByFriendlyName(friendlyName) {
_284
var client = this.client;
_284
_284
return client.list()
_284
.then(function (data) {
_284
return find(data, {friendlyName: friendlyName});
_284
});
_284
}
_284
_284
function deleteByFriendlyName(friendlyName) {
_284
var ctx = this;
_284
_284
return this.findByFriendlyName(friendlyName)
_284
.then(function(workspace) {
_284
if (workspace.remove) {
_284
return workspace.remove();
_284
}
_284
});
_284
}
_284
_284
function createWorkspace() {
_284
return this.client.create({
_284
friendlyName: WORKSPACE_NAME,
_284
EVENT_CALLBACKUrl: EVENT_CALLBACK,
_284
});
_284
}
_284
_284
function initWorkspace() {
_284
var ctx = this;
_284
var client = this.client;
_284
_284
return ctx.findByFriendlyName(WORKSPACE_NAME)
_284
.then(function(workspace) {
_284
var newWorkspace;
_284
_284
if (workspace) {
_284
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
_284
.then(createWorkspace.bind(ctx));
_284
} else {
_284
newWorkspace = ctx.createWorkspace();
_284
}
_284
_284
return newWorkspace;
_284
})
_284
.then(function(workspace) {
_284
ctx.initClient(workspace.sid);
_284
_284
return workspace;
_284
});
_284
}
_284
_284
return {
_284
createTaskQueues: createTaskQueues,
_284
createWorker: createWorker,
_284
createWorkers: createWorkers,
_284
createWorkflow: createWorkflow,
_284
createWorkflowActivities: createWorkflowActivities,
_284
createWorkflowConfig: createWorkflowConfig,
_284
createWorkspace: createWorkspace,
_284
deleteByFriendlyName: deleteByFriendlyName,
_284
findByFriendlyName: findByFriendlyName,
_284
initClient: initClient,
_284
initWorkspace: initWorkspace,
_284
setup: setup,
_284
};
_284
};

We have a brand new workspace, now we need workers. Let's create them on the next step.


We'll create two workers, Bob and Alice. They each have two attributes: contact_uri a phone number and products, a list of products each worker is specialized in. We also need to specify an activitySid and a name for each worker. The selected activity will define the status of the worker.

A set of default activities is created with your workspace. We use the Idle activity to make a worker available for incoming calls.

lib/workspace.js


_284
'use strict';
_284
_284
var twilio = require('twilio');
_284
var find = require('lodash/find');
_284
var map = require('lodash/map');
_284
var difference = require('lodash/difference');
_284
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
_284
var HOST = process.env.HOST;
_284
var EVENT_CALLBACK = `${HOST}/events`;
_284
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
_284
var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
_284
_284
module.exports = function() {
_284
function initClient(existingWorkspaceSid) {
_284
if (!existingWorkspaceSid) {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN).taskrouter.v1.workspaces;
_284
} else {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN)
_284
.taskrouter.v1.workspaces(existingWorkspaceSid);
_284
}
_284
}
_284
_284
function createWorker(opts) {
_284
var ctx = this;
_284
_284
return this.client.activities.list({friendlyName: 'Idle'})
_284
.then(function(idleActivity) {
_284
return ctx.client.workers.create({
_284
friendlyName: opts.name,
_284
attributes: JSON.stringify({
_284
'products': opts.products,
_284
'contact_uri': opts.phoneNumber,
_284
}),
_284
activitySid: idleActivity.sid,
_284
});
_284
});
_284
}
_284
_284
function createWorkflow() {
_284
var ctx = this;
_284
var config = this.createWorkflowConfig();
_284
_284
return ctx.client.workflows
_284
.create({
_284
friendlyName: 'Sales',
_284
assignmentCallbackUrl: HOST + '/call/assignment',
_284
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
_284
taskReservationTimeout: 15,
_284
configuration: config,
_284
})
_284
.then(function(workflow) {
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var idleActivity = find(activities, {friendlyName: 'Idle'});
_284
var offlineActivity = find(activities, {friendlyName: 'Offline'});
_284
_284
return {
_284
workflowSid: workflow.sid,
_284
activities: {
_284
idle: idleActivity.sid,
_284
offline: offlineActivity.sid,
_284
},
_284
workspaceSid: ctx.client._solution.sid,
_284
};
_284
});
_284
});
_284
}
_284
_284
function createTaskQueues() {
_284
var ctx = this;
_284
return this.client.activities.list()
_284
.then(function(activities) {
_284
var busyActivity = find(activities, {friendlyName: 'Busy'});
_284
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
_284
_284
return Promise.all([
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'SMS',
_284
targetWorkers: 'products HAS "ProgrammableSMS"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Voice',
_284
targetWorkers: 'products HAS "ProgrammableVoice"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Default',
_284
targetWorkers: '1==1',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
])
_284
.then(function(queues) {
_284
ctx.queues = queues;
_284
});
_284
});
_284
}
_284
_284
function createWorkers() {
_284
var ctx = this;
_284
_284
return Promise.all([
_284
ctx.createWorker({
_284
name: 'Bob',
_284
phoneNumber: process.env.BOB_NUMBER,
_284
products: ['ProgrammableSMS'],
_284
}),
_284
ctx.createWorker({
_284
name: 'Alice',
_284
phoneNumber: process.env.ALICE_NUMBER,
_284
products: ['ProgrammableVoice'],
_284
})
_284
])
_284
.then(function(workers) {
_284
var bobWorker = workers[0];
_284
var aliceWorker = workers[1];
_284
var workerInfo = {};
_284
_284
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
_284
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
_284
_284
return workerInfo;
_284
});
_284
}
_284
_284
function createWorkflowActivities() {
_284
var ctx = this;
_284
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
_284
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var existingActivities = map(activities, 'friendlyName');
_284
_284
var missingActivities = difference(activityNames, existingActivities);
_284
_284
var newActivities = map(missingActivities, function(friendlyName) {
_284
return ctx.client.activities
_284
.create({
_284
friendlyName: friendlyName,
_284
available: 'true'
_284
});
_284
});
_284
_284
return Promise.all(newActivities);
_284
})
_284
.then(function() {
_284
return ctx.client.activities.list();
_284
});
_284
}
_284
_284
function createWorkflowConfig() {
_284
var queues = this.queues;
_284
_284
if (!queues) {
_284
throw new Error('Queues must be initialized.');
_284
}
_284
_284
var defaultTarget = {
_284
queue: find(queues, {friendlyName: 'Default'}).sid,
_284
timeout: 30,
_284
priority: 1,
_284
};
_284
_284
var smsTarget = {
_284
queue: find(queues, {friendlyName: 'SMS'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var voiceTarget = {
_284
queue: find(queues, {friendlyName: 'Voice'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var rules = [
_284
{
_284
expression: 'selected_product=="ProgrammableSMS"',
_284
targets: [smsTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
{
_284
expression: 'selected_product=="ProgrammableVoice"',
_284
targets: [voiceTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
];
_284
_284
var config = {
_284
task_routing: {
_284
filters: rules,
_284
default_filter: defaultTarget,
_284
},
_284
};
_284
_284
return JSON.stringify(config);
_284
}
_284
_284
function setup() {
_284
var ctx = this;
_284
_284
ctx.initClient();
_284
_284
return this.initWorkspace()
_284
.then(createWorkflowActivities.bind(ctx))
_284
.then(createTaskQueues.bind(ctx))
_284
.then(createWorkflow.bind(ctx))
_284
.then(function(workspaceInfo) {
_284
return ctx.createWorkers()
_284
.then(function(workerInfo) {
_284
return [workerInfo, workspaceInfo];
_284
});
_284
});
_284
}
_284
_284
function findByFriendlyName(friendlyName) {
_284
var client = this.client;
_284
_284
return client.list()
_284
.then(function (data) {
_284
return find(data, {friendlyName: friendlyName});
_284
});
_284
}
_284
_284
function deleteByFriendlyName(friendlyName) {
_284
var ctx = this;
_284
_284
return this.findByFriendlyName(friendlyName)
_284
.then(function(workspace) {
_284
if (workspace.remove) {
_284
return workspace.remove();
_284
}
_284
});
_284
}
_284
_284
function createWorkspace() {
_284
return this.client.create({
_284
friendlyName: WORKSPACE_NAME,
_284
EVENT_CALLBACKUrl: EVENT_CALLBACK,
_284
});
_284
}
_284
_284
function initWorkspace() {
_284
var ctx = this;
_284
var client = this.client;
_284
_284
return ctx.findByFriendlyName(WORKSPACE_NAME)
_284
.then(function(workspace) {
_284
var newWorkspace;
_284
_284
if (workspace) {
_284
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
_284
.then(createWorkspace.bind(ctx));
_284
} else {
_284
newWorkspace = ctx.createWorkspace();
_284
}
_284
_284
return newWorkspace;
_284
})
_284
.then(function(workspace) {
_284
ctx.initClient(workspace.sid);
_284
_284
return workspace;
_284
});
_284
}
_284
_284
return {
_284
createTaskQueues: createTaskQueues,
_284
createWorker: createWorker,
_284
createWorkers: createWorkers,
_284
createWorkflow: createWorkflow,
_284
createWorkflowActivities: createWorkflowActivities,
_284
createWorkflowConfig: createWorkflowConfig,
_284
createWorkspace: createWorkspace,
_284
deleteByFriendlyName: deleteByFriendlyName,
_284
findByFriendlyName: findByFriendlyName,
_284
initClient: initClient,
_284
initWorkspace: initWorkspace,
_284
setup: setup,
_284
};
_284
};

After creating our workers, let's set up the Task Queues.


Next, we set up the Task Queues. Each with a friendlyName and a targetWorkers, which is an expression to match Workers. Our Task Queues are:

  1. SMS - Will target Workers specialized in Programmable SMS, such as Bob, using the expression '"ProgrammableSMS" in products' .
  2. Voice - Will do the same for Programmable Voice Workers, such as Alice, using the expression '"ProgrammableVoice" in products' .
  3. Default - This queue targets all users and can be used when there are no specialist around for the chosen product. We can use the "1==1" expression here.

lib/workspace.js


_284
'use strict';
_284
_284
var twilio = require('twilio');
_284
var find = require('lodash/find');
_284
var map = require('lodash/map');
_284
var difference = require('lodash/difference');
_284
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
_284
var HOST = process.env.HOST;
_284
var EVENT_CALLBACK = `${HOST}/events`;
_284
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
_284
var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
_284
_284
module.exports = function() {
_284
function initClient(existingWorkspaceSid) {
_284
if (!existingWorkspaceSid) {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN).taskrouter.v1.workspaces;
_284
} else {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN)
_284
.taskrouter.v1.workspaces(existingWorkspaceSid);
_284
}
_284
}
_284
_284
function createWorker(opts) {
_284
var ctx = this;
_284
_284
return this.client.activities.list({friendlyName: 'Idle'})
_284
.then(function(idleActivity) {
_284
return ctx.client.workers.create({
_284
friendlyName: opts.name,
_284
attributes: JSON.stringify({
_284
'products': opts.products,
_284
'contact_uri': opts.phoneNumber,
_284
}),
_284
activitySid: idleActivity.sid,
_284
});
_284
});
_284
}
_284
_284
function createWorkflow() {
_284
var ctx = this;
_284
var config = this.createWorkflowConfig();
_284
_284
return ctx.client.workflows
_284
.create({
_284
friendlyName: 'Sales',
_284
assignmentCallbackUrl: HOST + '/call/assignment',
_284
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
_284
taskReservationTimeout: 15,
_284
configuration: config,
_284
})
_284
.then(function(workflow) {
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var idleActivity = find(activities, {friendlyName: 'Idle'});
_284
var offlineActivity = find(activities, {friendlyName: 'Offline'});
_284
_284
return {
_284
workflowSid: workflow.sid,
_284
activities: {
_284
idle: idleActivity.sid,
_284
offline: offlineActivity.sid,
_284
},
_284
workspaceSid: ctx.client._solution.sid,
_284
};
_284
});
_284
});
_284
}
_284
_284
function createTaskQueues() {
_284
var ctx = this;
_284
return this.client.activities.list()
_284
.then(function(activities) {
_284
var busyActivity = find(activities, {friendlyName: 'Busy'});
_284
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
_284
_284
return Promise.all([
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'SMS',
_284
targetWorkers: 'products HAS "ProgrammableSMS"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Voice',
_284
targetWorkers: 'products HAS "ProgrammableVoice"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Default',
_284
targetWorkers: '1==1',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
])
_284
.then(function(queues) {
_284
ctx.queues = queues;
_284
});
_284
});
_284
}
_284
_284
function createWorkers() {
_284
var ctx = this;
_284
_284
return Promise.all([
_284
ctx.createWorker({
_284
name: 'Bob',
_284
phoneNumber: process.env.BOB_NUMBER,
_284
products: ['ProgrammableSMS'],
_284
}),
_284
ctx.createWorker({
_284
name: 'Alice',
_284
phoneNumber: process.env.ALICE_NUMBER,
_284
products: ['ProgrammableVoice'],
_284
})
_284
])
_284
.then(function(workers) {
_284
var bobWorker = workers[0];
_284
var aliceWorker = workers[1];
_284
var workerInfo = {};
_284
_284
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
_284
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
_284
_284
return workerInfo;
_284
});
_284
}
_284
_284
function createWorkflowActivities() {
_284
var ctx = this;
_284
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
_284
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var existingActivities = map(activities, 'friendlyName');
_284
_284
var missingActivities = difference(activityNames, existingActivities);
_284
_284
var newActivities = map(missingActivities, function(friendlyName) {
_284
return ctx.client.activities
_284
.create({
_284
friendlyName: friendlyName,
_284
available: 'true'
_284
});
_284
});
_284
_284
return Promise.all(newActivities);
_284
})
_284
.then(function() {
_284
return ctx.client.activities.list();
_284
});
_284
}
_284
_284
function createWorkflowConfig() {
_284
var queues = this.queues;
_284
_284
if (!queues) {
_284
throw new Error('Queues must be initialized.');
_284
}
_284
_284
var defaultTarget = {
_284
queue: find(queues, {friendlyName: 'Default'}).sid,
_284
timeout: 30,
_284
priority: 1,
_284
};
_284
_284
var smsTarget = {
_284
queue: find(queues, {friendlyName: 'SMS'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var voiceTarget = {
_284
queue: find(queues, {friendlyName: 'Voice'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var rules = [
_284
{
_284
expression: 'selected_product=="ProgrammableSMS"',
_284
targets: [smsTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
{
_284
expression: 'selected_product=="ProgrammableVoice"',
_284
targets: [voiceTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
];
_284
_284
var config = {
_284
task_routing: {
_284
filters: rules,
_284
default_filter: defaultTarget,
_284
},
_284
};
_284
_284
return JSON.stringify(config);
_284
}
_284
_284
function setup() {
_284
var ctx = this;
_284
_284
ctx.initClient();
_284
_284
return this.initWorkspace()
_284
.then(createWorkflowActivities.bind(ctx))
_284
.then(createTaskQueues.bind(ctx))
_284
.then(createWorkflow.bind(ctx))
_284
.then(function(workspaceInfo) {
_284
return ctx.createWorkers()
_284
.then(function(workerInfo) {
_284
return [workerInfo, workspaceInfo];
_284
});
_284
});
_284
}
_284
_284
function findByFriendlyName(friendlyName) {
_284
var client = this.client;
_284
_284
return client.list()
_284
.then(function (data) {
_284
return find(data, {friendlyName: friendlyName});
_284
});
_284
}
_284
_284
function deleteByFriendlyName(friendlyName) {
_284
var ctx = this;
_284
_284
return this.findByFriendlyName(friendlyName)
_284
.then(function(workspace) {
_284
if (workspace.remove) {
_284
return workspace.remove();
_284
}
_284
});
_284
}
_284
_284
function createWorkspace() {
_284
return this.client.create({
_284
friendlyName: WORKSPACE_NAME,
_284
EVENT_CALLBACKUrl: EVENT_CALLBACK,
_284
});
_284
}
_284
_284
function initWorkspace() {
_284
var ctx = this;
_284
var client = this.client;
_284
_284
return ctx.findByFriendlyName(WORKSPACE_NAME)
_284
.then(function(workspace) {
_284
var newWorkspace;
_284
_284
if (workspace) {
_284
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
_284
.then(createWorkspace.bind(ctx));
_284
} else {
_284
newWorkspace = ctx.createWorkspace();
_284
}
_284
_284
return newWorkspace;
_284
})
_284
.then(function(workspace) {
_284
ctx.initClient(workspace.sid);
_284
_284
return workspace;
_284
});
_284
}
_284
_284
return {
_284
createTaskQueues: createTaskQueues,
_284
createWorker: createWorker,
_284
createWorkers: createWorkers,
_284
createWorkflow: createWorkflow,
_284
createWorkflowActivities: createWorkflowActivities,
_284
createWorkflowConfig: createWorkflowConfig,
_284
createWorkspace: createWorkspace,
_284
deleteByFriendlyName: deleteByFriendlyName,
_284
findByFriendlyName: findByFriendlyName,
_284
initClient: initClient,
_284
initWorkspace: initWorkspace,
_284
setup: setup,
_284
};
_284
};

We have a Workspace, Workers and Task Queues... what's left? A Workflow. Let's see how to create one next!


Finally, we create the Workflow using the following parameters:

  1. friendlyName as the name of a Workflow.
  2. assignmentCallbackUrl and fallbackAssignmentCallbackUrl as the public URL where a request will be made when this Workflow assigns a Task to a Worker. We will learn how to implement it on the next steps.
  3. taskReservationTimeout as the maximum time we want to wait until a Worker is available for handling a Task.
  4. configuration which is a set of rules for placing Tasks into Task Queues. The routing configuration will take a Task's attribute and match this with Task Queues. This application's Workflow rules are defined as:

    • "selected_product==\ "ProgrammableSMS\"" expression for SMS Task Queue. This expression will match any Task with ProgrammableSMS as the selected_product attribute.
    • "selected_product==\ "ProgrammableVoice\"" expression for Voice Task Queue.

lib/workspace.js


_284
'use strict';
_284
_284
var twilio = require('twilio');
_284
var find = require('lodash/find');
_284
var map = require('lodash/map');
_284
var difference = require('lodash/difference');
_284
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
_284
var HOST = process.env.HOST;
_284
var EVENT_CALLBACK = `${HOST}/events`;
_284
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
_284
var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
_284
_284
module.exports = function() {
_284
function initClient(existingWorkspaceSid) {
_284
if (!existingWorkspaceSid) {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN).taskrouter.v1.workspaces;
_284
} else {
_284
this.client = twilio(ACCOUNT_SID, AUTH_TOKEN)
_284
.taskrouter.v1.workspaces(existingWorkspaceSid);
_284
}
_284
}
_284
_284
function createWorker(opts) {
_284
var ctx = this;
_284
_284
return this.client.activities.list({friendlyName: 'Idle'})
_284
.then(function(idleActivity) {
_284
return ctx.client.workers.create({
_284
friendlyName: opts.name,
_284
attributes: JSON.stringify({
_284
'products': opts.products,
_284
'contact_uri': opts.phoneNumber,
_284
}),
_284
activitySid: idleActivity.sid,
_284
});
_284
});
_284
}
_284
_284
function createWorkflow() {
_284
var ctx = this;
_284
var config = this.createWorkflowConfig();
_284
_284
return ctx.client.workflows
_284
.create({
_284
friendlyName: 'Sales',
_284
assignmentCallbackUrl: HOST + '/call/assignment',
_284
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
_284
taskReservationTimeout: 15,
_284
configuration: config,
_284
})
_284
.then(function(workflow) {
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var idleActivity = find(activities, {friendlyName: 'Idle'});
_284
var offlineActivity = find(activities, {friendlyName: 'Offline'});
_284
_284
return {
_284
workflowSid: workflow.sid,
_284
activities: {
_284
idle: idleActivity.sid,
_284
offline: offlineActivity.sid,
_284
},
_284
workspaceSid: ctx.client._solution.sid,
_284
};
_284
});
_284
});
_284
}
_284
_284
function createTaskQueues() {
_284
var ctx = this;
_284
return this.client.activities.list()
_284
.then(function(activities) {
_284
var busyActivity = find(activities, {friendlyName: 'Busy'});
_284
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
_284
_284
return Promise.all([
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'SMS',
_284
targetWorkers: 'products HAS "ProgrammableSMS"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Voice',
_284
targetWorkers: 'products HAS "ProgrammableVoice"',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
ctx.client.taskQueues.create({
_284
friendlyName: 'Default',
_284
targetWorkers: '1==1',
_284
assignmentActivitySid: busyActivity.sid,
_284
reservationActivitySid: reservedActivity.sid,
_284
}),
_284
])
_284
.then(function(queues) {
_284
ctx.queues = queues;
_284
});
_284
});
_284
}
_284
_284
function createWorkers() {
_284
var ctx = this;
_284
_284
return Promise.all([
_284
ctx.createWorker({
_284
name: 'Bob',
_284
phoneNumber: process.env.BOB_NUMBER,
_284
products: ['ProgrammableSMS'],
_284
}),
_284
ctx.createWorker({
_284
name: 'Alice',
_284
phoneNumber: process.env.ALICE_NUMBER,
_284
products: ['ProgrammableVoice'],
_284
})
_284
])
_284
.then(function(workers) {
_284
var bobWorker = workers[0];
_284
var aliceWorker = workers[1];
_284
var workerInfo = {};
_284
_284
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
_284
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
_284
_284
return workerInfo;
_284
});
_284
}
_284
_284
function createWorkflowActivities() {
_284
var ctx = this;
_284
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
_284
_284
return ctx.client.activities.list()
_284
.then(function(activities) {
_284
var existingActivities = map(activities, 'friendlyName');
_284
_284
var missingActivities = difference(activityNames, existingActivities);
_284
_284
var newActivities = map(missingActivities, function(friendlyName) {
_284
return ctx.client.activities
_284
.create({
_284
friendlyName: friendlyName,
_284
available: 'true'
_284
});
_284
});
_284
_284
return Promise.all(newActivities);
_284
})
_284
.then(function() {
_284
return ctx.client.activities.list();
_284
});
_284
}
_284
_284
function createWorkflowConfig() {
_284
var queues = this.queues;
_284
_284
if (!queues) {
_284
throw new Error('Queues must be initialized.');
_284
}
_284
_284
var defaultTarget = {
_284
queue: find(queues, {friendlyName: 'Default'}).sid,
_284
timeout: 30,
_284
priority: 1,
_284
};
_284
_284
var smsTarget = {
_284
queue: find(queues, {friendlyName: 'SMS'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var voiceTarget = {
_284
queue: find(queues, {friendlyName: 'Voice'}).sid,
_284
timeout: 30,
_284
priority: 5,
_284
};
_284
_284
var rules = [
_284
{
_284
expression: 'selected_product=="ProgrammableSMS"',
_284
targets: [smsTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
{
_284
expression: 'selected_product=="ProgrammableVoice"',
_284
targets: [voiceTarget, defaultTarget],
_284
timeout: 30,
_284
},
_284
];
_284
_284
var config = {
_284
task_routing: {
_284
filters: rules,
_284
default_filter: defaultTarget,
_284
},
_284
};
_284
_284
return JSON.stringify(config);
_284
}
_284
_284
function setup() {
_284
var ctx = this;
_284
_284
ctx.initClient();
_284
_284
return this.initWorkspace()
_284
.then(createWorkflowActivities.bind(ctx))
_284
.then(createTaskQueues.bind(ctx))
_284
.then(createWorkflow.bind(ctx))
_284
.then(function(workspaceInfo) {
_284
return ctx.createWorkers()
_284
.then(function(workerInfo) {
_284
return [workerInfo, workspaceInfo];
_284
});
_284
});
_284
}
_284
_284
function findByFriendlyName(friendlyName) {
_284
var client = this.client;
_284
_284
return client.list()
_284
.then(function (data) {
_284
return find(data, {friendlyName: friendlyName});
_284
});
_284
}
_284
_284
function deleteByFriendlyName(friendlyName) {
_284
var ctx = this;
_284
_284
return this.findByFriendlyName(friendlyName)
_284
.then(function(workspace) {
_284
if (workspace.remove) {
_284
return workspace.remove();
_284
}
_284
});
_284
}
_284
_284
function createWorkspace() {
_284
return this.client.create({
_284
friendlyName: WORKSPACE_NAME,
_284
EVENT_CALLBACKUrl: EVENT_CALLBACK,
_284
});
_284
}
_284
_284
function initWorkspace() {
_284
var ctx = this;
_284
var client = this.client;
_284
_284
return ctx.findByFriendlyName(WORKSPACE_NAME)
_284
.then(function(workspace) {
_284
var newWorkspace;
_284
_284
if (workspace) {
_284
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
_284
.then(createWorkspace.bind(ctx));
_284
} else {
_284
newWorkspace = ctx.createWorkspace();
_284
}
_284
_284
return newWorkspace;
_284
})
_284
.then(function(workspace) {
_284
ctx.initClient(workspace.sid);
_284
_284
return workspace;
_284
});
_284
}
_284
_284
return {
_284
createTaskQueues: createTaskQueues,
_284
createWorker: createWorker,
_284
createWorkers: createWorkers,
_284
createWorkflow: createWorkflow,
_284
createWorkflowActivities: createWorkflowActivities,
_284
createWorkflowConfig: createWorkflowConfig,
_284
createWorkspace: createWorkspace,
_284
deleteByFriendlyName: deleteByFriendlyName,
_284
findByFriendlyName: findByFriendlyName,
_284
initClient: initClient,
_284
initWorkspace: initWorkspace,
_284
setup: setup,
_284
};
_284
};

Our workspace is completely setup. Now it's time to see how we use it to route calls.


Handle Twilio's Request

handle-twilios-request page anchor

Right after receiving a call, Twilio will send a request to the URL specified on the number's configuration.

The endpoint will then process the request and generate a TwiML response. We'll use the Say verb to give the user product alternatives they can select by pressing a key. The Gather verb allows us to capture the user's key press.

Handling Twilio's Requests

handling-twilios-requests page anchor

routes/call.js


_45
'use strict';
_45
_45
var express = require('express'),
_45
router = express.Router(),
_45
VoiceResponse = require('twilio/lib/twiml/VoiceResponse');
_45
_45
module.exports = function (app) {
_45
// POST /call/incoming
_45
router.post('/incoming/', function (req, res) {
_45
var twimlResponse = new VoiceResponse();
_45
var gather = twimlResponse.gather({
_45
numDigits: 1,
_45
action: '/call/enqueue',
_45
method: 'POST'
_45
});
_45
gather.say('For Programmable SMS, press one. For Voice, press any other key.');
_45
res.type('text/xml');
_45
res.send(twimlResponse.toString());
_45
});
_45
_45
// POST /call/enqueue
_45
router.post('/enqueue/', function (req, res) {
_45
var pressedKey = req.body.Digits;
_45
var twimlResponse = new VoiceResponse();
_45
var selectedProduct = (pressedKey === '1') ? 'ProgrammableSMS' : 'ProgrammableVoice';
_45
var enqueue = twimlResponse.enqueueTask(
_45
{workflowSid: app.get('workspaceInfo').workflowSid}
_45
);
_45
enqueue.task({}, JSON.stringify({selected_product: selectedProduct}));
_45
_45
res.type('text/xml');
_45
res.send(twimlResponse.toString());
_45
});
_45
_45
// POST /call/assignment
_45
router.post('/assignment/', function (req, res) {
_45
res.type('application/json');
_45
res.send({
_45
instruction: "dequeue",
_45
post_work_activity_sid: app.get('workspaceInfo').activities.idle
_45
});
_45
});
_45
_45
return router;
_45
};

We just asked the caller to choose a product, next we will use their choice to create the appropiate Task.


This is the endpoint set as the action URL on the Gather verb on the previous step. A request is made to this endpoint when the user presses a key during the call. This request has a Digits parameter that holds the pressed keys. A Task will be created based on the pressed digit with the selected_product as an attribute. The Workflow will take this Task's attributes and match with the configured expressions in order to find a Task Queue for this Task, so an appropriate available Worker can be assigned to handle it.

We use the Enqueue verb with a WorkflowSid attribute to integrate with TaskRouter. Then the voice call will be put on hold while TaskRouter tries to find an available Worker to handle this Task.

routes/call.js


_45
'use strict';
_45
_45
var express = require('express'),
_45
router = express.Router(),
_45
VoiceResponse = require('twilio/lib/twiml/VoiceResponse');
_45
_45
module.exports = function (app) {
_45
// POST /call/incoming
_45
router.post('/incoming/', function (req, res) {
_45
var twimlResponse = new VoiceResponse();
_45
var gather = twimlResponse.gather({
_45
numDigits: 1,
_45
action: '/call/enqueue',
_45
method: 'POST'
_45
});
_45
gather.say('For Programmable SMS, press one. For Voice, press any other key.');
_45
res.type('text/xml');
_45
res.send(twimlResponse.toString());
_45
});
_45
_45
// POST /call/enqueue
_45
router.post('/enqueue/', function (req, res) {
_45
var pressedKey = req.body.Digits;
_45
var twimlResponse = new VoiceResponse();
_45
var selectedProduct = (pressedKey === '1') ? 'ProgrammableSMS' : 'ProgrammableVoice';
_45
var enqueue = twimlResponse.enqueueTask(
_45
{workflowSid: app.get('workspaceInfo').workflowSid}
_45
);
_45
enqueue.task({}, JSON.stringify({selected_product: selectedProduct}));
_45
_45
res.type('text/xml');
_45
res.send(twimlResponse.toString());
_45
});
_45
_45
// POST /call/assignment
_45
router.post('/assignment/', function (req, res) {
_45
res.type('application/json');
_45
res.send({
_45
instruction: "dequeue",
_45
post_work_activity_sid: app.get('workspaceInfo').activities.idle
_45
});
_45
});
_45
_45
return router;
_45
};

After sending a Task to Twilio, let's see how we tell TaskRouter which Worker to use to execute that task.


When TaskRouter selects a Worker, it does the following:

  1. The Task's Assignment Status is set to 'reserved'.
  2. A Reservation instance is generated, linking the Task to the selected Worker.
  3. At the same time the Reservation is created, a POST request is made to the Workflow's AssignmentCallbackURL, which was configured while creating the Workflow. This request includes the full details of the Task, the selected Worker, and the Reservation.

Handling this Assignment Callback is a key component of building a TaskRouter application as we can instruct how the Worker will handle a Task. We could send a text, email, push notifications or make a call.

Since we created this Task during a voice call with an Enqueue verb, lets instruct TaskRouter to dequeue the call and dial a Worker. If we do not specify a to parameter with a phone number, TaskRouter will pick the Worker's contact_uri attribute.

We also send a post_work_activity_sid which will tell TaskRouter which Activity to assign this worker after the call ends.

routes/call.js


_45
'use strict';
_45
_45
var express = require('express'),
_45
router = express.Router(),
_45
VoiceResponse = require('twilio/lib/twiml/VoiceResponse');
_45
_45
module.exports = function (app) {
_45
// POST /call/incoming
_45
router.post('/incoming/', function (req, res) {
_45
var twimlResponse = new VoiceResponse();
_45
var gather = twimlResponse.gather({
_45
numDigits: 1,
_45
action: '/call/enqueue',
_45
method: 'POST'
_45
});
_45
gather.say('For Programmable SMS, press one. For Voice, press any other key.');
_45
res.type('text/xml');
_45
res.send(twimlResponse.toString());
_45
});
_45
_45
// POST /call/enqueue
_45
router.post('/enqueue/', function (req, res) {
_45
var pressedKey = req.body.Digits;
_45
var twimlResponse = new VoiceResponse();
_45
var selectedProduct = (pressedKey === '1') ? 'ProgrammableSMS' : 'ProgrammableVoice';
_45
var enqueue = twimlResponse.enqueueTask(
_45
{workflowSid: app.get('workspaceInfo').workflowSid}
_45
);
_45
enqueue.task({}, JSON.stringify({selected_product: selectedProduct}));
_45
_45
res.type('text/xml');
_45
res.send(twimlResponse.toString());
_45
});
_45
_45
// POST /call/assignment
_45
router.post('/assignment/', function (req, res) {
_45
res.type('application/json');
_45
res.send({
_45
instruction: "dequeue",
_45
post_work_activity_sid: app.get('workspaceInfo').activities.idle
_45
});
_45
});
_45
_45
return router;
_45
};

Now that our Tasks are routed properly, let's deal with missed calls in the next step.


This endpoint will be called after each TaskRouter Event is triggered. In our application, we are trying to collect missed calls, so we would like to handle the workflow.timeout event. This event is triggered when the Task waits more than the limit set on the Workflow Configuration-- or rather when no worker is available.

Here we use TwilioRestClient to route this call to a Voicemail Twimlet. Twimlets are tiny web applications for voice. This one will generate a TwiML response using Say verb and record a message using Record verb. The recorded message will then be transcribed and sent to the email address configured.

Note that we are also listening for task.canceled. This is triggered when the customer hangs up before being assigned to an agent, therefore canceling the task. Capturing this event allows us to collect the information from the customers that hang up before the Workflow times out.

routes/events.js


_71
'use strict';
_71
_71
var express = require('express'),
_71
MissedCall = require('../models/missed-call'),
_71
util = require('util'),
_71
querystring = require('querystring'),
_71
router = express.Router(),
_71
Q = require('q');
_71
_71
// POST /events
_71
router.post('/', function (req, res) {
_71
var eventType = req.body.EventType;
_71
var taskAttributes = (req.body.TaskAttributes)? JSON.parse(req.body.TaskAttributes) : {};
_71
_71
function saveMissedCall(){
_71
return MissedCall.create({
_71
selectedProduct: taskAttributes.selected_product,
_71
phoneNumber: taskAttributes.from
_71
});
_71
}
_71
_71
var eventHandler = {
_71
'task.canceled': saveMissedCall,
_71
'workflow.timeout': function() {
_71
return saveMissedCall().then(voicemail(taskAttributes.call_sid));
_71
},
_71
'worker.activity.update': function(){
_71
var workerAttributes = JSON.parse(req.body.WorkerAttributes);
_71
if (req.body.WorkerActivityName === 'Offline') {
_71
notifyOfflineStatus(workerAttributes.contact_uri);
_71
}
_71
return Q.resolve({});
_71
},
_71
'default': function() { return Q.resolve({}); }
_71
};
_71
_71
(eventHandler[eventType] || eventHandler['default'])().then(function () {
_71
res.json({});
_71
});
_71
});
_71
_71
function voicemail (callSid){
_71
var client = buildClient(),
_71
query = querystring.stringify({
_71
Message: 'Sorry, All agents are busy. Please leave a message. We\'ll call you as soon as possible',
_71
Email: process.env.MISSED_CALLS_EMAIL_ADDRESS}),
_71
voicemailUrl = util.format("http://twimlets.com/voicemail?%s", query);
_71
_71
client.calls(callSid).update({
_71
method: 'POST',
_71
url: voicemailUrl
_71
});
_71
}
_71
_71
function notifyOfflineStatus(phone_number) {
_71
var client = buildClient(),
_71
message = 'Your status has changed to Offline. Reply with "On" to get back Online';
_71
client.sendMessage({
_71
to: phone_number,
_71
from: process.env.TWILIO_NUMBER,
_71
body: message
_71
});
_71
}
_71
_71
function buildClient() {
_71
var accountSid = process.env.TWILIO_ACCOUNT_SID,
_71
authToken = process.env.TWILIO_AUTH_TOKEN;
_71
return require('twilio')(accountSid, authToken);
_71
}
_71
_71
module.exports = router;

Most of the features of our application are implemented. The last piece is allowing the Workers to change their availability status. Let's see how to do that next.


Change a Worker's Activity

change-a-workers-activity page anchor

We have created this endpoint, so a worker can send an SMS message to the support line with the command "On" or "Off" to change their availability status.

This is important as a worker's activity will change to Offline when they miss a call. When this happens, they receive an SMS letting them know that their activity has changed, and that they can reply with the On command to make themselves available for incoming calls again.

Handle Message to update the Worker Status

handle-message-to-update-the-worker-status page anchor

routes/sms.js


_27
'use strict';
_27
_27
var express = require('express'),
_27
router = express.Router(),
_27
twimlGenerator = require('../lib/twiml-generator');
_27
_27
module.exports = function (app) {
_27
// POST /sms/incoming
_27
router.post('/incoming/', function (req, res) {
_27
var targetActivity = (req.body.Body.toLowerCase() === "on")? "idle":"offline";
_27
var activitySid = app.get('workspaceInfo').activities[targetActivity];
_27
changeWorkerActivitySid(req.body.From, activitySid);
_27
res.type('text/xml');
_27
res.send(twimlGenerator.generateConfirmMessage(targetActivity));
_27
});
_27
_27
function changeWorkerActivitySid(workerNumber, activitySid){
_27
var accountSid = process.env.TWILIO_ACCOUNT_SID,
_27
authToken = process.env.TWILIO_AUTH_TOKEN,
_27
workspaceSid = app.get('workspaceInfo').workspaceSid,
_27
workerSid = app.get('workerInfo')[workerNumber],
_27
twilio = require('twilio'),
_27
client = new twilio.TaskRouterClient(accountSid, authToken, workspaceSid);
_27
client.workspace.workers(workerSid).update({activitySid: activitySid});
_27
}
_27
return router;
_27
};

Congratulations! You finished this tutorial. As you can see, using Twilio's TaskRouter is quite simple.


If you're a Node.js/Express developer working with Twilio, you might enjoy these other tutorials:

Warm-Transfer

Have you ever been disconnected from a support call while being transferred to another support agent? Warm transfer eliminates this problem. Using Twilio powered warm transfers your agents will have the ability to conference in another agent in realtime.

Automated-Survey(link takes you to an external page)

Instantly collect structured data from your users with a survey conducted over a call or SMS text messages.

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think!


Rate this page: