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.

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 buildClient configures and returns a TaskRouterClient, which is provided by the Twilio Node.js library.

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

lib/workspace.js

Create, Setup and Configure the Workspace

lib/workspace.js

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.

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

lib/workspace.js

Create Workspace

lib/workspace.js

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.

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

lib/workspace.js

Create Task Queues

lib/workspace.js

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

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.

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

lib/workspace.js

Create a Workflow

lib/workspace.js

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

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

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.

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, e-mail, 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.

Assign a Worker

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.

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

routes/events.js

Collect Missed Calls

routes/events.js

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.

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.

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

'use strict';vartwilio=require('twilio');varfind=require('lodash/find');varmap=require('lodash/map');vardifference=require('lodash/difference');varWORKSPACE_NAME='TaskRouter Node Workspace';varHOST=process.env.HOST;varEVENT_CALLBACK=`${HOST}/events`;varACCOUNT_SID=process.env.TWILIO_ACCOUNT_SID;varAUTH_TOKEN=process.env.TWILIO_AUTH_TOKEN;module.exports=function(){functioninitClient(existingWorkspaceSid){if(!existingWorkspaceSid){this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces;}else{this.client=twilio(ACCOUNT_SID,AUTH_TOKEN).taskrouter.v1.workspaces(existingWorkspaceSid);}}functioncreateWorker(opts){varctx=this;returnthis.client.activities.list({friendlyName:'Idle'}).then(function(idleActivity){returnctx.client.workers.create({friendlyName:opts.name,attributes:JSON.stringify({'products':opts.products,'contact_uri':opts.phoneNumber,}),activitySid:idleActivity.sid,});});}functioncreateWorkflow(){varctx=this;varconfig=this.createWorkflowConfig();returnctx.client.workflows.create({friendlyName:'Sales',assignmentCallbackUrl:HOST+'/call/assignment',fallbackAssignmentCallbackUrl:HOST+'/call/assignment',taskReservationTimeout:15,configuration:config,}).then(function(workflow){returnctx.client.activities.list().then(function(activities){varidleActivity=find(activities,{friendlyName:'Idle'});varofflineActivity=find(activities,{friendlyName:'Offline'});return{workflowSid:workflow.sid,activities:{idle:idleActivity.sid,offline:offlineActivity.sid,},workspaceSid:ctx.client._solution.sid,};});});}functioncreateTaskQueues(){varctx=this;returnthis.client.activities.list().then(function(activities){varbusyActivity=find(activities,{friendlyName:'Busy'});varreservedActivity=find(activities,{friendlyName:'Reserved'});returnPromise.all([ctx.client.taskQueues.create({friendlyName:'SMS',targetWorkers:'products HAS "ProgrammableSMS"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Voice',targetWorkers:'products HAS "ProgrammableVoice"',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),ctx.client.taskQueues.create({friendlyName:'Default',targetWorkers:'1==1',assignmentActivitySid:busyActivity.sid,reservationActivitySid:reservedActivity.sid,}),]).then(function(queues){ctx.queues=queues;});});}functioncreateWorkers(){varctx=this;returnPromise.all([ctx.createWorker({name:'Bob',phoneNumber:process.env.BOB_NUMBER,products:['ProgrammableSMS'],}),ctx.createWorker({name:'Alice',phoneNumber:process.env.ALICE_NUMBER,products:['ProgrammableVoice'],})]).then(function(workers){varbobWorker=workers[0];varaliceWorker=workers[1];varworkerInfo={};workerInfo[process.env.ALICE_NUMBER]=aliceWorker.sid;workerInfo[process.env.BOB_NUMBER]=bobWorker.sid;returnworkerInfo;});}functioncreateWorkflowActivities(){varctx=this;varactivityNames=['Idle','Busy','Offline','Reserved'];returnctx.client.activities.list().then(function(activities){varexistingActivities=map(activities,'friendlyName');varmissingActivities=difference(activityNames,existingActivities);varnewActivities=map(missingActivities,function(friendlyName){returnctx.client.activities.create({friendlyName:friendlyName,available:'true'});});returnPromise.all(newActivities);}).then(function(){returnctx.client.activities.list();});}functioncreateWorkflowConfig(){varqueues=this.queues;if(!queues){thrownewError('Queues must be initialized.');}vardefaultTarget={queue:find(queues,{friendlyName:'Default'}).sid,timeout:30,priority:1,};varsmsTarget={queue:find(queues,{friendlyName:'SMS'}).sid,timeout:30,priority:5,};varvoiceTarget={queue:find(queues,{friendlyName:'Voice'}).sid,timeout:30,priority:5,};varrules=[{expression:'selected_product=="ProgrammableSMS"',targets:[smsTarget,defaultTarget],timeout:30,},{expression:'selected_product=="ProgrammableVoice"',targets:[voiceTarget,defaultTarget],timeout:30,},];varconfig={task_routing:{filters:rules,default_filter:defaultTarget,},};returnJSON.stringify(config);}functionsetup(){varctx=this;ctx.initClient();returnthis.initWorkspace().then(createWorkflowActivities.bind(ctx)).then(createTaskQueues.bind(ctx)).then(createWorkflow.bind(ctx)).then(function(workspaceInfo){returnctx.createWorkers().then(function(workerInfo){return[workerInfo,workspaceInfo];});});}functionfindByFriendlyName(friendlyName){varclient=this.client;returnclient.list().then(function(data){returnfind(data,{friendlyName:friendlyName});});}functiondeleteByFriendlyName(friendlyName){varctx=this;returnthis.findByFriendlyName(friendlyName).then(function(workspace){if(workspace.remove){returnworkspace.remove();}});}functioncreateWorkspace(){returnthis.client.create({friendlyName:WORKSPACE_NAME,EVENT_CALLBACKUrl:EVENT_CALLBACK,});}functioninitWorkspace(){varctx=this;varclient=this.client;returnctx.findByFriendlyName(WORKSPACE_NAME).then(function(workspace){varnewWorkspace;if(workspace){newWorkspace=ctx.deleteByFriendlyName(WORKSPACE_NAME).then(createWorkspace.bind(ctx));}else{newWorkspace=ctx.createWorkspace();}returnnewWorkspace;}).then(function(workspace){ctx.initClient(workspace.sid);returnworkspace;});}return{createTaskQueues:createTaskQueues,createWorker:createWorker,createWorkers:createWorkers,createWorkflow:createWorkflow,createWorkflowActivities:createWorkflowActivities,createWorkflowConfig:createWorkflowConfig,createWorkspace:createWorkspace,deleteByFriendlyName:deleteByFriendlyName,findByFriendlyName:findByFriendlyName,initClient:initClient,initWorkspace:initWorkspace,setup:setup,};};

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