In this tutorial we will show how to automate the routing of calls from customers to your support agents. In this example customers would select a product, then be connected to a specialist for that product. If no one is available our customer's number will be saved so that our agent can call them back.
In order to instruct TaskRouter to handle the Tasks, we need to configure a Workspace. We can do this in the TaskRouter Console or programmatically using the TaskRouter REST API.
In this 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:
We'll use a TaskRouterClient
provided in the twilio-csharp helper library to create and configure the workspace.
TaskRouter.Web/App_Start/WorkspaceConfig.cs
_233using System;_233using System.Collections.Generic;_233using System.Linq;_233using System.Web.Helpers;_233using TaskRouter.Web.Infrastructure;_233using Twilio;_233using Twilio.Rest.Taskrouter.V1;_233using Twilio.Rest.Taskrouter.V1.Workspace;_233_233namespace 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
_233using System;_233using System.Collections.Generic;_233using System.Linq;_233using System.Web.Helpers;_233using TaskRouter.Web.Infrastructure;_233using Twilio;_233using Twilio.Rest.Taskrouter.V1;_233using Twilio.Rest.Taskrouter.V1.Workspace;_233_233namespace 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
_233using System;_233using System.Collections.Generic;_233using System.Linq;_233using System.Web.Helpers;_233using TaskRouter.Web.Infrastructure;_233using Twilio;_233using Twilio.Rest.Taskrouter.V1;_233using Twilio.Rest.Taskrouter.V1.Workspace;_233_233namespace 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:
SMS
- Will target Workers specialized in Programmable SMS, such as Bob, using the expression
"products HAS \"ProgrammableSMS\""
.
Voice
- Will do the same for Programmable Voice Workers, such as Alice, using the expression
"products HAS \"ProgrammableVoice\""
.
TaskRouter.Web/App_Start/WorkspaceConfig.cs
_233using System;_233using System.Collections.Generic;_233using System.Linq;_233using System.Web.Helpers;_233using TaskRouter.Web.Infrastructure;_233using Twilio;_233using Twilio.Rest.Taskrouter.V1;_233using Twilio.Rest.Taskrouter.V1.Workspace;_233_233namespace 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:
friendlyName
as the name of a Workflow.
assignmentCallbackUrl
and
fallbackAssignmentCallbackUrl
as the public URL where a request will be made when this Workflow assigns a Task to a Worker. We will learn how to implement it on the next steps.
Timeout
as the maximum time we want to wait until a Worker is available for handling a Task.
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
_233using System;_233using System.Collections.Generic;_233using System.Linq;_233using System.Web.Helpers;_233using TaskRouter.Web.Infrastructure;_233using Twilio;_233using Twilio.Rest.Taskrouter.V1;_233using Twilio.Rest.Taskrouter.V1.Workspace;_233_233namespace 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.
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.
TaskRouter.Web/Controllers/CallController.cs
_50using System.Web.Mvc;_50using TaskRouter.Web.Infrastructure;_50using TaskRouter.Web.Models;_50using TaskRouter.Web.Services;_50using Twilio.AspNet.Mvc;_50using Twilio.TwiML;_50_50namespace 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
_50using System.Web.Mvc;_50using TaskRouter.Web.Infrastructure;_50using TaskRouter.Web.Models;_50using TaskRouter.Web.Services;_50using Twilio.AspNet.Mvc;_50using Twilio.TwiML;_50_50namespace 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:
POST
request is made to the Workflow's AssignmentCallbackURL, which was configured using the
WorkspaceConfig
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
_112using Newtonsoft.Json;_112using System;_112using System.Linq;_112using System.Threading.Tasks;_112using System.Web.Mvc;_112using TaskRouter.Web.Infrastructure;_112using TaskRouter.Web.Models;_112using TaskRouter.Web.Services;_112using Twilio;_112using Twilio.AspNet.Mvc;_112using Twilio.Rest.Api.V2010.Account;_112using Twilio.Types;_112_112namespace 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. 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
_112using Newtonsoft.Json;_112using System;_112using System.Linq;_112using System.Threading.Tasks;_112using System.Web.Mvc;_112using TaskRouter.Web.Infrastructure;_112using TaskRouter.Web.Models;_112using TaskRouter.Web.Services;_112using Twilio;_112using Twilio.AspNet.Mvc;_112using Twilio.Rest.Api.V2010.Account;_112using Twilio.Types;_112_112namespace 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.
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.
TaskRouter.Web/Controllers/MessageController.cs
_61using System;_61using System.Web.Mvc;_61using TaskRouter.Web.Infrastructure;_61using Twilio;_61using Twilio.AspNet.Mvc;_61using Twilio.Rest.Taskrouter.V1.Workspace;_61using Twilio.TwiML;_61_61namespace 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:
Learn how to use Twilio Client to make browser-to-phone and browser-to-browser calls with ease.
Learn how to implement ETA Notifications using ASP.NET MVC and Twilio.
Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think!