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

Dynamic Call Center with C# and ASP.NET MVC


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


This is what the application does at a high level

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

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

In this ASP.NET MVC application this step will be executed in the Application_Start event every time you run 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

We'll use a TaskRouterClient provided in the twilio-csharp(link takes you to an external page) helper library to create and configure the workspace.

Create, Setup and Configure the Workspace

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

TaskRouter.Web/App_Start/WorkspaceConfig.cs


_233
using System;
_233
using System.Collections.Generic;
_233
using System.Linq;
_233
using System.Web.Helpers;
_233
using TaskRouter.Web.Infrastructure;
_233
using Twilio;
_233
using Twilio.Rest.Taskrouter.V1;
_233
using Twilio.Rest.Taskrouter.V1.Workspace;
_233
_233
namespace TaskRouter.Web
_233
{
_233
public class WorkspaceConfig
_233
{
_233
private readonly Config _config;
_233
_233
private const string VoiceQueue = "VoiceQueue";
_233
private const string SmsQueue = "SMSQueue";
_233
private const string AllQueue = "AllQueue";
_233
_233
public static void RegisterWorkspace()
_233
{
_233
new WorkspaceConfig().Register();
_233
}
_233
_233
public WorkspaceConfig():this(new Config())
_233
{
_233
}
_233
_233
public WorkspaceConfig(Config config)
_233
{
_233
TwilioClient.Init(config.AccountSID, config.AuthToken);
_233
_config = config;
_233
_233
}
_233
_233
public WorkspaceConfig(Type workspaceResource):this()
_233
{
_233
}
_233
_233
public virtual ActivityResource GetActivityByFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Read(workspaceSid, friendlyName).First();
_233
}
_233
_233
public virtual ActivityResource CreateActivityWithFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Create(workspaceSid, friendlyName);
_233
}
_233
_233
public virtual WorkspaceResource GetWorkspaceByFriendlyName(string friendlyName)
_233
{
_233
return WorkspaceResource.Read(friendlyName).FirstOrDefault();
_233
}
_233
_233
public virtual WorkspaceResource CreateWorkspace(string friendlyName, Uri eventCallbackUrl)
_233
{
_233
return WorkspaceResource.Create(friendlyName, eventCallbackUrl);
_233
}
_233
_233
public virtual bool DeleteWorkspace(string workspaceSid)
_233
{
_233
return WorkspaceResource.Delete(workspaceSid);
_233
}
_233
_233
public virtual WorkerResource CreateWorker(string workspaceSid, string bob, string activitySid, string attributes)
_233
{
_233
return WorkerResource.Create(workspaceSid, bob, activitySid, attributes);
_233
}
_233
_233
public void Register()
_233
{
_233
var workspace = DeleteAndCreateWorkspace(
_233
"Twilio Workspace", new Uri(new Uri(_config.HostUrl), "/callback/events").AbsoluteUri);
_233
var workspaceSid = workspace.Sid;
_233
_233
var assignmentActivity = GetActivityByFriendlyName(workspaceSid, "Unavailable");
_233
var idleActivity = GetActivityByFriendlyName(workspaceSid, "Available");
_233
var reservationActivity = CreateActivityWithFriendlyName(workspaceSid, "Reserved");
_233
var offlineActivity = GetActivityByFriendlyName(workspaceSid, "Offline");
_233
_233
var workers = CreateWorkers(workspaceSid, idleActivity);
_233
var taskQueues = CreateTaskQueues(workspaceSid, assignmentActivity, reservationActivity);
_233
var workflow = CreateWorkflow(workspaceSid, taskQueues);
_233
_233
Singleton.Instance.WorkspaceSid = workspaceSid;
_233
Singleton.Instance.WorkflowSid = workflow.Sid;
_233
Singleton.Instance.Workers = workers;
_233
Singleton.Instance.PostWorkActivitySid = idleActivity.Sid;
_233
Singleton.Instance.IdleActivitySid = idleActivity.Sid;
_233
Singleton.Instance.OfflineActivitySid = offlineActivity.Sid;
_233
}
_233
_233
public virtual WorkspaceResource DeleteAndCreateWorkspace(string friendlyName, string eventCallbackUrl) {
_233
var workspace = GetWorkspaceByFriendlyName(friendlyName);
_233
if (workspace != null)
_233
{
_233
DeleteWorkspace(workspace.Sid);
_233
}
_233
_233
return CreateWorkspace(friendlyName, new Uri(eventCallbackUrl));
_233
}
_233
_233
private IDictionary<string, string> CreateWorkers(string workspaceSid, ActivityResource activity)
_233
{
_233
var attributesForBob = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableSMS"
_233
},
_233
contact_uri = _config.AgentForProgrammableSMS
_233
};
_233
_233
var bobWorker = CreateWorker(workspaceSid, "Bob", activity.Sid, Json.Encode(attributesForBob));
_233
_233
var attributesForAlice = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableVoice"
_233
},
_233
contact_uri = _config.AgentForProgrammableVoice
_233
};
_233
_233
var alice = CreateWorker(workspaceSid, "Alice", activity.Sid, Json.Encode(attributesForAlice));
_233
_233
return new Dictionary<string, string>
_233
{
_233
{ _config.AgentForProgrammableSMS, bobWorker.Sid },
_233
{ _config.AgentForProgrammableVoice, alice.Sid },
_233
};
_233
}
_233
_233
public virtual TaskQueueResource CreateTaskQueue(
_233
string workspaceSid, string friendlyName,
_233
string assignmentActivitySid, string reservationActivitySid, string targetWorkers)
_233
{
_233
var queue = TaskQueueResource.Create(
_233
workspaceSid,
_233
friendlyName: friendlyName,
_233
assignmentActivitySid: assignmentActivitySid,
_233
reservationActivitySid: reservationActivitySid
_233
);
_233
_233
TaskQueueResource.Update(
_233
workspaceSid,
_233
queue.Sid,
_233
friendlyName,
_233
targetWorkers,
_233
assignmentActivitySid,
_233
reservationActivitySid,
_233
1);
_233
_233
return queue;
_233
}
_233
_233
private IDictionary<string, TaskQueueResource> CreateTaskQueues(
_233
string workspaceSid, ActivityResource assignmentActivity, ActivityResource reservationActivity)
_233
{
_233
_233
var voiceQueue = CreateTaskQueue(
_233
workspaceSid, "Voice",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableVoice'");
_233
_233
var smsQueue = CreateTaskQueue(
_233
workspaceSid, "SMS",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableSMS'");
_233
_233
var allQueue = CreateTaskQueue(
_233
workspaceSid, "All",
_233
assignmentActivity.Sid, reservationActivity.Sid, "1 == 1");
_233
_233
return new Dictionary<string, TaskQueueResource> {
_233
{ VoiceQueue, voiceQueue },
_233
{ SmsQueue, smsQueue },
_233
{ AllQueue, allQueue }
_233
};
_233
}
_233
_233
public virtual WorkflowResource CreateWorkflow(string workspaceSid, IDictionary<string, TaskQueueResource> taskQueues)
_233
{
_233
var voiceQueue = taskQueues[VoiceQueue];
_233
var smsQueue = taskQueues[SmsQueue];
_233
var allQueue = taskQueues[AllQueue];
_233
_233
var voiceFilter = new {
_233
friendlyName = "Voice",
_233
expression = "selected_product==\"ProgrammableVoice\"",
_233
targets = new List<object>() {
_233
new { queue = voiceQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var smsFilter = new {
_233
friendlyName = "SMS",
_233
expression = "selected_product==\"ProgrammableSMS\"",
_233
targets = new List<object>() {
_233
new { queue = smsQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var workflowConfiguration = new
_233
{
_233
task_routing = new
_233
{
_233
filters = new List<object>()
_233
{
_233
voiceFilter,
_233
smsFilter
_233
},
_233
default_filter = new
_233
{
_233
queue = allQueue.Sid,
_233
expression = "1==1",
_233
priority = "1",
_233
timeout = "30"
_233
}
_233
}
_233
};
_233
_233
// Call REST API
_233
return WorkflowResource.Create(
_233
workspaceSid,
_233
"Tech Support",
_233
Json.Encode(workflowConfiguration),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
15);
_233
}
_233
}
_233
}

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 requests will be made every time an event is triggered in our workspace.

TaskRouter.Web/App_Start/WorkspaceConfig.cs


_233
using System;
_233
using System.Collections.Generic;
_233
using System.Linq;
_233
using System.Web.Helpers;
_233
using TaskRouter.Web.Infrastructure;
_233
using Twilio;
_233
using Twilio.Rest.Taskrouter.V1;
_233
using Twilio.Rest.Taskrouter.V1.Workspace;
_233
_233
namespace TaskRouter.Web
_233
{
_233
public class WorkspaceConfig
_233
{
_233
private readonly Config _config;
_233
_233
private const string VoiceQueue = "VoiceQueue";
_233
private const string SmsQueue = "SMSQueue";
_233
private const string AllQueue = "AllQueue";
_233
_233
public static void RegisterWorkspace()
_233
{
_233
new WorkspaceConfig().Register();
_233
}
_233
_233
public WorkspaceConfig():this(new Config())
_233
{
_233
}
_233
_233
public WorkspaceConfig(Config config)
_233
{
_233
TwilioClient.Init(config.AccountSID, config.AuthToken);
_233
_config = config;
_233
_233
}
_233
_233
public WorkspaceConfig(Type workspaceResource):this()
_233
{
_233
}
_233
_233
public virtual ActivityResource GetActivityByFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Read(workspaceSid, friendlyName).First();
_233
}
_233
_233
public virtual ActivityResource CreateActivityWithFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Create(workspaceSid, friendlyName);
_233
}
_233
_233
public virtual WorkspaceResource GetWorkspaceByFriendlyName(string friendlyName)
_233
{
_233
return WorkspaceResource.Read(friendlyName).FirstOrDefault();
_233
}
_233
_233
public virtual WorkspaceResource CreateWorkspace(string friendlyName, Uri eventCallbackUrl)
_233
{
_233
return WorkspaceResource.Create(friendlyName, eventCallbackUrl);
_233
}
_233
_233
public virtual bool DeleteWorkspace(string workspaceSid)
_233
{
_233
return WorkspaceResource.Delete(workspaceSid);
_233
}
_233
_233
public virtual WorkerResource CreateWorker(string workspaceSid, string bob, string activitySid, string attributes)
_233
{
_233
return WorkerResource.Create(workspaceSid, bob, activitySid, attributes);
_233
}
_233
_233
public void Register()
_233
{
_233
var workspace = DeleteAndCreateWorkspace(
_233
"Twilio Workspace", new Uri(new Uri(_config.HostUrl), "/callback/events").AbsoluteUri);
_233
var workspaceSid = workspace.Sid;
_233
_233
var assignmentActivity = GetActivityByFriendlyName(workspaceSid, "Unavailable");
_233
var idleActivity = GetActivityByFriendlyName(workspaceSid, "Available");
_233
var reservationActivity = CreateActivityWithFriendlyName(workspaceSid, "Reserved");
_233
var offlineActivity = GetActivityByFriendlyName(workspaceSid, "Offline");
_233
_233
var workers = CreateWorkers(workspaceSid, idleActivity);
_233
var taskQueues = CreateTaskQueues(workspaceSid, assignmentActivity, reservationActivity);
_233
var workflow = CreateWorkflow(workspaceSid, taskQueues);
_233
_233
Singleton.Instance.WorkspaceSid = workspaceSid;
_233
Singleton.Instance.WorkflowSid = workflow.Sid;
_233
Singleton.Instance.Workers = workers;
_233
Singleton.Instance.PostWorkActivitySid = idleActivity.Sid;
_233
Singleton.Instance.IdleActivitySid = idleActivity.Sid;
_233
Singleton.Instance.OfflineActivitySid = offlineActivity.Sid;
_233
}
_233
_233
public virtual WorkspaceResource DeleteAndCreateWorkspace(string friendlyName, string eventCallbackUrl) {
_233
var workspace = GetWorkspaceByFriendlyName(friendlyName);
_233
if (workspace != null)
_233
{
_233
DeleteWorkspace(workspace.Sid);
_233
}
_233
_233
return CreateWorkspace(friendlyName, new Uri(eventCallbackUrl));
_233
}
_233
_233
private IDictionary<string, string> CreateWorkers(string workspaceSid, ActivityResource activity)
_233
{
_233
var attributesForBob = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableSMS"
_233
},
_233
contact_uri = _config.AgentForProgrammableSMS
_233
};
_233
_233
var bobWorker = CreateWorker(workspaceSid, "Bob", activity.Sid, Json.Encode(attributesForBob));
_233
_233
var attributesForAlice = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableVoice"
_233
},
_233
contact_uri = _config.AgentForProgrammableVoice
_233
};
_233
_233
var alice = CreateWorker(workspaceSid, "Alice", activity.Sid, Json.Encode(attributesForAlice));
_233
_233
return new Dictionary<string, string>
_233
{
_233
{ _config.AgentForProgrammableSMS, bobWorker.Sid },
_233
{ _config.AgentForProgrammableVoice, alice.Sid },
_233
};
_233
}
_233
_233
public virtual TaskQueueResource CreateTaskQueue(
_233
string workspaceSid, string friendlyName,
_233
string assignmentActivitySid, string reservationActivitySid, string targetWorkers)
_233
{
_233
var queue = TaskQueueResource.Create(
_233
workspaceSid,
_233
friendlyName: friendlyName,
_233
assignmentActivitySid: assignmentActivitySid,
_233
reservationActivitySid: reservationActivitySid
_233
);
_233
_233
TaskQueueResource.Update(
_233
workspaceSid,
_233
queue.Sid,
_233
friendlyName,
_233
targetWorkers,
_233
assignmentActivitySid,
_233
reservationActivitySid,
_233
1);
_233
_233
return queue;
_233
}
_233
_233
private IDictionary<string, TaskQueueResource> CreateTaskQueues(
_233
string workspaceSid, ActivityResource assignmentActivity, ActivityResource reservationActivity)
_233
{
_233
_233
var voiceQueue = CreateTaskQueue(
_233
workspaceSid, "Voice",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableVoice'");
_233
_233
var smsQueue = CreateTaskQueue(
_233
workspaceSid, "SMS",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableSMS'");
_233
_233
var allQueue = CreateTaskQueue(
_233
workspaceSid, "All",
_233
assignmentActivity.Sid, reservationActivity.Sid, "1 == 1");
_233
_233
return new Dictionary<string, TaskQueueResource> {
_233
{ VoiceQueue, voiceQueue },
_233
{ SmsQueue, smsQueue },
_233
{ AllQueue, allQueue }
_233
};
_233
}
_233
_233
public virtual WorkflowResource CreateWorkflow(string workspaceSid, IDictionary<string, TaskQueueResource> taskQueues)
_233
{
_233
var voiceQueue = taskQueues[VoiceQueue];
_233
var smsQueue = taskQueues[SmsQueue];
_233
var allQueue = taskQueues[AllQueue];
_233
_233
var voiceFilter = new {
_233
friendlyName = "Voice",
_233
expression = "selected_product==\"ProgrammableVoice\"",
_233
targets = new List<object>() {
_233
new { queue = voiceQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var smsFilter = new {
_233
friendlyName = "SMS",
_233
expression = "selected_product==\"ProgrammableSMS\"",
_233
targets = new List<object>() {
_233
new { queue = smsQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var workflowConfiguration = new
_233
{
_233
task_routing = new
_233
{
_233
filters = new List<object>()
_233
{
_233
voiceFilter,
_233
smsFilter
_233
},
_233
default_filter = new
_233
{
_233
queue = allQueue.Sid,
_233
expression = "1==1",
_233
priority = "1",
_233
timeout = "30"
_233
}
_233
}
_233
};
_233
_233
// Call REST API
_233
return WorkflowResource.Create(
_233
workspaceSid,
_233
"Tech Support",
_233
Json.Encode(workflowConfiguration),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
15);
_233
}
_233
}
_233
}

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 activity.Sid 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.

TaskRouter.Web/App_Start/WorkspaceConfig.cs


_233
using System;
_233
using System.Collections.Generic;
_233
using System.Linq;
_233
using System.Web.Helpers;
_233
using TaskRouter.Web.Infrastructure;
_233
using Twilio;
_233
using Twilio.Rest.Taskrouter.V1;
_233
using Twilio.Rest.Taskrouter.V1.Workspace;
_233
_233
namespace TaskRouter.Web
_233
{
_233
public class WorkspaceConfig
_233
{
_233
private readonly Config _config;
_233
_233
private const string VoiceQueue = "VoiceQueue";
_233
private const string SmsQueue = "SMSQueue";
_233
private const string AllQueue = "AllQueue";
_233
_233
public static void RegisterWorkspace()
_233
{
_233
new WorkspaceConfig().Register();
_233
}
_233
_233
public WorkspaceConfig():this(new Config())
_233
{
_233
}
_233
_233
public WorkspaceConfig(Config config)
_233
{
_233
TwilioClient.Init(config.AccountSID, config.AuthToken);
_233
_config = config;
_233
_233
}
_233
_233
public WorkspaceConfig(Type workspaceResource):this()
_233
{
_233
}
_233
_233
public virtual ActivityResource GetActivityByFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Read(workspaceSid, friendlyName).First();
_233
}
_233
_233
public virtual ActivityResource CreateActivityWithFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Create(workspaceSid, friendlyName);
_233
}
_233
_233
public virtual WorkspaceResource GetWorkspaceByFriendlyName(string friendlyName)
_233
{
_233
return WorkspaceResource.Read(friendlyName).FirstOrDefault();
_233
}
_233
_233
public virtual WorkspaceResource CreateWorkspace(string friendlyName, Uri eventCallbackUrl)
_233
{
_233
return WorkspaceResource.Create(friendlyName, eventCallbackUrl);
_233
}
_233
_233
public virtual bool DeleteWorkspace(string workspaceSid)
_233
{
_233
return WorkspaceResource.Delete(workspaceSid);
_233
}
_233
_233
public virtual WorkerResource CreateWorker(string workspaceSid, string bob, string activitySid, string attributes)
_233
{
_233
return WorkerResource.Create(workspaceSid, bob, activitySid, attributes);
_233
}
_233
_233
public void Register()
_233
{
_233
var workspace = DeleteAndCreateWorkspace(
_233
"Twilio Workspace", new Uri(new Uri(_config.HostUrl), "/callback/events").AbsoluteUri);
_233
var workspaceSid = workspace.Sid;
_233
_233
var assignmentActivity = GetActivityByFriendlyName(workspaceSid, "Unavailable");
_233
var idleActivity = GetActivityByFriendlyName(workspaceSid, "Available");
_233
var reservationActivity = CreateActivityWithFriendlyName(workspaceSid, "Reserved");
_233
var offlineActivity = GetActivityByFriendlyName(workspaceSid, "Offline");
_233
_233
var workers = CreateWorkers(workspaceSid, idleActivity);
_233
var taskQueues = CreateTaskQueues(workspaceSid, assignmentActivity, reservationActivity);
_233
var workflow = CreateWorkflow(workspaceSid, taskQueues);
_233
_233
Singleton.Instance.WorkspaceSid = workspaceSid;
_233
Singleton.Instance.WorkflowSid = workflow.Sid;
_233
Singleton.Instance.Workers = workers;
_233
Singleton.Instance.PostWorkActivitySid = idleActivity.Sid;
_233
Singleton.Instance.IdleActivitySid = idleActivity.Sid;
_233
Singleton.Instance.OfflineActivitySid = offlineActivity.Sid;
_233
}
_233
_233
public virtual WorkspaceResource DeleteAndCreateWorkspace(string friendlyName, string eventCallbackUrl) {
_233
var workspace = GetWorkspaceByFriendlyName(friendlyName);
_233
if (workspace != null)
_233
{
_233
DeleteWorkspace(workspace.Sid);
_233
}
_233
_233
return CreateWorkspace(friendlyName, new Uri(eventCallbackUrl));
_233
}
_233
_233
private IDictionary<string, string> CreateWorkers(string workspaceSid, ActivityResource activity)
_233
{
_233
var attributesForBob = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableSMS"
_233
},
_233
contact_uri = _config.AgentForProgrammableSMS
_233
};
_233
_233
var bobWorker = CreateWorker(workspaceSid, "Bob", activity.Sid, Json.Encode(attributesForBob));
_233
_233
var attributesForAlice = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableVoice"
_233
},
_233
contact_uri = _config.AgentForProgrammableVoice
_233
};
_233
_233
var alice = CreateWorker(workspaceSid, "Alice", activity.Sid, Json.Encode(attributesForAlice));
_233
_233
return new Dictionary<string, string>
_233
{
_233
{ _config.AgentForProgrammableSMS, bobWorker.Sid },
_233
{ _config.AgentForProgrammableVoice, alice.Sid },
_233
};
_233
}
_233
_233
public virtual TaskQueueResource CreateTaskQueue(
_233
string workspaceSid, string friendlyName,
_233
string assignmentActivitySid, string reservationActivitySid, string targetWorkers)
_233
{
_233
var queue = TaskQueueResource.Create(
_233
workspaceSid,
_233
friendlyName: friendlyName,
_233
assignmentActivitySid: assignmentActivitySid,
_233
reservationActivitySid: reservationActivitySid
_233
);
_233
_233
TaskQueueResource.Update(
_233
workspaceSid,
_233
queue.Sid,
_233
friendlyName,
_233
targetWorkers,
_233
assignmentActivitySid,
_233
reservationActivitySid,
_233
1);
_233
_233
return queue;
_233
}
_233
_233
private IDictionary<string, TaskQueueResource> CreateTaskQueues(
_233
string workspaceSid, ActivityResource assignmentActivity, ActivityResource reservationActivity)
_233
{
_233
_233
var voiceQueue = CreateTaskQueue(
_233
workspaceSid, "Voice",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableVoice'");
_233
_233
var smsQueue = CreateTaskQueue(
_233
workspaceSid, "SMS",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableSMS'");
_233
_233
var allQueue = CreateTaskQueue(
_233
workspaceSid, "All",
_233
assignmentActivity.Sid, reservationActivity.Sid, "1 == 1");
_233
_233
return new Dictionary<string, TaskQueueResource> {
_233
{ VoiceQueue, voiceQueue },
_233
{ SmsQueue, smsQueue },
_233
{ AllQueue, allQueue }
_233
};
_233
}
_233
_233
public virtual WorkflowResource CreateWorkflow(string workspaceSid, IDictionary<string, TaskQueueResource> taskQueues)
_233
{
_233
var voiceQueue = taskQueues[VoiceQueue];
_233
var smsQueue = taskQueues[SmsQueue];
_233
var allQueue = taskQueues[AllQueue];
_233
_233
var voiceFilter = new {
_233
friendlyName = "Voice",
_233
expression = "selected_product==\"ProgrammableVoice\"",
_233
targets = new List<object>() {
_233
new { queue = voiceQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var smsFilter = new {
_233
friendlyName = "SMS",
_233
expression = "selected_product==\"ProgrammableSMS\"",
_233
targets = new List<object>() {
_233
new { queue = smsQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var workflowConfiguration = new
_233
{
_233
task_routing = new
_233
{
_233
filters = new List<object>()
_233
{
_233
voiceFilter,
_233
smsFilter
_233
},
_233
default_filter = new
_233
{
_233
queue = allQueue.Sid,
_233
expression = "1==1",
_233
priority = "1",
_233
timeout = "30"
_233
}
_233
}
_233
};
_233
_233
// Call REST API
_233
return WorkflowResource.Create(
_233
workspaceSid,
_233
"Tech Support",
_233
Json.Encode(workflowConfiguration),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
15);
_233
}
_233
}
_233
}

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\"" .

TaskRouter.Web/App_Start/WorkspaceConfig.cs


_233
using System;
_233
using System.Collections.Generic;
_233
using System.Linq;
_233
using System.Web.Helpers;
_233
using TaskRouter.Web.Infrastructure;
_233
using Twilio;
_233
using Twilio.Rest.Taskrouter.V1;
_233
using Twilio.Rest.Taskrouter.V1.Workspace;
_233
_233
namespace TaskRouter.Web
_233
{
_233
public class WorkspaceConfig
_233
{
_233
private readonly Config _config;
_233
_233
private const string VoiceQueue = "VoiceQueue";
_233
private const string SmsQueue = "SMSQueue";
_233
private const string AllQueue = "AllQueue";
_233
_233
public static void RegisterWorkspace()
_233
{
_233
new WorkspaceConfig().Register();
_233
}
_233
_233
public WorkspaceConfig():this(new Config())
_233
{
_233
}
_233
_233
public WorkspaceConfig(Config config)
_233
{
_233
TwilioClient.Init(config.AccountSID, config.AuthToken);
_233
_config = config;
_233
_233
}
_233
_233
public WorkspaceConfig(Type workspaceResource):this()
_233
{
_233
}
_233
_233
public virtual ActivityResource GetActivityByFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Read(workspaceSid, friendlyName).First();
_233
}
_233
_233
public virtual ActivityResource CreateActivityWithFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Create(workspaceSid, friendlyName);
_233
}
_233
_233
public virtual WorkspaceResource GetWorkspaceByFriendlyName(string friendlyName)
_233
{
_233
return WorkspaceResource.Read(friendlyName).FirstOrDefault();
_233
}
_233
_233
public virtual WorkspaceResource CreateWorkspace(string friendlyName, Uri eventCallbackUrl)
_233
{
_233
return WorkspaceResource.Create(friendlyName, eventCallbackUrl);
_233
}
_233
_233
public virtual bool DeleteWorkspace(string workspaceSid)
_233
{
_233
return WorkspaceResource.Delete(workspaceSid);
_233
}
_233
_233
public virtual WorkerResource CreateWorker(string workspaceSid, string bob, string activitySid, string attributes)
_233
{
_233
return WorkerResource.Create(workspaceSid, bob, activitySid, attributes);
_233
}
_233
_233
public void Register()
_233
{
_233
var workspace = DeleteAndCreateWorkspace(
_233
"Twilio Workspace", new Uri(new Uri(_config.HostUrl), "/callback/events").AbsoluteUri);
_233
var workspaceSid = workspace.Sid;
_233
_233
var assignmentActivity = GetActivityByFriendlyName(workspaceSid, "Unavailable");
_233
var idleActivity = GetActivityByFriendlyName(workspaceSid, "Available");
_233
var reservationActivity = CreateActivityWithFriendlyName(workspaceSid, "Reserved");
_233
var offlineActivity = GetActivityByFriendlyName(workspaceSid, "Offline");
_233
_233
var workers = CreateWorkers(workspaceSid, idleActivity);
_233
var taskQueues = CreateTaskQueues(workspaceSid, assignmentActivity, reservationActivity);
_233
var workflow = CreateWorkflow(workspaceSid, taskQueues);
_233
_233
Singleton.Instance.WorkspaceSid = workspaceSid;
_233
Singleton.Instance.WorkflowSid = workflow.Sid;
_233
Singleton.Instance.Workers = workers;
_233
Singleton.Instance.PostWorkActivitySid = idleActivity.Sid;
_233
Singleton.Instance.IdleActivitySid = idleActivity.Sid;
_233
Singleton.Instance.OfflineActivitySid = offlineActivity.Sid;
_233
}
_233
_233
public virtual WorkspaceResource DeleteAndCreateWorkspace(string friendlyName, string eventCallbackUrl) {
_233
var workspace = GetWorkspaceByFriendlyName(friendlyName);
_233
if (workspace != null)
_233
{
_233
DeleteWorkspace(workspace.Sid);
_233
}
_233
_233
return CreateWorkspace(friendlyName, new Uri(eventCallbackUrl));
_233
}
_233
_233
private IDictionary<string, string> CreateWorkers(string workspaceSid, ActivityResource activity)
_233
{
_233
var attributesForBob = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableSMS"
_233
},
_233
contact_uri = _config.AgentForProgrammableSMS
_233
};
_233
_233
var bobWorker = CreateWorker(workspaceSid, "Bob", activity.Sid, Json.Encode(attributesForBob));
_233
_233
var attributesForAlice = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableVoice"
_233
},
_233
contact_uri = _config.AgentForProgrammableVoice
_233
};
_233
_233
var alice = CreateWorker(workspaceSid, "Alice", activity.Sid, Json.Encode(attributesForAlice));
_233
_233
return new Dictionary<string, string>
_233
{
_233
{ _config.AgentForProgrammableSMS, bobWorker.Sid },
_233
{ _config.AgentForProgrammableVoice, alice.Sid },
_233
};
_233
}
_233
_233
public virtual TaskQueueResource CreateTaskQueue(
_233
string workspaceSid, string friendlyName,
_233
string assignmentActivitySid, string reservationActivitySid, string targetWorkers)
_233
{
_233
var queue = TaskQueueResource.Create(
_233
workspaceSid,
_233
friendlyName: friendlyName,
_233
assignmentActivitySid: assignmentActivitySid,
_233
reservationActivitySid: reservationActivitySid
_233
);
_233
_233
TaskQueueResource.Update(
_233
workspaceSid,
_233
queue.Sid,
_233
friendlyName,
_233
targetWorkers,
_233
assignmentActivitySid,
_233
reservationActivitySid,
_233
1);
_233
_233
return queue;
_233
}
_233
_233
private IDictionary<string, TaskQueueResource> CreateTaskQueues(
_233
string workspaceSid, ActivityResource assignmentActivity, ActivityResource reservationActivity)
_233
{
_233
_233
var voiceQueue = CreateTaskQueue(
_233
workspaceSid, "Voice",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableVoice'");
_233
_233
var smsQueue = CreateTaskQueue(
_233
workspaceSid, "SMS",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableSMS'");
_233
_233
var allQueue = CreateTaskQueue(
_233
workspaceSid, "All",
_233
assignmentActivity.Sid, reservationActivity.Sid, "1 == 1");
_233
_233
return new Dictionary<string, TaskQueueResource> {
_233
{ VoiceQueue, voiceQueue },
_233
{ SmsQueue, smsQueue },
_233
{ AllQueue, allQueue }
_233
};
_233
}
_233
_233
public virtual WorkflowResource CreateWorkflow(string workspaceSid, IDictionary<string, TaskQueueResource> taskQueues)
_233
{
_233
var voiceQueue = taskQueues[VoiceQueue];
_233
var smsQueue = taskQueues[SmsQueue];
_233
var allQueue = taskQueues[AllQueue];
_233
_233
var voiceFilter = new {
_233
friendlyName = "Voice",
_233
expression = "selected_product==\"ProgrammableVoice\"",
_233
targets = new List<object>() {
_233
new { queue = voiceQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var smsFilter = new {
_233
friendlyName = "SMS",
_233
expression = "selected_product==\"ProgrammableSMS\"",
_233
targets = new List<object>() {
_233
new { queue = smsQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var workflowConfiguration = new
_233
{
_233
task_routing = new
_233
{
_233
filters = new List<object>()
_233
{
_233
voiceFilter,
_233
smsFilter
_233
},
_233
default_filter = new
_233
{
_233
queue = allQueue.Sid,
_233
expression = "1==1",
_233
priority = "1",
_233
timeout = "30"
_233
}
_233
}
_233
};
_233
_233
// Call REST API
_233
return WorkflowResource.Create(
_233
workspaceSid,
_233
"Tech Support",
_233
Json.Encode(workflowConfiguration),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
15);
_233
}
_233
}
_233
}

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. Timeout as the maximum time we want to wait until a Worker is available for handling a Task.
  4. workflowConfiguration 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.

TaskRouter.Web/App_Start/WorkspaceConfig.cs


_233
using System;
_233
using System.Collections.Generic;
_233
using System.Linq;
_233
using System.Web.Helpers;
_233
using TaskRouter.Web.Infrastructure;
_233
using Twilio;
_233
using Twilio.Rest.Taskrouter.V1;
_233
using Twilio.Rest.Taskrouter.V1.Workspace;
_233
_233
namespace TaskRouter.Web
_233
{
_233
public class WorkspaceConfig
_233
{
_233
private readonly Config _config;
_233
_233
private const string VoiceQueue = "VoiceQueue";
_233
private const string SmsQueue = "SMSQueue";
_233
private const string AllQueue = "AllQueue";
_233
_233
public static void RegisterWorkspace()
_233
{
_233
new WorkspaceConfig().Register();
_233
}
_233
_233
public WorkspaceConfig():this(new Config())
_233
{
_233
}
_233
_233
public WorkspaceConfig(Config config)
_233
{
_233
TwilioClient.Init(config.AccountSID, config.AuthToken);
_233
_config = config;
_233
_233
}
_233
_233
public WorkspaceConfig(Type workspaceResource):this()
_233
{
_233
}
_233
_233
public virtual ActivityResource GetActivityByFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Read(workspaceSid, friendlyName).First();
_233
}
_233
_233
public virtual ActivityResource CreateActivityWithFriendlyName(string workspaceSid, string friendlyName)
_233
{
_233
return ActivityResource.Create(workspaceSid, friendlyName);
_233
}
_233
_233
public virtual WorkspaceResource GetWorkspaceByFriendlyName(string friendlyName)
_233
{
_233
return WorkspaceResource.Read(friendlyName).FirstOrDefault();
_233
}
_233
_233
public virtual WorkspaceResource CreateWorkspace(string friendlyName, Uri eventCallbackUrl)
_233
{
_233
return WorkspaceResource.Create(friendlyName, eventCallbackUrl);
_233
}
_233
_233
public virtual bool DeleteWorkspace(string workspaceSid)
_233
{
_233
return WorkspaceResource.Delete(workspaceSid);
_233
}
_233
_233
public virtual WorkerResource CreateWorker(string workspaceSid, string bob, string activitySid, string attributes)
_233
{
_233
return WorkerResource.Create(workspaceSid, bob, activitySid, attributes);
_233
}
_233
_233
public void Register()
_233
{
_233
var workspace = DeleteAndCreateWorkspace(
_233
"Twilio Workspace", new Uri(new Uri(_config.HostUrl), "/callback/events").AbsoluteUri);
_233
var workspaceSid = workspace.Sid;
_233
_233
var assignmentActivity = GetActivityByFriendlyName(workspaceSid, "Unavailable");
_233
var idleActivity = GetActivityByFriendlyName(workspaceSid, "Available");
_233
var reservationActivity = CreateActivityWithFriendlyName(workspaceSid, "Reserved");
_233
var offlineActivity = GetActivityByFriendlyName(workspaceSid, "Offline");
_233
_233
var workers = CreateWorkers(workspaceSid, idleActivity);
_233
var taskQueues = CreateTaskQueues(workspaceSid, assignmentActivity, reservationActivity);
_233
var workflow = CreateWorkflow(workspaceSid, taskQueues);
_233
_233
Singleton.Instance.WorkspaceSid = workspaceSid;
_233
Singleton.Instance.WorkflowSid = workflow.Sid;
_233
Singleton.Instance.Workers = workers;
_233
Singleton.Instance.PostWorkActivitySid = idleActivity.Sid;
_233
Singleton.Instance.IdleActivitySid = idleActivity.Sid;
_233
Singleton.Instance.OfflineActivitySid = offlineActivity.Sid;
_233
}
_233
_233
public virtual WorkspaceResource DeleteAndCreateWorkspace(string friendlyName, string eventCallbackUrl) {
_233
var workspace = GetWorkspaceByFriendlyName(friendlyName);
_233
if (workspace != null)
_233
{
_233
DeleteWorkspace(workspace.Sid);
_233
}
_233
_233
return CreateWorkspace(friendlyName, new Uri(eventCallbackUrl));
_233
}
_233
_233
private IDictionary<string, string> CreateWorkers(string workspaceSid, ActivityResource activity)
_233
{
_233
var attributesForBob = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableSMS"
_233
},
_233
contact_uri = _config.AgentForProgrammableSMS
_233
};
_233
_233
var bobWorker = CreateWorker(workspaceSid, "Bob", activity.Sid, Json.Encode(attributesForBob));
_233
_233
var attributesForAlice = new
_233
{
_233
products = new List<object>()
_233
{
_233
"ProgrammableVoice"
_233
},
_233
contact_uri = _config.AgentForProgrammableVoice
_233
};
_233
_233
var alice = CreateWorker(workspaceSid, "Alice", activity.Sid, Json.Encode(attributesForAlice));
_233
_233
return new Dictionary<string, string>
_233
{
_233
{ _config.AgentForProgrammableSMS, bobWorker.Sid },
_233
{ _config.AgentForProgrammableVoice, alice.Sid },
_233
};
_233
}
_233
_233
public virtual TaskQueueResource CreateTaskQueue(
_233
string workspaceSid, string friendlyName,
_233
string assignmentActivitySid, string reservationActivitySid, string targetWorkers)
_233
{
_233
var queue = TaskQueueResource.Create(
_233
workspaceSid,
_233
friendlyName: friendlyName,
_233
assignmentActivitySid: assignmentActivitySid,
_233
reservationActivitySid: reservationActivitySid
_233
);
_233
_233
TaskQueueResource.Update(
_233
workspaceSid,
_233
queue.Sid,
_233
friendlyName,
_233
targetWorkers,
_233
assignmentActivitySid,
_233
reservationActivitySid,
_233
1);
_233
_233
return queue;
_233
}
_233
_233
private IDictionary<string, TaskQueueResource> CreateTaskQueues(
_233
string workspaceSid, ActivityResource assignmentActivity, ActivityResource reservationActivity)
_233
{
_233
_233
var voiceQueue = CreateTaskQueue(
_233
workspaceSid, "Voice",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableVoice'");
_233
_233
var smsQueue = CreateTaskQueue(
_233
workspaceSid, "SMS",
_233
assignmentActivity.Sid, reservationActivity.Sid, "products HAS 'ProgrammableSMS'");
_233
_233
var allQueue = CreateTaskQueue(
_233
workspaceSid, "All",
_233
assignmentActivity.Sid, reservationActivity.Sid, "1 == 1");
_233
_233
return new Dictionary<string, TaskQueueResource> {
_233
{ VoiceQueue, voiceQueue },
_233
{ SmsQueue, smsQueue },
_233
{ AllQueue, allQueue }
_233
};
_233
}
_233
_233
public virtual WorkflowResource CreateWorkflow(string workspaceSid, IDictionary<string, TaskQueueResource> taskQueues)
_233
{
_233
var voiceQueue = taskQueues[VoiceQueue];
_233
var smsQueue = taskQueues[SmsQueue];
_233
var allQueue = taskQueues[AllQueue];
_233
_233
var voiceFilter = new {
_233
friendlyName = "Voice",
_233
expression = "selected_product==\"ProgrammableVoice\"",
_233
targets = new List<object>() {
_233
new { queue = voiceQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var smsFilter = new {
_233
friendlyName = "SMS",
_233
expression = "selected_product==\"ProgrammableSMS\"",
_233
targets = new List<object>() {
_233
new { queue = smsQueue.Sid, Priority = "5", Timeout = "30" },
_233
new { queue = allQueue.Sid, Expression = "1==1", Priority = "1", Timeout = "30" }
_233
}
_233
};
_233
_233
var workflowConfiguration = new
_233
{
_233
task_routing = new
_233
{
_233
filters = new List<object>()
_233
{
_233
voiceFilter,
_233
smsFilter
_233
},
_233
default_filter = new
_233
{
_233
queue = allQueue.Sid,
_233
expression = "1==1",
_233
priority = "1",
_233
timeout = "30"
_233
}
_233
}
_233
};
_233
_233
// Call REST API
_233
return WorkflowResource.Create(
_233
workspaceSid,
_233
"Tech Support",
_233
Json.Encode(workflowConfiguration),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
new Uri($"{_config.HostUrl}/callback/assignment"),
_233
15);
_233
}
_233
}
_233
}

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, and a key they can press in order to select one. The Gather verb allows us to capture the user's key press.

Handling Twilio's Requests

handling-twilios-requests page anchor

TaskRouter.Web/Controllers/CallController.cs


_50
using System.Web.Mvc;
_50
using TaskRouter.Web.Infrastructure;
_50
using TaskRouter.Web.Models;
_50
using TaskRouter.Web.Services;
_50
using Twilio.AspNet.Mvc;
_50
using Twilio.TwiML;
_50
_50
namespace TaskRouter.Web.Controllers
_50
{
_50
public class CallController : TwilioController
_50
{
_50
private readonly IMissedCallsService _service;
_50
_50
public CallController()
_50
{
_50
_service = new MissedCallsService(new TaskRouterDbContext());
_50
}
_50
_50
public CallController(IMissedCallsService service)
_50
{
_50
_service = service;
_50
}
_50
_50
[HttpPost]
_50
public ActionResult Incoming()
_50
{
_50
var response = new VoiceResponse();
_50
var gather = new Gather(numDigits: 1, action: "/call/enqueue", method: "POST");
_50
gather.Say("For Programmable SMS, press one. For Voice, press any other key.");
_50
response.Gather(gather);
_50
_50
return TwiML(response);
_50
}
_50
_50
[HttpPost]
_50
public ActionResult Enqueue(string digits)
_50
{
_50
var selectedProduct = digits == "1" ? "ProgrammableSMS" : "ProgrammableVoice";
_50
var response = new VoiceResponse();
_50
_50
response.Enqueue(
_50
selectedProduct,
_50
workflowSid: Singleton.Instance.WorkflowSid);
_50
_50
return TwiML(response);
_50
}
_50
_50
_50
}
_50
}

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


This is the endpoint set as the action URL on the Gather verb on the previous step. A request is made to this endpoint when the user presses a key during the call. This request has a Digits parameter that holds the pressed keys. A Task will be created based on the pressed digit with the selected_product as an attribute. The Workflow will take this Task's attributes and match them 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.

TaskRouter.Web/Controllers/CallController.cs


_50
using System.Web.Mvc;
_50
using TaskRouter.Web.Infrastructure;
_50
using TaskRouter.Web.Models;
_50
using TaskRouter.Web.Services;
_50
using Twilio.AspNet.Mvc;
_50
using Twilio.TwiML;
_50
_50
namespace TaskRouter.Web.Controllers
_50
{
_50
public class CallController : TwilioController
_50
{
_50
private readonly IMissedCallsService _service;
_50
_50
public CallController()
_50
{
_50
_service = new MissedCallsService(new TaskRouterDbContext());
_50
}
_50
_50
public CallController(IMissedCallsService service)
_50
{
_50
_service = service;
_50
}
_50
_50
[HttpPost]
_50
public ActionResult Incoming()
_50
{
_50
var response = new VoiceResponse();
_50
var gather = new Gather(numDigits: 1, action: "/call/enqueue", method: "POST");
_50
gather.Say("For Programmable SMS, press one. For Voice, press any other key.");
_50
response.Gather(gather);
_50
_50
return TwiML(response);
_50
}
_50
_50
[HttpPost]
_50
public ActionResult Enqueue(string digits)
_50
{
_50
var selectedProduct = digits == "1" ? "ProgrammableSMS" : "ProgrammableVoice";
_50
var response = new VoiceResponse();
_50
_50
response.Enqueue(
_50
selectedProduct,
_50
workflowSid: Singleton.Instance.WorkflowSid);
_50
_50
return TwiML(response);
_50
}
_50
_50
_50
}
_50
}

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 using the WorkspaceConfig(link takes you to an external page) class when the application is initialized. 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, let's 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.

TaskRouter.Web/Controllers/CallbackController.cs


_112
using Newtonsoft.Json;
_112
using System;
_112
using System.Linq;
_112
using System.Threading.Tasks;
_112
using System.Web.Mvc;
_112
using TaskRouter.Web.Infrastructure;
_112
using TaskRouter.Web.Models;
_112
using TaskRouter.Web.Services;
_112
using Twilio;
_112
using Twilio.AspNet.Mvc;
_112
using Twilio.Rest.Api.V2010.Account;
_112
using Twilio.Types;
_112
_112
namespace TaskRouter.Web.Controllers
_112
{
_112
public class CallbackController : TwilioController
_112
{
_112
private readonly IMissedCallsService _service;
_112
_112
public CallbackController()
_112
{
_112
_service = new MissedCallsService(new TaskRouterDbContext());
_112
_112
if (Config.ENV != "test")
_112
{
_112
TwilioClient.Init(Config.AccountSID, Config.AuthToken);
_112
}
_112
}
_112
_112
public CallbackController(IMissedCallsService service)
_112
{
_112
_service = service;
_112
}
_112
_112
[HttpPost]
_112
public ActionResult Assignment()
_112
{
_112
var response = new
_112
{
_112
instruction = "dequeue",
_112
post_work_activity_sid = Singleton.Instance.PostWorkActivitySid
_112
};
_112
_112
return new JsonResult() { Data = response };
_112
}
_112
_112
[HttpPost]
_112
public async Task<ActionResult> Events(
_112
string eventType, string taskAttributes, string workerSid, string workerActivityName, string workerAttributes)
_112
{
_112
if (IsEventTimeoutOrCanceled(eventType))
_112
{
_112
await CreateMissedCallAndRedirectToVoiceMail(taskAttributes);
_112
}
_112
_112
if (HasWorkerChangedToOffline(eventType, workerActivityName))
_112
{
_112
SendMessageToWorker(workerSid, workerAttributes);
_112
}
_112
_112
return new EmptyResult();
_112
}
_112
_112
_112
private bool IsEventTimeoutOrCanceled(string eventType)
_112
{
_112
var desiredEvents = new string[] { "workflow.timeout", "task.canceled" };
_112
return desiredEvents.Any(e => e == eventType);
_112
}
_112
_112
private bool HasWorkerChangedToOffline(string eventType, string workerActivityName)
_112
{
_112
return eventType == "worker.activity.update" && workerActivityName == "Offline";
_112
}
_112
_112
private async Task CreateMissedCallAndRedirectToVoiceMail(string taskAttributes)
_112
{
_112
dynamic attributes = JsonConvert.DeserializeObject(taskAttributes);
_112
var missedCall = new MissedCall
_112
{
_112
PhoneNumber = attributes.from,
_112
Product = attributes.selected_product,
_112
CreatedAt = DateTime.Now
_112
};
_112
_112
await _service.CreateAsync(missedCall);
_112
string voiceSid = attributes.call_sid;
_112
VoiceMail(voiceSid);
_112
}
_112
_112
private void SendMessageToWorker(string workerSid, string workerAttributes)
_112
{
_112
const string message = "You went offline. To make yourself available reply with \"on\"";
_112
_112
dynamic attributes = JsonConvert.DeserializeObject(workerAttributes);
_112
string workerPhoneNumber = attributes.contact_uri;
_112
_112
MessageResource.Create(
_112
to: new PhoneNumber(Config.TwilioNumber),
_112
from: new PhoneNumber(workerPhoneNumber),
_112
body: message
_112
);
_112
}
_112
_112
private void VoiceMail(string callSid)
_112
{
_112
var msg = "Sorry, All agents are busy. Please leave a message. We will call you as soon as possible";
_112
var routeUrl = "http://twimlets.com/voicemail?Email=" + Config.VoiceMail + "&Message=" + Url.Encode(msg);
_112
CallResource.Update(callSid, url: new Uri(routeUrl));
_112
}
_112
}
_112
}

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 Workflow Configuration-- or rather when no worker is available.

Here we use TwilioRestClient to route this call to a Voicemail Twimlet(link takes you to an external page). 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.

TaskRouter.Web/Controllers/CallbackController.cs


_112
using Newtonsoft.Json;
_112
using System;
_112
using System.Linq;
_112
using System.Threading.Tasks;
_112
using System.Web.Mvc;
_112
using TaskRouter.Web.Infrastructure;
_112
using TaskRouter.Web.Models;
_112
using TaskRouter.Web.Services;
_112
using Twilio;
_112
using Twilio.AspNet.Mvc;
_112
using Twilio.Rest.Api.V2010.Account;
_112
using Twilio.Types;
_112
_112
namespace TaskRouter.Web.Controllers
_112
{
_112
public class CallbackController : TwilioController
_112
{
_112
private readonly IMissedCallsService _service;
_112
_112
public CallbackController()
_112
{
_112
_service = new MissedCallsService(new TaskRouterDbContext());
_112
_112
if (Config.ENV != "test")
_112
{
_112
TwilioClient.Init(Config.AccountSID, Config.AuthToken);
_112
}
_112
}
_112
_112
public CallbackController(IMissedCallsService service)
_112
{
_112
_service = service;
_112
}
_112
_112
[HttpPost]
_112
public ActionResult Assignment()
_112
{
_112
var response = new
_112
{
_112
instruction = "dequeue",
_112
post_work_activity_sid = Singleton.Instance.PostWorkActivitySid
_112
};
_112
_112
return new JsonResult() { Data = response };
_112
}
_112
_112
[HttpPost]
_112
public async Task<ActionResult> Events(
_112
string eventType, string taskAttributes, string workerSid, string workerActivityName, string workerAttributes)
_112
{
_112
if (IsEventTimeoutOrCanceled(eventType))
_112
{
_112
await CreateMissedCallAndRedirectToVoiceMail(taskAttributes);
_112
}
_112
_112
if (HasWorkerChangedToOffline(eventType, workerActivityName))
_112
{
_112
SendMessageToWorker(workerSid, workerAttributes);
_112
}
_112
_112
return new EmptyResult();
_112
}
_112
_112
_112
private bool IsEventTimeoutOrCanceled(string eventType)
_112
{
_112
var desiredEvents = new string[] { "workflow.timeout", "task.canceled" };
_112
return desiredEvents.Any(e => e == eventType);
_112
}
_112
_112
private bool HasWorkerChangedToOffline(string eventType, string workerActivityName)
_112
{
_112
return eventType == "worker.activity.update" && workerActivityName == "Offline";
_112
}
_112
_112
private async Task CreateMissedCallAndRedirectToVoiceMail(string taskAttributes)
_112
{
_112
dynamic attributes = JsonConvert.DeserializeObject(taskAttributes);
_112
var missedCall = new MissedCall
_112
{
_112
PhoneNumber = attributes.from,
_112
Product = attributes.selected_product,
_112
CreatedAt = DateTime.Now
_112
};
_112
_112
await _service.CreateAsync(missedCall);
_112
string voiceSid = attributes.call_sid;
_112
VoiceMail(voiceSid);
_112
}
_112
_112
private void SendMessageToWorker(string workerSid, string workerAttributes)
_112
{
_112
const string message = "You went offline. To make yourself available reply with \"on\"";
_112
_112
dynamic attributes = JsonConvert.DeserializeObject(workerAttributes);
_112
string workerPhoneNumber = attributes.contact_uri;
_112
_112
MessageResource.Create(
_112
to: new PhoneNumber(Config.TwilioNumber),
_112
from: new PhoneNumber(workerPhoneNumber),
_112
body: message
_112
);
_112
}
_112
_112
private void VoiceMail(string callSid)
_112
{
_112
var msg = "Sorry, All agents are busy. Please leave a message. We will call you as soon as possible";
_112
var routeUrl = "http://twimlets.com/voicemail?Email=" + Config.VoiceMail + "&Message=" + Url.Encode(msg);
_112
CallResource.Update(callSid, url: new Uri(routeUrl));
_112
}
_112
}
_112
}

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.

Changing a Worker's Activity

changing-a-workers-activity page anchor

TaskRouter.Web/Controllers/MessageController.cs


_61
using System;
_61
using System.Web.Mvc;
_61
using TaskRouter.Web.Infrastructure;
_61
using Twilio;
_61
using Twilio.AspNet.Mvc;
_61
using Twilio.Rest.Taskrouter.V1.Workspace;
_61
using Twilio.TwiML;
_61
_61
namespace TaskRouter.Web.Controllers
_61
{
_61
public class MessageController : TwilioController
_61
{
_61
private const string On = "on";
_61
private const string Off = "off";
_61
_61
public MessageController()
_61
{
_61
if (Config.ENV != "test")
_61
{
_61
TwilioClient.Init(Config.AccountSID, Config.AuthToken);
_61
}
_61
}
_61
_61
public virtual WorkerResource FetchWorker(string workspaceSid, string workerSid)
_61
{
_61
return WorkerResource.Fetch(workspaceSid, workerSid);
_61
}
_61
_61
public virtual WorkerResource UpdateWorker(string pathWorkspaceSid, string pathSid, string activitySid = null,
_61
string attributes = null, string friendlyName = null)
_61
{
_61
return WorkerResource.Update(pathWorkspaceSid, pathSid, activitySid, attributes, friendlyName);
_61
}
_61
_61
[HttpPost]
_61
public ActionResult Incoming(string from, string body)
_61
{
_61
var workspaceSid = Singleton.Instance.WorkspaceSid;
_61
var workerSid = Singleton.Instance.Workers[from];
_61
var idleActivitySid = Singleton.Instance.IdleActivitySid;
_61
var offlineActivitySid = Singleton.Instance.OfflineActivitySid;
_61
var message = "Unrecognized command, reply with \"on\" to activate your worker or \"off\" otherwise";
_61
_61
var worker = FetchWorker(workspaceSid, workerSid);
_61
_61
if (body.Equals(On, StringComparison.InvariantCultureIgnoreCase))
_61
{
_61
UpdateWorker(workspaceSid, workerSid, idleActivitySid, worker.Attributes, worker.FriendlyName);
_61
message = "Your worker is online";
_61
}
_61
_61
if (body.Equals(Off, StringComparison.InvariantCultureIgnoreCase))
_61
{
_61
UpdateWorker(workspaceSid, workerSid, offlineActivitySid, worker.Attributes, worker.FriendlyName);
_61
message = "Your worker is offline";
_61
}
_61
_61
return TwiML(new MessagingResponse().Message(message));
_61
}
_61
}
_61
}

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


If you're a .NET developer working with Twilio, you might also enjoy these tutorials:

Browser-Calls

Learn how to use Twilio Client to make browser-to-phone and browser-to-browser calls with ease.

ETA-Notifications(link takes you to an external page)

Learn how to implement ETA Notifications using ASP.NET MVC and Twilio.

Did this help?

did-this-help page anchor

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


Rate this page: