Skip to contentSkip to navigationSkip to topbar
Page tools
Useful for sharing or LLM

On this page
Looking for more inspiration?Visit the

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.


What the application does

what-the-application-does page anchor

At a high level, the application does the following:

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

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

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

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

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

To build a client for this API, you need a TWILIO_ACCOUNT_SID, TWILIO_API_KEY, and TWILIO_API_SECRET. Find your Account SID in the Twilio Console(link takes you to an external page) or the legacy Console(link takes you to an external page) and create an API key. The function initClient configures and returns a TaskRouterClient, which the Twilio Node.js library provides.

Create, Setup and Configure the Workspace

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

lib/workspace.js

1
'use strict';
2
3
var twilio = require('twilio');
4
var find = require('lodash/find');
5
var map = require('lodash/map');
6
var difference = require('lodash/difference');
7
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
8
var HOST = process.env.HOST;
9
var EVENT_CALLBACK = `${HOST}/events`;
10
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
11
var API_KEY = process.env.TWILIO_API_KEY;
12
var API_SECRET = process.env.TWILIO_API_SECRET;
13
14
module.exports = function() {
15
function initClient(existingWorkspaceSid) {
16
if (!existingWorkspaceSid) {
17
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID }).taskrouter.v1.workspaces;
18
} else {
19
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID })
20
.taskrouter.v1.workspaces(existingWorkspaceSid);
21
}
22
}
23
24
function createWorker(opts) {
25
var ctx = this;
26
27
return this.client.activities.list({friendlyName: 'Idle'})
28
.then(function(idleActivity) {
29
return ctx.client.workers.create({
30
friendlyName: opts.name,
31
attributes: JSON.stringify({
32
'products': opts.products,
33
'contact_uri': opts.phoneNumber,
34
}),
35
activitySid: idleActivity.sid,
36
});
37
});
38
}
39
40
function createWorkflow() {
41
var ctx = this;
42
var config = this.createWorkflowConfig();
43
44
return ctx.client.workflows
45
.create({
46
friendlyName: 'Sales',
47
assignmentCallbackUrl: HOST + '/call/assignment',
48
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
49
taskReservationTimeout: 15,
50
configuration: config,
51
})
52
.then(function(workflow) {
53
return ctx.client.activities.list()
54
.then(function(activities) {
55
var idleActivity = find(activities, {friendlyName: 'Idle'});
56
var offlineActivity = find(activities, {friendlyName: 'Offline'});
57
58
return {
59
workflowSid: workflow.sid,
60
activities: {
61
idle: idleActivity.sid,
62
offline: offlineActivity.sid,
63
},
64
workspaceSid: ctx.client._solution.sid,
65
};
66
});
67
});
68
}
69
70
function createTaskQueues() {
71
var ctx = this;
72
return this.client.activities.list()
73
.then(function(activities) {
74
var busyActivity = find(activities, {friendlyName: 'Busy'});
75
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
76
77
return Promise.all([
78
ctx.client.taskQueues.create({
79
friendlyName: 'SMS',
80
targetWorkers: 'products HAS "ProgrammableSMS"',
81
assignmentActivitySid: busyActivity.sid,
82
reservationActivitySid: reservedActivity.sid,
83
}),
84
ctx.client.taskQueues.create({
85
friendlyName: 'Voice',
86
targetWorkers: 'products HAS "ProgrammableVoice"',
87
assignmentActivitySid: busyActivity.sid,
88
reservationActivitySid: reservedActivity.sid,
89
}),
90
ctx.client.taskQueues.create({
91
friendlyName: 'Default',
92
targetWorkers: '1==1',
93
assignmentActivitySid: busyActivity.sid,
94
reservationActivitySid: reservedActivity.sid,
95
}),
96
])
97
.then(function(queues) {
98
ctx.queues = queues;
99
});
100
});
101
}
102
103
function createWorkers() {
104
var ctx = this;
105
106
return Promise.all([
107
ctx.createWorker({
108
name: 'Bob',
109
phoneNumber: process.env.BOB_NUMBER,
110
products: ['ProgrammableSMS'],
111
}),
112
ctx.createWorker({
113
name: 'Alice',
114
phoneNumber: process.env.ALICE_NUMBER,
115
products: ['ProgrammableVoice'],
116
})
117
])
118
.then(function(workers) {
119
var bobWorker = workers[0];
120
var aliceWorker = workers[1];
121
var workerInfo = {};
122
123
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
124
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
125
126
return workerInfo;
127
});
128
}
129
130
function createWorkflowActivities() {
131
var ctx = this;
132
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
133
134
return ctx.client.activities.list()
135
.then(function(activities) {
136
var existingActivities = map(activities, 'friendlyName');
137
138
var missingActivities = difference(activityNames, existingActivities);
139
140
var newActivities = map(missingActivities, function(friendlyName) {
141
return ctx.client.activities
142
.create({
143
friendlyName: friendlyName,
144
available: 'true'
145
});
146
});
147
148
return Promise.all(newActivities);
149
})
150
.then(function() {
151
return ctx.client.activities.list();
152
});
153
}
154
155
function createWorkflowConfig() {
156
var queues = this.queues;
157
158
if (!queues) {
159
throw new Error('Queues must be initialized.');
160
}
161
162
var defaultTarget = {
163
queue: find(queues, {friendlyName: 'Default'}).sid,
164
timeout: 30,
165
priority: 1,
166
};
167
168
var smsTarget = {
169
queue: find(queues, {friendlyName: 'SMS'}).sid,
170
timeout: 30,
171
priority: 5,
172
};
173
174
var voiceTarget = {
175
queue: find(queues, {friendlyName: 'Voice'}).sid,
176
timeout: 30,
177
priority: 5,
178
};
179
180
var rules = [
181
{
182
expression: 'selected_product=="ProgrammableSMS"',
183
targets: [smsTarget, defaultTarget],
184
timeout: 30,
185
},
186
{
187
expression: 'selected_product=="ProgrammableVoice"',
188
targets: [voiceTarget, defaultTarget],
189
timeout: 30,
190
},
191
];
192
193
var config = {
194
task_routing: {
195
filters: rules,
196
default_filter: defaultTarget,
197
},
198
};
199
200
return JSON.stringify(config);
201
}
202
203
function setup() {
204
var ctx = this;
205
206
ctx.initClient();
207
208
return this.initWorkspace()
209
.then(createWorkflowActivities.bind(ctx))
210
.then(createTaskQueues.bind(ctx))
211
.then(createWorkflow.bind(ctx))
212
.then(function(workspaceInfo) {
213
return ctx.createWorkers()
214
.then(function(workerInfo) {
215
return [workerInfo, workspaceInfo];
216
});
217
});
218
}
219
220
function findByFriendlyName(friendlyName) {
221
var client = this.client;
222
223
return client.list()
224
.then(function (data) {
225
return find(data, {friendlyName: friendlyName});
226
});
227
}
228
229
function deleteByFriendlyName(friendlyName) {
230
var ctx = this;
231
232
return this.findByFriendlyName(friendlyName)
233
.then(function(workspace) {
234
if (workspace.remove) {
235
return workspace.remove();
236
}
237
});
238
}
239
240
function createWorkspace() {
241
return this.client.create({
242
friendlyName: WORKSPACE_NAME,
243
EVENT_CALLBACKUrl: EVENT_CALLBACK,
244
});
245
}
246
247
function initWorkspace() {
248
var ctx = this;
249
var client = this.client;
250
251
return ctx.findByFriendlyName(WORKSPACE_NAME)
252
.then(function(workspace) {
253
var newWorkspace;
254
255
if (workspace) {
256
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
257
.then(createWorkspace.bind(ctx));
258
} else {
259
newWorkspace = ctx.createWorkspace();
260
}
261
262
return newWorkspace;
263
})
264
.then(function(workspace) {
265
ctx.initClient(workspace.sid);
266
267
return workspace;
268
});
269
}
270
271
return {
272
createTaskQueues: createTaskQueues,
273
createWorker: createWorker,
274
createWorkers: createWorkers,
275
createWorkflow: createWorkflow,
276
createWorkflowActivities: createWorkflowActivities,
277
createWorkflowConfig: createWorkflowConfig,
278
createWorkspace: createWorkspace,
279
deleteByFriendlyName: deleteByFriendlyName,
280
findByFriendlyName: findByFriendlyName,
281
initClient: initClient,
282
initWorkspace: initWorkspace,
283
setup: setup,
284
};
285
};

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

1
'use strict';
2
3
var twilio = require('twilio');
4
var find = require('lodash/find');
5
var map = require('lodash/map');
6
var difference = require('lodash/difference');
7
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
8
var HOST = process.env.HOST;
9
var EVENT_CALLBACK = `${HOST}/events`;
10
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
11
var API_KEY = process.env.TWILIO_API_KEY;
12
var API_SECRET = process.env.TWILIO_API_SECRET;
13
14
module.exports = function() {
15
function initClient(existingWorkspaceSid) {
16
if (!existingWorkspaceSid) {
17
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID }).taskrouter.v1.workspaces;
18
} else {
19
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID })
20
.taskrouter.v1.workspaces(existingWorkspaceSid);
21
}
22
}
23
24
function createWorker(opts) {
25
var ctx = this;
26
27
return this.client.activities.list({friendlyName: 'Idle'})
28
.then(function(idleActivity) {
29
return ctx.client.workers.create({
30
friendlyName: opts.name,
31
attributes: JSON.stringify({
32
'products': opts.products,
33
'contact_uri': opts.phoneNumber,
34
}),
35
activitySid: idleActivity.sid,
36
});
37
});
38
}
39
40
function createWorkflow() {
41
var ctx = this;
42
var config = this.createWorkflowConfig();
43
44
return ctx.client.workflows
45
.create({
46
friendlyName: 'Sales',
47
assignmentCallbackUrl: HOST + '/call/assignment',
48
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
49
taskReservationTimeout: 15,
50
configuration: config,
51
})
52
.then(function(workflow) {
53
return ctx.client.activities.list()
54
.then(function(activities) {
55
var idleActivity = find(activities, {friendlyName: 'Idle'});
56
var offlineActivity = find(activities, {friendlyName: 'Offline'});
57
58
return {
59
workflowSid: workflow.sid,
60
activities: {
61
idle: idleActivity.sid,
62
offline: offlineActivity.sid,
63
},
64
workspaceSid: ctx.client._solution.sid,
65
};
66
});
67
});
68
}
69
70
function createTaskQueues() {
71
var ctx = this;
72
return this.client.activities.list()
73
.then(function(activities) {
74
var busyActivity = find(activities, {friendlyName: 'Busy'});
75
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
76
77
return Promise.all([
78
ctx.client.taskQueues.create({
79
friendlyName: 'SMS',
80
targetWorkers: 'products HAS "ProgrammableSMS"',
81
assignmentActivitySid: busyActivity.sid,
82
reservationActivitySid: reservedActivity.sid,
83
}),
84
ctx.client.taskQueues.create({
85
friendlyName: 'Voice',
86
targetWorkers: 'products HAS "ProgrammableVoice"',
87
assignmentActivitySid: busyActivity.sid,
88
reservationActivitySid: reservedActivity.sid,
89
}),
90
ctx.client.taskQueues.create({
91
friendlyName: 'Default',
92
targetWorkers: '1==1',
93
assignmentActivitySid: busyActivity.sid,
94
reservationActivitySid: reservedActivity.sid,
95
}),
96
])
97
.then(function(queues) {
98
ctx.queues = queues;
99
});
100
});
101
}
102
103
function createWorkers() {
104
var ctx = this;
105
106
return Promise.all([
107
ctx.createWorker({
108
name: 'Bob',
109
phoneNumber: process.env.BOB_NUMBER,
110
products: ['ProgrammableSMS'],
111
}),
112
ctx.createWorker({
113
name: 'Alice',
114
phoneNumber: process.env.ALICE_NUMBER,
115
products: ['ProgrammableVoice'],
116
})
117
])
118
.then(function(workers) {
119
var bobWorker = workers[0];
120
var aliceWorker = workers[1];
121
var workerInfo = {};
122
123
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
124
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
125
126
return workerInfo;
127
});
128
}
129
130
function createWorkflowActivities() {
131
var ctx = this;
132
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
133
134
return ctx.client.activities.list()
135
.then(function(activities) {
136
var existingActivities = map(activities, 'friendlyName');
137
138
var missingActivities = difference(activityNames, existingActivities);
139
140
var newActivities = map(missingActivities, function(friendlyName) {
141
return ctx.client.activities
142
.create({
143
friendlyName: friendlyName,
144
available: 'true'
145
});
146
});
147
148
return Promise.all(newActivities);
149
})
150
.then(function() {
151
return ctx.client.activities.list();
152
});
153
}
154
155
function createWorkflowConfig() {
156
var queues = this.queues;
157
158
if (!queues) {
159
throw new Error('Queues must be initialized.');
160
}
161
162
var defaultTarget = {
163
queue: find(queues, {friendlyName: 'Default'}).sid,
164
timeout: 30,
165
priority: 1,
166
};
167
168
var smsTarget = {
169
queue: find(queues, {friendlyName: 'SMS'}).sid,
170
timeout: 30,
171
priority: 5,
172
};
173
174
var voiceTarget = {
175
queue: find(queues, {friendlyName: 'Voice'}).sid,
176
timeout: 30,
177
priority: 5,
178
};
179
180
var rules = [
181
{
182
expression: 'selected_product=="ProgrammableSMS"',
183
targets: [smsTarget, defaultTarget],
184
timeout: 30,
185
},
186
{
187
expression: 'selected_product=="ProgrammableVoice"',
188
targets: [voiceTarget, defaultTarget],
189
timeout: 30,
190
},
191
];
192
193
var config = {
194
task_routing: {
195
filters: rules,
196
default_filter: defaultTarget,
197
},
198
};
199
200
return JSON.stringify(config);
201
}
202
203
function setup() {
204
var ctx = this;
205
206
ctx.initClient();
207
208
return this.initWorkspace()
209
.then(createWorkflowActivities.bind(ctx))
210
.then(createTaskQueues.bind(ctx))
211
.then(createWorkflow.bind(ctx))
212
.then(function(workspaceInfo) {
213
return ctx.createWorkers()
214
.then(function(workerInfo) {
215
return [workerInfo, workspaceInfo];
216
});
217
});
218
}
219
220
function findByFriendlyName(friendlyName) {
221
var client = this.client;
222
223
return client.list()
224
.then(function (data) {
225
return find(data, {friendlyName: friendlyName});
226
});
227
}
228
229
function deleteByFriendlyName(friendlyName) {
230
var ctx = this;
231
232
return this.findByFriendlyName(friendlyName)
233
.then(function(workspace) {
234
if (workspace.remove) {
235
return workspace.remove();
236
}
237
});
238
}
239
240
function createWorkspace() {
241
return this.client.create({
242
friendlyName: WORKSPACE_NAME,
243
EVENT_CALLBACKUrl: EVENT_CALLBACK,
244
});
245
}
246
247
function initWorkspace() {
248
var ctx = this;
249
var client = this.client;
250
251
return ctx.findByFriendlyName(WORKSPACE_NAME)
252
.then(function(workspace) {
253
var newWorkspace;
254
255
if (workspace) {
256
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
257
.then(createWorkspace.bind(ctx));
258
} else {
259
newWorkspace = ctx.createWorkspace();
260
}
261
262
return newWorkspace;
263
})
264
.then(function(workspace) {
265
ctx.initClient(workspace.sid);
266
267
return workspace;
268
});
269
}
270
271
return {
272
createTaskQueues: createTaskQueues,
273
createWorker: createWorker,
274
createWorkers: createWorkers,
275
createWorkflow: createWorkflow,
276
createWorkflowActivities: createWorkflowActivities,
277
createWorkflowConfig: createWorkflowConfig,
278
createWorkspace: createWorkspace,
279
deleteByFriendlyName: deleteByFriendlyName,
280
findByFriendlyName: findByFriendlyName,
281
initClient: initClient,
282
initWorkspace: initWorkspace,
283
setup: setup,
284
};
285
};

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

1
'use strict';
2
3
var twilio = require('twilio');
4
var find = require('lodash/find');
5
var map = require('lodash/map');
6
var difference = require('lodash/difference');
7
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
8
var HOST = process.env.HOST;
9
var EVENT_CALLBACK = `${HOST}/events`;
10
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
11
var API_KEY = process.env.TWILIO_API_KEY;
12
var API_SECRET = process.env.TWILIO_API_SECRET;
13
14
module.exports = function() {
15
function initClient(existingWorkspaceSid) {
16
if (!existingWorkspaceSid) {
17
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID }).taskrouter.v1.workspaces;
18
} else {
19
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID })
20
.taskrouter.v1.workspaces(existingWorkspaceSid);
21
}
22
}
23
24
function createWorker(opts) {
25
var ctx = this;
26
27
return this.client.activities.list({friendlyName: 'Idle'})
28
.then(function(idleActivity) {
29
return ctx.client.workers.create({
30
friendlyName: opts.name,
31
attributes: JSON.stringify({
32
'products': opts.products,
33
'contact_uri': opts.phoneNumber,
34
}),
35
activitySid: idleActivity.sid,
36
});
37
});
38
}
39
40
function createWorkflow() {
41
var ctx = this;
42
var config = this.createWorkflowConfig();
43
44
return ctx.client.workflows
45
.create({
46
friendlyName: 'Sales',
47
assignmentCallbackUrl: HOST + '/call/assignment',
48
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
49
taskReservationTimeout: 15,
50
configuration: config,
51
})
52
.then(function(workflow) {
53
return ctx.client.activities.list()
54
.then(function(activities) {
55
var idleActivity = find(activities, {friendlyName: 'Idle'});
56
var offlineActivity = find(activities, {friendlyName: 'Offline'});
57
58
return {
59
workflowSid: workflow.sid,
60
activities: {
61
idle: idleActivity.sid,
62
offline: offlineActivity.sid,
63
},
64
workspaceSid: ctx.client._solution.sid,
65
};
66
});
67
});
68
}
69
70
function createTaskQueues() {
71
var ctx = this;
72
return this.client.activities.list()
73
.then(function(activities) {
74
var busyActivity = find(activities, {friendlyName: 'Busy'});
75
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
76
77
return Promise.all([
78
ctx.client.taskQueues.create({
79
friendlyName: 'SMS',
80
targetWorkers: 'products HAS "ProgrammableSMS"',
81
assignmentActivitySid: busyActivity.sid,
82
reservationActivitySid: reservedActivity.sid,
83
}),
84
ctx.client.taskQueues.create({
85
friendlyName: 'Voice',
86
targetWorkers: 'products HAS "ProgrammableVoice"',
87
assignmentActivitySid: busyActivity.sid,
88
reservationActivitySid: reservedActivity.sid,
89
}),
90
ctx.client.taskQueues.create({
91
friendlyName: 'Default',
92
targetWorkers: '1==1',
93
assignmentActivitySid: busyActivity.sid,
94
reservationActivitySid: reservedActivity.sid,
95
}),
96
])
97
.then(function(queues) {
98
ctx.queues = queues;
99
});
100
});
101
}
102
103
function createWorkers() {
104
var ctx = this;
105
106
return Promise.all([
107
ctx.createWorker({
108
name: 'Bob',
109
phoneNumber: process.env.BOB_NUMBER,
110
products: ['ProgrammableSMS'],
111
}),
112
ctx.createWorker({
113
name: 'Alice',
114
phoneNumber: process.env.ALICE_NUMBER,
115
products: ['ProgrammableVoice'],
116
})
117
])
118
.then(function(workers) {
119
var bobWorker = workers[0];
120
var aliceWorker = workers[1];
121
var workerInfo = {};
122
123
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
124
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
125
126
return workerInfo;
127
});
128
}
129
130
function createWorkflowActivities() {
131
var ctx = this;
132
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
133
134
return ctx.client.activities.list()
135
.then(function(activities) {
136
var existingActivities = map(activities, 'friendlyName');
137
138
var missingActivities = difference(activityNames, existingActivities);
139
140
var newActivities = map(missingActivities, function(friendlyName) {
141
return ctx.client.activities
142
.create({
143
friendlyName: friendlyName,
144
available: 'true'
145
});
146
});
147
148
return Promise.all(newActivities);
149
})
150
.then(function() {
151
return ctx.client.activities.list();
152
});
153
}
154
155
function createWorkflowConfig() {
156
var queues = this.queues;
157
158
if (!queues) {
159
throw new Error('Queues must be initialized.');
160
}
161
162
var defaultTarget = {
163
queue: find(queues, {friendlyName: 'Default'}).sid,
164
timeout: 30,
165
priority: 1,
166
};
167
168
var smsTarget = {
169
queue: find(queues, {friendlyName: 'SMS'}).sid,
170
timeout: 30,
171
priority: 5,
172
};
173
174
var voiceTarget = {
175
queue: find(queues, {friendlyName: 'Voice'}).sid,
176
timeout: 30,
177
priority: 5,
178
};
179
180
var rules = [
181
{
182
expression: 'selected_product=="ProgrammableSMS"',
183
targets: [smsTarget, defaultTarget],
184
timeout: 30,
185
},
186
{
187
expression: 'selected_product=="ProgrammableVoice"',
188
targets: [voiceTarget, defaultTarget],
189
timeout: 30,
190
},
191
];
192
193
var config = {
194
task_routing: {
195
filters: rules,
196
default_filter: defaultTarget,
197
},
198
};
199
200
return JSON.stringify(config);
201
}
202
203
function setup() {
204
var ctx = this;
205
206
ctx.initClient();
207
208
return this.initWorkspace()
209
.then(createWorkflowActivities.bind(ctx))
210
.then(createTaskQueues.bind(ctx))
211
.then(createWorkflow.bind(ctx))
212
.then(function(workspaceInfo) {
213
return ctx.createWorkers()
214
.then(function(workerInfo) {
215
return [workerInfo, workspaceInfo];
216
});
217
});
218
}
219
220
function findByFriendlyName(friendlyName) {
221
var client = this.client;
222
223
return client.list()
224
.then(function (data) {
225
return find(data, {friendlyName: friendlyName});
226
});
227
}
228
229
function deleteByFriendlyName(friendlyName) {
230
var ctx = this;
231
232
return this.findByFriendlyName(friendlyName)
233
.then(function(workspace) {
234
if (workspace.remove) {
235
return workspace.remove();
236
}
237
});
238
}
239
240
function createWorkspace() {
241
return this.client.create({
242
friendlyName: WORKSPACE_NAME,
243
EVENT_CALLBACKUrl: EVENT_CALLBACK,
244
});
245
}
246
247
function initWorkspace() {
248
var ctx = this;
249
var client = this.client;
250
251
return ctx.findByFriendlyName(WORKSPACE_NAME)
252
.then(function(workspace) {
253
var newWorkspace;
254
255
if (workspace) {
256
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
257
.then(createWorkspace.bind(ctx));
258
} else {
259
newWorkspace = ctx.createWorkspace();
260
}
261
262
return newWorkspace;
263
})
264
.then(function(workspace) {
265
ctx.initClient(workspace.sid);
266
267
return workspace;
268
});
269
}
270
271
return {
272
createTaskQueues: createTaskQueues,
273
createWorker: createWorker,
274
createWorkers: createWorkers,
275
createWorkflow: createWorkflow,
276
createWorkflowActivities: createWorkflowActivities,
277
createWorkflowConfig: createWorkflowConfig,
278
createWorkspace: createWorkspace,
279
deleteByFriendlyName: deleteByFriendlyName,
280
findByFriendlyName: findByFriendlyName,
281
initClient: initClient,
282
initWorkspace: initWorkspace,
283
setup: setup,
284
};
285
};

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


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

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

lib/workspace.js

1
'use strict';
2
3
var twilio = require('twilio');
4
var find = require('lodash/find');
5
var map = require('lodash/map');
6
var difference = require('lodash/difference');
7
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
8
var HOST = process.env.HOST;
9
var EVENT_CALLBACK = `${HOST}/events`;
10
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
11
var API_KEY = process.env.TWILIO_API_KEY;
12
var API_SECRET = process.env.TWILIO_API_SECRET;
13
14
module.exports = function() {
15
function initClient(existingWorkspaceSid) {
16
if (!existingWorkspaceSid) {
17
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID }).taskrouter.v1.workspaces;
18
} else {
19
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID })
20
.taskrouter.v1.workspaces(existingWorkspaceSid);
21
}
22
}
23
24
function createWorker(opts) {
25
var ctx = this;
26
27
return this.client.activities.list({friendlyName: 'Idle'})
28
.then(function(idleActivity) {
29
return ctx.client.workers.create({
30
friendlyName: opts.name,
31
attributes: JSON.stringify({
32
'products': opts.products,
33
'contact_uri': opts.phoneNumber,
34
}),
35
activitySid: idleActivity.sid,
36
});
37
});
38
}
39
40
function createWorkflow() {
41
var ctx = this;
42
var config = this.createWorkflowConfig();
43
44
return ctx.client.workflows
45
.create({
46
friendlyName: 'Sales',
47
assignmentCallbackUrl: HOST + '/call/assignment',
48
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
49
taskReservationTimeout: 15,
50
configuration: config,
51
})
52
.then(function(workflow) {
53
return ctx.client.activities.list()
54
.then(function(activities) {
55
var idleActivity = find(activities, {friendlyName: 'Idle'});
56
var offlineActivity = find(activities, {friendlyName: 'Offline'});
57
58
return {
59
workflowSid: workflow.sid,
60
activities: {
61
idle: idleActivity.sid,
62
offline: offlineActivity.sid,
63
},
64
workspaceSid: ctx.client._solution.sid,
65
};
66
});
67
});
68
}
69
70
function createTaskQueues() {
71
var ctx = this;
72
return this.client.activities.list()
73
.then(function(activities) {
74
var busyActivity = find(activities, {friendlyName: 'Busy'});
75
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
76
77
return Promise.all([
78
ctx.client.taskQueues.create({
79
friendlyName: 'SMS',
80
targetWorkers: 'products HAS "ProgrammableSMS"',
81
assignmentActivitySid: busyActivity.sid,
82
reservationActivitySid: reservedActivity.sid,
83
}),
84
ctx.client.taskQueues.create({
85
friendlyName: 'Voice',
86
targetWorkers: 'products HAS "ProgrammableVoice"',
87
assignmentActivitySid: busyActivity.sid,
88
reservationActivitySid: reservedActivity.sid,
89
}),
90
ctx.client.taskQueues.create({
91
friendlyName: 'Default',
92
targetWorkers: '1==1',
93
assignmentActivitySid: busyActivity.sid,
94
reservationActivitySid: reservedActivity.sid,
95
}),
96
])
97
.then(function(queues) {
98
ctx.queues = queues;
99
});
100
});
101
}
102
103
function createWorkers() {
104
var ctx = this;
105
106
return Promise.all([
107
ctx.createWorker({
108
name: 'Bob',
109
phoneNumber: process.env.BOB_NUMBER,
110
products: ['ProgrammableSMS'],
111
}),
112
ctx.createWorker({
113
name: 'Alice',
114
phoneNumber: process.env.ALICE_NUMBER,
115
products: ['ProgrammableVoice'],
116
})
117
])
118
.then(function(workers) {
119
var bobWorker = workers[0];
120
var aliceWorker = workers[1];
121
var workerInfo = {};
122
123
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
124
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
125
126
return workerInfo;
127
});
128
}
129
130
function createWorkflowActivities() {
131
var ctx = this;
132
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
133
134
return ctx.client.activities.list()
135
.then(function(activities) {
136
var existingActivities = map(activities, 'friendlyName');
137
138
var missingActivities = difference(activityNames, existingActivities);
139
140
var newActivities = map(missingActivities, function(friendlyName) {
141
return ctx.client.activities
142
.create({
143
friendlyName: friendlyName,
144
available: 'true'
145
});
146
});
147
148
return Promise.all(newActivities);
149
})
150
.then(function() {
151
return ctx.client.activities.list();
152
});
153
}
154
155
function createWorkflowConfig() {
156
var queues = this.queues;
157
158
if (!queues) {
159
throw new Error('Queues must be initialized.');
160
}
161
162
var defaultTarget = {
163
queue: find(queues, {friendlyName: 'Default'}).sid,
164
timeout: 30,
165
priority: 1,
166
};
167
168
var smsTarget = {
169
queue: find(queues, {friendlyName: 'SMS'}).sid,
170
timeout: 30,
171
priority: 5,
172
};
173
174
var voiceTarget = {
175
queue: find(queues, {friendlyName: 'Voice'}).sid,
176
timeout: 30,
177
priority: 5,
178
};
179
180
var rules = [
181
{
182
expression: 'selected_product=="ProgrammableSMS"',
183
targets: [smsTarget, defaultTarget],
184
timeout: 30,
185
},
186
{
187
expression: 'selected_product=="ProgrammableVoice"',
188
targets: [voiceTarget, defaultTarget],
189
timeout: 30,
190
},
191
];
192
193
var config = {
194
task_routing: {
195
filters: rules,
196
default_filter: defaultTarget,
197
},
198
};
199
200
return JSON.stringify(config);
201
}
202
203
function setup() {
204
var ctx = this;
205
206
ctx.initClient();
207
208
return this.initWorkspace()
209
.then(createWorkflowActivities.bind(ctx))
210
.then(createTaskQueues.bind(ctx))
211
.then(createWorkflow.bind(ctx))
212
.then(function(workspaceInfo) {
213
return ctx.createWorkers()
214
.then(function(workerInfo) {
215
return [workerInfo, workspaceInfo];
216
});
217
});
218
}
219
220
function findByFriendlyName(friendlyName) {
221
var client = this.client;
222
223
return client.list()
224
.then(function (data) {
225
return find(data, {friendlyName: friendlyName});
226
});
227
}
228
229
function deleteByFriendlyName(friendlyName) {
230
var ctx = this;
231
232
return this.findByFriendlyName(friendlyName)
233
.then(function(workspace) {
234
if (workspace.remove) {
235
return workspace.remove();
236
}
237
});
238
}
239
240
function createWorkspace() {
241
return this.client.create({
242
friendlyName: WORKSPACE_NAME,
243
EVENT_CALLBACKUrl: EVENT_CALLBACK,
244
});
245
}
246
247
function initWorkspace() {
248
var ctx = this;
249
var client = this.client;
250
251
return ctx.findByFriendlyName(WORKSPACE_NAME)
252
.then(function(workspace) {
253
var newWorkspace;
254
255
if (workspace) {
256
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
257
.then(createWorkspace.bind(ctx));
258
} else {
259
newWorkspace = ctx.createWorkspace();
260
}
261
262
return newWorkspace;
263
})
264
.then(function(workspace) {
265
ctx.initClient(workspace.sid);
266
267
return workspace;
268
});
269
}
270
271
return {
272
createTaskQueues: createTaskQueues,
273
createWorker: createWorker,
274
createWorkers: createWorkers,
275
createWorkflow: createWorkflow,
276
createWorkflowActivities: createWorkflowActivities,
277
createWorkflowConfig: createWorkflowConfig,
278
createWorkspace: createWorkspace,
279
deleteByFriendlyName: deleteByFriendlyName,
280
findByFriendlyName: findByFriendlyName,
281
initClient: initClient,
282
initWorkspace: initWorkspace,
283
setup: setup,
284
};
285
};

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


Finally, we create the Workflow using the following parameters:

  1. friendlyName as the name of a Workflow.

  2. assignmentCallbackUrl and fallbackAssignmentCallbackUrl as the public URL where a request will be made when this Workflow assigns a Task to a Worker. We will learn how to implement it on the next steps.

  3. taskReservationTimeout as the maximum time we want to wait until a Worker is available for handling a Task.

  4. configuration which is a set of rules for placing Tasks into Task Queues. The routing configuration will take a Task's attribute and match this with Task Queues. This application's Workflow rules are defined as:

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

lib/workspace.js

1
'use strict';
2
3
var twilio = require('twilio');
4
var find = require('lodash/find');
5
var map = require('lodash/map');
6
var difference = require('lodash/difference');
7
var WORKSPACE_NAME = 'TaskRouter Node Workspace';
8
var HOST = process.env.HOST;
9
var EVENT_CALLBACK = `${HOST}/events`;
10
var ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID;
11
var API_KEY = process.env.TWILIO_API_KEY;
12
var API_SECRET = process.env.TWILIO_API_SECRET;
13
14
module.exports = function() {
15
function initClient(existingWorkspaceSid) {
16
if (!existingWorkspaceSid) {
17
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID }).taskrouter.v1.workspaces;
18
} else {
19
this.client = twilio(API_KEY, API_SECRET, { accountSid: ACCOUNT_SID })
20
.taskrouter.v1.workspaces(existingWorkspaceSid);
21
}
22
}
23
24
function createWorker(opts) {
25
var ctx = this;
26
27
return this.client.activities.list({friendlyName: 'Idle'})
28
.then(function(idleActivity) {
29
return ctx.client.workers.create({
30
friendlyName: opts.name,
31
attributes: JSON.stringify({
32
'products': opts.products,
33
'contact_uri': opts.phoneNumber,
34
}),
35
activitySid: idleActivity.sid,
36
});
37
});
38
}
39
40
function createWorkflow() {
41
var ctx = this;
42
var config = this.createWorkflowConfig();
43
44
return ctx.client.workflows
45
.create({
46
friendlyName: 'Sales',
47
assignmentCallbackUrl: HOST + '/call/assignment',
48
fallbackAssignmentCallbackUrl: HOST + '/call/assignment',
49
taskReservationTimeout: 15,
50
configuration: config,
51
})
52
.then(function(workflow) {
53
return ctx.client.activities.list()
54
.then(function(activities) {
55
var idleActivity = find(activities, {friendlyName: 'Idle'});
56
var offlineActivity = find(activities, {friendlyName: 'Offline'});
57
58
return {
59
workflowSid: workflow.sid,
60
activities: {
61
idle: idleActivity.sid,
62
offline: offlineActivity.sid,
63
},
64
workspaceSid: ctx.client._solution.sid,
65
};
66
});
67
});
68
}
69
70
function createTaskQueues() {
71
var ctx = this;
72
return this.client.activities.list()
73
.then(function(activities) {
74
var busyActivity = find(activities, {friendlyName: 'Busy'});
75
var reservedActivity = find(activities, {friendlyName: 'Reserved'});
76
77
return Promise.all([
78
ctx.client.taskQueues.create({
79
friendlyName: 'SMS',
80
targetWorkers: 'products HAS "ProgrammableSMS"',
81
assignmentActivitySid: busyActivity.sid,
82
reservationActivitySid: reservedActivity.sid,
83
}),
84
ctx.client.taskQueues.create({
85
friendlyName: 'Voice',
86
targetWorkers: 'products HAS "ProgrammableVoice"',
87
assignmentActivitySid: busyActivity.sid,
88
reservationActivitySid: reservedActivity.sid,
89
}),
90
ctx.client.taskQueues.create({
91
friendlyName: 'Default',
92
targetWorkers: '1==1',
93
assignmentActivitySid: busyActivity.sid,
94
reservationActivitySid: reservedActivity.sid,
95
}),
96
])
97
.then(function(queues) {
98
ctx.queues = queues;
99
});
100
});
101
}
102
103
function createWorkers() {
104
var ctx = this;
105
106
return Promise.all([
107
ctx.createWorker({
108
name: 'Bob',
109
phoneNumber: process.env.BOB_NUMBER,
110
products: ['ProgrammableSMS'],
111
}),
112
ctx.createWorker({
113
name: 'Alice',
114
phoneNumber: process.env.ALICE_NUMBER,
115
products: ['ProgrammableVoice'],
116
})
117
])
118
.then(function(workers) {
119
var bobWorker = workers[0];
120
var aliceWorker = workers[1];
121
var workerInfo = {};
122
123
workerInfo[process.env.ALICE_NUMBER] = aliceWorker.sid;
124
workerInfo[process.env.BOB_NUMBER] = bobWorker.sid;
125
126
return workerInfo;
127
});
128
}
129
130
function createWorkflowActivities() {
131
var ctx = this;
132
var activityNames = ['Idle', 'Busy', 'Offline', 'Reserved'];
133
134
return ctx.client.activities.list()
135
.then(function(activities) {
136
var existingActivities = map(activities, 'friendlyName');
137
138
var missingActivities = difference(activityNames, existingActivities);
139
140
var newActivities = map(missingActivities, function(friendlyName) {
141
return ctx.client.activities
142
.create({
143
friendlyName: friendlyName,
144
available: 'true'
145
});
146
});
147
148
return Promise.all(newActivities);
149
})
150
.then(function() {
151
return ctx.client.activities.list();
152
});
153
}
154
155
function createWorkflowConfig() {
156
var queues = this.queues;
157
158
if (!queues) {
159
throw new Error('Queues must be initialized.');
160
}
161
162
var defaultTarget = {
163
queue: find(queues, {friendlyName: 'Default'}).sid,
164
timeout: 30,
165
priority: 1,
166
};
167
168
var smsTarget = {
169
queue: find(queues, {friendlyName: 'SMS'}).sid,
170
timeout: 30,
171
priority: 5,
172
};
173
174
var voiceTarget = {
175
queue: find(queues, {friendlyName: 'Voice'}).sid,
176
timeout: 30,
177
priority: 5,
178
};
179
180
var rules = [
181
{
182
expression: 'selected_product=="ProgrammableSMS"',
183
targets: [smsTarget, defaultTarget],
184
timeout: 30,
185
},
186
{
187
expression: 'selected_product=="ProgrammableVoice"',
188
targets: [voiceTarget, defaultTarget],
189
timeout: 30,
190
},
191
];
192
193
var config = {
194
task_routing: {
195
filters: rules,
196
default_filter: defaultTarget,
197
},
198
};
199
200
return JSON.stringify(config);
201
}
202
203
function setup() {
204
var ctx = this;
205
206
ctx.initClient();
207
208
return this.initWorkspace()
209
.then(createWorkflowActivities.bind(ctx))
210
.then(createTaskQueues.bind(ctx))
211
.then(createWorkflow.bind(ctx))
212
.then(function(workspaceInfo) {
213
return ctx.createWorkers()
214
.then(function(workerInfo) {
215
return [workerInfo, workspaceInfo];
216
});
217
});
218
}
219
220
function findByFriendlyName(friendlyName) {
221
var client = this.client;
222
223
return client.list()
224
.then(function (data) {
225
return find(data, {friendlyName: friendlyName});
226
});
227
}
228
229
function deleteByFriendlyName(friendlyName) {
230
var ctx = this;
231
232
return this.findByFriendlyName(friendlyName)
233
.then(function(workspace) {
234
if (workspace.remove) {
235
return workspace.remove();
236
}
237
});
238
}
239
240
function createWorkspace() {
241
return this.client.create({
242
friendlyName: WORKSPACE_NAME,
243
EVENT_CALLBACKUrl: EVENT_CALLBACK,
244
});
245
}
246
247
function initWorkspace() {
248
var ctx = this;
249
var client = this.client;
250
251
return ctx.findByFriendlyName(WORKSPACE_NAME)
252
.then(function(workspace) {
253
var newWorkspace;
254
255
if (workspace) {
256
newWorkspace = ctx.deleteByFriendlyName(WORKSPACE_NAME)
257
.then(createWorkspace.bind(ctx));
258
} else {
259
newWorkspace = ctx.createWorkspace();
260
}
261
262
return newWorkspace;
263
})
264
.then(function(workspace) {
265
ctx.initClient(workspace.sid);
266
267
return workspace;
268
});
269
}
270
271
return {
272
createTaskQueues: createTaskQueues,
273
createWorker: createWorker,
274
createWorkers: createWorkers,
275
createWorkflow: createWorkflow,
276
createWorkflowActivities: createWorkflowActivities,
277
createWorkflowConfig: createWorkflowConfig,
278
createWorkspace: createWorkspace,
279
deleteByFriendlyName: deleteByFriendlyName,
280
findByFriendlyName: findByFriendlyName,
281
initClient: initClient,
282
initWorkspace: initWorkspace,
283
setup: setup,
284
};
285
};

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


Handle Twilio's Request

handle-twilios-request page anchor

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

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

Handling Twilio's Requests

handling-twilios-requests page anchor

routes/call.js

1
'use strict';
2
3
var express = require('express'),
4
router = express.Router(),
5
VoiceResponse = require('twilio/lib/twiml/VoiceResponse');
6
7
module.exports = function (app) {
8
// POST /call/incoming
9
router.post('/incoming/', function (req, res) {
10
var twimlResponse = new VoiceResponse();
11
var gather = twimlResponse.gather({
12
numDigits: 1,
13
action: '/call/enqueue',
14
method: 'POST'
15
});
16
gather.say('For Programmable SMS, press one. For Voice, press any other key.');
17
res.type('text/xml');
18
res.send(twimlResponse.toString());
19
});
20
21
// POST /call/enqueue
22
router.post('/enqueue/', function (req, res) {
23
var pressedKey = req.body.Digits;
24
var twimlResponse = new VoiceResponse();
25
var selectedProduct = (pressedKey === '1') ? 'ProgrammableSMS' : 'ProgrammableVoice';
26
var enqueue = twimlResponse.enqueueTask(
27
{workflowSid: app.get('workspaceInfo').workflowSid}
28
);
29
enqueue.task({}, JSON.stringify({selected_product: selectedProduct}));
30
31
res.type('text/xml');
32
res.send(twimlResponse.toString());
33
});
34
35
// POST /call/assignment
36
router.post('/assignment/', function (req, res) {
37
res.type('application/json');
38
res.send({
39
instruction: "dequeue",
40
post_work_activity_sid: app.get('workspaceInfo').activities.idle
41
});
42
});
43
44
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

1
'use strict';
2
3
var express = require('express'),
4
router = express.Router(),
5
VoiceResponse = require('twilio/lib/twiml/VoiceResponse');
6
7
module.exports = function (app) {
8
// POST /call/incoming
9
router.post('/incoming/', function (req, res) {
10
var twimlResponse = new VoiceResponse();
11
var gather = twimlResponse.gather({
12
numDigits: 1,
13
action: '/call/enqueue',
14
method: 'POST'
15
});
16
gather.say('For Programmable SMS, press one. For Voice, press any other key.');
17
res.type('text/xml');
18
res.send(twimlResponse.toString());
19
});
20
21
// POST /call/enqueue
22
router.post('/enqueue/', function (req, res) {
23
var pressedKey = req.body.Digits;
24
var twimlResponse = new VoiceResponse();
25
var selectedProduct = (pressedKey === '1') ? 'ProgrammableSMS' : 'ProgrammableVoice';
26
var enqueue = twimlResponse.enqueueTask(
27
{workflowSid: app.get('workspaceInfo').workflowSid}
28
);
29
enqueue.task({}, JSON.stringify({selected_product: selectedProduct}));
30
31
res.type('text/xml');
32
res.send(twimlResponse.toString());
33
});
34
35
// POST /call/assignment
36
router.post('/assignment/', function (req, res) {
37
res.type('application/json');
38
res.send({
39
instruction: "dequeue",
40
post_work_activity_sid: app.get('workspaceInfo').activities.idle
41
});
42
});
43
44
return router;
45
};

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


When TaskRouter selects a Worker, it does the following:

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

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

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

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

routes/call.js

1
'use strict';
2
3
var express = require('express'),
4
router = express.Router(),
5
VoiceResponse = require('twilio/lib/twiml/VoiceResponse');
6
7
module.exports = function (app) {
8
// POST /call/incoming
9
router.post('/incoming/', function (req, res) {
10
var twimlResponse = new VoiceResponse();
11
var gather = twimlResponse.gather({
12
numDigits: 1,
13
action: '/call/enqueue',
14
method: 'POST'
15
});
16
gather.say('For Programmable SMS, press one. For Voice, press any other key.');
17
res.type('text/xml');
18
res.send(twimlResponse.toString());
19
});
20
21
// POST /call/enqueue
22
router.post('/enqueue/', function (req, res) {
23
var pressedKey = req.body.Digits;
24
var twimlResponse = new VoiceResponse();
25
var selectedProduct = (pressedKey === '1') ? 'ProgrammableSMS' : 'ProgrammableVoice';
26
var enqueue = twimlResponse.enqueueTask(
27
{workflowSid: app.get('workspaceInfo').workflowSid}
28
);
29
enqueue.task({}, JSON.stringify({selected_product: selectedProduct}));
30
31
res.type('text/xml');
32
res.send(twimlResponse.toString());
33
});
34
35
// POST /call/assignment
36
router.post('/assignment/', function (req, res) {
37
res.type('application/json');
38
res.send({
39
instruction: "dequeue",
40
post_work_activity_sid: app.get('workspaceInfo').activities.idle
41
});
42
});
43
44
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

1
'use strict';
2
3
var express = require('express'),
4
MissedCall = require('../models/missed-call'),
5
util = require('util'),
6
querystring = require('querystring'),
7
router = express.Router(),
8
Q = require('q');
9
10
// POST /events
11
router.post('/', function (req, res) {
12
var eventType = req.body.EventType;
13
var taskAttributes = (req.body.TaskAttributes)? JSON.parse(req.body.TaskAttributes) : {};
14
15
function saveMissedCall(){
16
return MissedCall.create({
17
selectedProduct: taskAttributes.selected_product,
18
phoneNumber: taskAttributes.from
19
});
20
}
21
22
var eventHandler = {
23
'task.canceled': saveMissedCall,
24
'workflow.timeout': function() {
25
return saveMissedCall().then(voicemail(taskAttributes.call_sid));
26
},
27
'worker.activity.update': function(){
28
var workerAttributes = JSON.parse(req.body.WorkerAttributes);
29
if (req.body.WorkerActivityName === 'Offline') {
30
notifyOfflineStatus(workerAttributes.contact_uri);
31
}
32
return Q.resolve({});
33
},
34
'default': function() { return Q.resolve({}); }
35
};
36
37
(eventHandler[eventType] || eventHandler['default'])().then(function () {
38
res.json({});
39
});
40
});
41
42
function voicemail (callSid){
43
var client = buildClient(),
44
query = querystring.stringify({
45
Message: 'Sorry, All agents are busy. Please leave a message. We\'ll call you as soon as possible',
46
Email: process.env.MISSED_CALLS_EMAIL_ADDRESS}),
47
voicemailUrl = util.format("https://twimlets.com/voicemail?%s", query);
48
49
client.calls(callSid).update({
50
method: 'POST',
51
url: voicemailUrl
52
});
53
}
54
55
function notifyOfflineStatus(phone_number) {
56
var client = buildClient(),
57
message = 'Your status has changed to Offline. Reply with "On" to get back Online';
58
client.sendMessage({
59
to: phone_number,
60
from: process.env.TWILIO_NUMBER,
61
body: message
62
});
63
}
64
65
function buildClient() {
66
var accountSid = process.env.TWILIO_ACCOUNT_SID,
67
apiKey = process.env.TWILIO_API_KEY,
68
apiSecret = process.env.TWILIO_API_SECRET;
69
return require('twilio')(apiKey, apiSecret, { accountSid: accountSid });
70
}
71
72
module.exports = router;

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


Change a Worker's Activity

change-a-workers-activity page anchor

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

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

Handle Message to update the Worker Status

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

routes/sms.js

1
'use strict';
2
3
var express = require('express'),
4
router = express.Router(),
5
twimlGenerator = require('../lib/twiml-generator');
6
7
module.exports = function (app) {
8
// POST /sms/incoming
9
router.post('/incoming/', function (req, res) {
10
var targetActivity = (req.body.Body.toLowerCase() === "on")? "idle":"offline";
11
var activitySid = app.get('workspaceInfo').activities[targetActivity];
12
changeWorkerActivitySid(req.body.From, activitySid);
13
res.type('text/xml');
14
res.send(twimlGenerator.generateConfirmMessage(targetActivity));
15
});
16
17
function changeWorkerActivitySid(workerNumber, activitySid){
18
var accountSid = process.env.TWILIO_ACCOUNT_SID,
19
apiKey = process.env.TWILIO_API_KEY,
20
apiSecret = process.env.TWILIO_API_SECRET,
21
workspaceSid = app.get('workspaceInfo').workspaceSid,
22
workerSid = app.get('workerInfo')[workerNumber],
23
client = require('twilio')(apiKey, apiSecret, { accountSid: accountSid });
24
client.taskrouter.v1
25
.workspaces(workspaceSid)
26
.workers(workerSid)
27
.update({activitySid: activitySid});
28
}
29
return router;
30
};

You've finished this tutorial and built a dynamic call center that routes calls to specialist agents with TaskRouter.


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

Warm-Transfer

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

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

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