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.
In order to instruct TaskRouter to handle the Tasks, we need to configure a Workspace. We can do this in the TaskRouter Console 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:
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.
lib/workspace.js
_284'use strict';_284_284var twilio = require('twilio');_284var find = require('lodash/find');_284var map = require('lodash/map');_284var difference = require('lodash/difference');_284var WORKSPACE_NAME = 'TaskRouter Node Workspace';_284var HOST = process.env.HOST;_284var EVENT_CALLBACK = `${HOST}/events`;_284var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;_284var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;_284_284module.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_284var twilio = require('twilio');_284var find = require('lodash/find');_284var map = require('lodash/map');_284var difference = require('lodash/difference');_284var WORKSPACE_NAME = 'TaskRouter Node Workspace';_284var HOST = process.env.HOST;_284var EVENT_CALLBACK = `${HOST}/events`;_284var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;_284var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;_284_284module.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_284var twilio = require('twilio');_284var find = require('lodash/find');_284var map = require('lodash/map');_284var difference = require('lodash/difference');_284var WORKSPACE_NAME = 'TaskRouter Node Workspace';_284var HOST = process.env.HOST;_284var EVENT_CALLBACK = `${HOST}/events`;_284var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;_284var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;_284_284module.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:
SMS
- Will target Workers specialized in Programmable SMS, such as Bob, using the expression
'"ProgrammableSMS" in products'
.
Voice
- Will do the same for Programmable Voice Workers, such as Alice, using the expression
'"ProgrammableVoice" in products'
.
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_284var twilio = require('twilio');_284var find = require('lodash/find');_284var map = require('lodash/map');_284var difference = require('lodash/difference');_284var WORKSPACE_NAME = 'TaskRouter Node Workspace';_284var HOST = process.env.HOST;_284var EVENT_CALLBACK = `${HOST}/events`;_284var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;_284var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;_284_284module.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:
friendlyName
as the name of a Workflow.
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.
taskReservationTimeout
as the maximum time we want to wait until a Worker is available for handling a Task.
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_284var twilio = require('twilio');_284var find = require('lodash/find');_284var map = require('lodash/map');_284var difference = require('lodash/difference');_284var WORKSPACE_NAME = 'TaskRouter Node Workspace';_284var HOST = process.env.HOST;_284var EVENT_CALLBACK = `${HOST}/events`;_284var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;_284var AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;_284_284module.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.
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.
routes/call.js
_45'use strict';_45_45var express = require('express'),_45 router = express.Router(),_45 VoiceResponse = require('twilio/lib/twiml/VoiceResponse');_45_45module.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 appropriate 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_45var express = require('express'),_45 router = express.Router(),_45 VoiceResponse = require('twilio/lib/twiml/VoiceResponse');_45_45module.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:
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_45var express = require('express'),_45 router = express.Router(),_45 VoiceResponse = require('twilio/lib/twiml/VoiceResponse');_45_45module.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_71var 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_71router.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_71function 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_71function 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_71function buildClient() {_71 var accountSid = process.env.TWILIO_ACCOUNT_SID,_71 authToken = process.env.TWILIO_AUTH_TOKEN;_71 return require('twilio')(accountSid, authToken);_71}_71_71module.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.
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.
routes/sms.js
_27'use strict';_27_27var express = require('express'),_27 router = express.Router(),_27 twimlGenerator = require('../lib/twiml-generator');_27_27module.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:
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.
Instantly collect structured data from your users with a survey conducted over a call or SMS text messages.