In this tutorial we will show how to automate the routing of calls from customers to your support agents. Customers will be able to select a product and wait while TaskRouter tries to contact a product specialist for the best support experience. If no one is available, our application will save the customer's number and selected product so an agent can call them back later on.
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.
A Workspace is the container element for any TaskRouter application. The elements are:
/src/main/resources/workspace.json
1{2"name": "Twilio Workspace",3"event_callback": "%(host)s/events",4"workers": [5{6"name": "Bob",7"attributes": {8"products": [9"ProgrammableSMS"10],11"contact_uri": "%(bob_number)s"12}13},14{15"name": "Alice",16"attributes": {17"products": [18"ProgrammableVoice"19],20"contact_uri": "%(alice_number)s"21}22}23],24"activities": [25{26"name": "Offline",27"availability": "false"28},29{30"name": "Idle",31"availability": "true"32},33{34"name": "Busy",35"availability": "false"36},37{38"name": "Reserved",39"availability": "false"40}41],42"task_queues": [43{44"name": "Default",45"targetWorkers": "1==1"46},47{48"name": "SMS",49"targetWorkers": "products HAS \"ProgrammableSMS\""50},51{52"name": "Voice",53"targetWorkers": "products HAS \"ProgrammableVoice\""54}55],56"workflow": {57"name": "Sales",58"callback": "%(host)s/assignment",59"timeout": "15",60"routingConfiguration": [61{62"expression": "selected_product==\"ProgrammableSMS\"",63"targetTaskQueue": "SMS"64},65{66"expression": "selected_product==\"ProgrammableVoice\"",67"targetTaskQueue": "Voice"68}69]70}71}
In order to build a client for this API, we need as system variables a TWILIO_ACCOUNT_SID
and TWILIO_AUTH_TOKEN
which you can find on Twilio Console. The class TwilioAppSettings
creates a TwilioTaskRouterClient
, which is provided by the Twilio Java library. This client is used by WorkspaceFacade which encapsulates all logic related to the Workspace
class.
Let's take a look at a Gradle task that will handle the Workspace setup for us.
In this application the Gradle task createWorkspace
is used to orchestrate calls to our WorkspaceFacade
class in order to handle a Workspace. CreateWorkspaceTask
is the java main class behind the Gradle task. It uses data provided by workspace.json
and expects 3 arguments in the following order:
hostname
- A public URL to which Twilio can send requests. This can be either a cloud service or ngrok, which can expose a local application to the internet.bobPhone
- The telephone number of Bob, the Programmable SMS specialist.alicePhone
- Same for Alice, the Programmable Voice specialist.The function createWorkspaceConfig
is used to load the configuration of the workspace from workspace.json
.
src/main/java/com/twilio/taskrouter/application/CreateWorkspaceTask.java
1package com.twilio.taskrouter.application;23import com.google.inject.Guice;4import com.google.inject.Injector;5import com.twilio.rest.taskrouter.v1.workspace.Activity;6import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;7import com.twilio.rest.taskrouter.v1.workspace.Workflow;8import com.twilio.taskrouter.WorkflowRule;9import com.twilio.taskrouter.WorkflowRuleTarget;10import com.twilio.taskrouter.domain.common.TwilioAppSettings;11import com.twilio.taskrouter.domain.common.Utils;12import com.twilio.taskrouter.domain.error.TaskRouterException;13import com.twilio.taskrouter.domain.model.WorkspaceFacade;14import org.apache.commons.lang3.StringUtils;15import org.apache.commons.lang3.text.StrSubstitutor;1617import javax.json.Json;18import javax.json.JsonArray;19import javax.json.JsonObject;20import javax.json.JsonReader;21import java.io.File;22import java.io.IOException;23import java.io.StringReader;24import java.net.URISyntaxException;25import java.net.URL;26import java.util.Arrays;27import java.util.HashMap;28import java.util.List;29import java.util.Map;30import java.util.Optional;31import java.util.Properties;32import java.util.logging.Logger;33import java.util.stream.Collectors;3435import static java.lang.System.exit;3637//import org.apache.commons.lang3.StringUtils;38//import org.apache.commons.lang3.text.StrSubstitutor;3940/**41* Creates a workspace42*/43class CreateWorkspaceTask {4445private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());4647public static void main(String[] args) {4849System.out.println("Creating workspace...");50if (args.length < 3) {51System.out.println("You must specify 3 parameters:");52System.out.println("- Server hostname. E.g, <hash>.ngrok.com");53System.out.println("- Phone of the first agent (Bob)");54System.out.println("- Phone of the secondary agent (Alice)");55exit(1);56}5758String hostname = args[0];59String bobPhone = args[1];60String alicePhone = args[2];61System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",62hostname, bobPhone, alicePhone));6364//Get the configuration65JsonObject workspaceConfig = createWorkspaceConfig(args);6667//Get or Create the Workspace68Injector injector = Guice.createInjector();69final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);7071String workspaceName = workspaceConfig.getString("name");72Map<String, String> workspaceParams = new HashMap<>();73workspaceParams.put("FriendlyName", workspaceName);74workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));7576try {77WorkspaceFacade workspaceFacade = WorkspaceFacade78.create(twilioSettings.getTwilioRestClient(), workspaceParams);7980addWorkersToWorkspace(workspaceFacade, workspaceConfig);81addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);82Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);8384printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);85} catch (TaskRouterException e) {86LOG.severe(e.getMessage());87exit(1);88}89}9091public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,92JsonObject workspaceJsonConfig) {93JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");94Activity idleActivity = workspaceFacade.getIdleActivity();9596workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {97Map<String, String> workerParams = new HashMap<>();98workerParams.put("FriendlyName", workerJson.getString("name"));99workerParams.put("ActivitySid", idleActivity.getSid());100workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());101102try {103workspaceFacade.addWorker(workerParams);104} catch (TaskRouterException e) {105LOG.warning(e.getMessage());106}107});108}109110public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,111JsonObject workspaceJsonConfig) {112JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");113Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->114new TaskRouterException("The activity for reservations 'Reserved' was not found. "115+ "TaskQueues cannot be added."));116Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->117new TaskRouterException("The activity for assignments 'Busy' was not found. "118+ "TaskQueues cannot be added."));119taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {120Map<String, String> taskQueueParams = new HashMap<>();121taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));122taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));123taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());124taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());125126try {127workspaceFacade.addTaskQueue(taskQueueParams);128} catch (TaskRouterException e) {129LOG.warning(e.getMessage());130}131});132}133134public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,135JsonObject workspaceConfig) {136JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");137String workflowName = workflowJson.getString("name");138return workspaceFacade.findWorkflowByName(workflowName)139.orElseGet(() -> {140Map<String, String> workflowParams = new HashMap<>();141workflowParams.put("FriendlyName", workflowName);142workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));143workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));144workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));145146String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);147workflowParams.put("Configuration", workflowConfigJson);148149return workspaceFacade.addWorkflow(workflowParams);150});151}152153public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,154Workflow workflow,155TwilioAppSettings twilioSettings) {156Activity idleActivity = workspaceFacade.getIdleActivity();157158Properties workspaceParams = new Properties();159workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());160workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());161workspaceParams.put("workspace.sid", workspaceFacade.getSid());162workspaceParams.put("workflow.sid", workflow.getSid());163workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());164workspaceParams.put("email", twilioSettings.getEmail());165workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());166167File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);168169try {170Utils.saveProperties(workspaceParams,171workspacePropertiesFile,172"Properties for last created Twilio TaskRouter workspace");173} catch (IOException e) {174LOG.severe("Could not save workspace.properties with current configuration");175exit(1);176}177178String successMsg = String.format("Workspace '%s' was created successfully.",179workspaceFacade.getFriendlyName());180final int lineLength = successMsg.length() + 2;181182System.out.println(StringUtils.repeat("#", lineLength));183System.out.println(String.format(" %s ", successMsg));184System.out.println(StringUtils.repeat("#", lineLength));185System.out.println("The following variables were registered:");186System.out.println("\n");187workspaceParams.entrySet().stream().forEach(propertyEntry -> {188System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));189});190System.out.println("\n");191System.out.println(StringUtils.repeat("#", lineLength));192}193194public static JsonObject createWorkspaceConfig(String[] args) {195final String configFileName = "workspace.json";196197Optional<URL> url =198Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));199return url.map(u -> {200try {201File workspaceConfigJsonFile = new File(u.toURI());202String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);203String parsedContent = parseWorkspaceJsonContent(jsonContent, args);204205try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {206return jsonReader.readObject();207}208} catch (URISyntaxException e) {209throw new TaskRouterException(String.format("Wrong uri to find %s: %s",210configFileName, e.getMessage()));211} catch (IOException e) {212throw new TaskRouterException(String.format("Error while reading %s: %s",213configFileName, e.getMessage()));214}215}).orElseThrow(216() -> new TaskRouterException("There's no valid configuration in " + configFileName));217}218219private static String parseWorkspaceJsonContent(final String unparsedContent,220final String... args) {221Map<String, String> values = new HashMap<>();222values.put("host", args[0]);223values.put("bob_number", args[1]);224values.put("alice_number", args[2]);225226StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");227return strSubstitutor.replace(unparsedContent);228}229230public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,231JsonObject workflowJson) {232try {233JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");234TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")235.orElseThrow(() -> new TaskRouterException("Default queue not found"));236WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())237.expression("1=1")238.priority(1)239.timeout(30)240.build();241242List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()243.map(ruleJson -> {244String ruleQueueName = ruleJson.getString("targetTaskQueue");245TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(246() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));247248WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())249.priority(5)250.timeout(30)251.build();252253List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);254255return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();256}).collect(Collectors.toList());257258com.twilio.taskrouter.Workflow config;259config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);260return config.toJson();261} catch (Exception ex) {262throw new TaskRouterException("Error while creating workflow json configuration", ex);263}264}265}
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 identifier. In order to create a workspace we need to provide a FriendlyName
, and a EventCallbackUrl
which contains an URL to be called every time an event is triggered in the workspace.
src/main/java/com/twilio/taskrouter/domain/model/WorkspaceFacade.java
1package com.twilio.taskrouter.domain.model;23import com.fasterxml.jackson.databind.ObjectMapper;4import com.twilio.base.ResourceSet;5import com.twilio.http.TwilioRestClient;6import com.twilio.rest.taskrouter.v1.Workspace;7import com.twilio.rest.taskrouter.v1.WorkspaceCreator;8import com.twilio.rest.taskrouter.v1.WorkspaceDeleter;9import com.twilio.rest.taskrouter.v1.WorkspaceFetcher;10import com.twilio.rest.taskrouter.v1.WorkspaceReader;11import com.twilio.rest.taskrouter.v1.workspace.Activity;12import com.twilio.rest.taskrouter.v1.workspace.ActivityReader;13import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;14import com.twilio.rest.taskrouter.v1.workspace.TaskQueueCreator;15import com.twilio.rest.taskrouter.v1.workspace.TaskQueueReader;16import com.twilio.rest.taskrouter.v1.workspace.Worker;17import com.twilio.rest.taskrouter.v1.workspace.WorkerCreator;18import com.twilio.rest.taskrouter.v1.workspace.WorkerReader;19import com.twilio.rest.taskrouter.v1.workspace.WorkerUpdater;20import com.twilio.rest.taskrouter.v1.workspace.Workflow;21import com.twilio.rest.taskrouter.v1.workspace.WorkflowCreator;22import com.twilio.rest.taskrouter.v1.workspace.WorkflowReader;23import com.twilio.taskrouter.domain.error.TaskRouterException;2425import java.io.IOException;26import java.util.HashMap;27import java.util.Map;28import java.util.Optional;29import java.util.stream.StreamSupport;3031public class WorkspaceFacade {3233private final TwilioRestClient client;3435private final Workspace workspace;3637private Activity idleActivity;3839private Map<String, Worker> phoneToWorker;4041public WorkspaceFacade(TwilioRestClient client, Workspace workspace) {42this.client = client;43this.workspace = workspace;44}4546public static WorkspaceFacade create(TwilioRestClient client,47Map<String, String> params) {48String workspaceName = params.get("FriendlyName");49String eventCallbackUrl = params.get("EventCallbackUrl");5051ResourceSet<Workspace> execute = new WorkspaceReader()52.setFriendlyName(workspaceName)53.read(client);54StreamSupport.stream(execute.spliterator(), false)55.findFirst()56.ifPresent(workspace -> new WorkspaceDeleter(workspace.getSid()).delete(client));5758Workspace workspace = new WorkspaceCreator(workspaceName)59.setEventCallbackUrl(eventCallbackUrl)60.create(client);6162return new WorkspaceFacade(client, workspace);63}6465public static Optional<WorkspaceFacade> findBySid(String workspaceSid,66TwilioRestClient client) {67Workspace workspace = new WorkspaceFetcher(workspaceSid).fetch(client);68return Optional.of(new WorkspaceFacade(client, workspace));69}7071public String getFriendlyName() {72return workspace.getFriendlyName();73}7475public String getSid() {76return workspace.getSid();77}7879public Worker addWorker(Map<String, String> workerParams) {80return new WorkerCreator(workspace.getSid(), workerParams.get("FriendlyName"))81.setActivitySid(workerParams.get("ActivitySid"))82.setAttributes(workerParams.get("Attributes"))83.create(client);84}8586public void addTaskQueue(Map<String, String> taskQueueParams) {87new TaskQueueCreator(this.workspace.getSid(),88taskQueueParams.get("FriendlyName"),89taskQueueParams.get("ReservationActivitySid"),90taskQueueParams.get("AssignmentActivitySid"))91.create(client);92}9394public Workflow addWorkflow(Map<String, String> workflowParams) {95return new WorkflowCreator(workspace.getSid(),96workflowParams.get("FriendlyName"),97workflowParams.get("Configuration"))98.setAssignmentCallbackUrl(workflowParams.get("AssignmentCallbackUrl"))99.setFallbackAssignmentCallbackUrl(workflowParams.get("FallbackAssignmentCallbackUrl"))100.setTaskReservationTimeout(Integer.valueOf(workflowParams.get("TaskReservationTimeout")))101.create(client);102}103104public Optional<Activity> findActivityByName(String activityName) {105return StreamSupport.stream(new ActivityReader(this.workspace.getSid())106.setFriendlyName(activityName)107.read(client).spliterator(), false108).findFirst();109}110111public Optional<TaskQueue> findTaskQueueByName(String queueName) {112return StreamSupport.stream(new TaskQueueReader(this.workspace.getSid())113.setFriendlyName(queueName)114.read(client).spliterator(), false115).findFirst();116}117118public Optional<Workflow> findWorkflowByName(String workflowName) {119return StreamSupport.stream(new WorkflowReader(this.workspace.getSid())120.setFriendlyName(workflowName)121.read(client).spliterator(), false122).findFirst();123}124125public Optional<Worker> findWorkerByPhone(String workerPhone) {126return Optional.ofNullable(getPhoneToWorker().get(workerPhone));127}128129public Map<String, Worker> getPhoneToWorker() {130if (phoneToWorker == null) {131phoneToWorker = new HashMap<>();132StreamSupport.stream(133new WorkerReader(this.workspace.getSid()).read(client).spliterator(), false134).forEach(worker -> {135try {136HashMap<String, Object> attributes = new ObjectMapper()137.readValue(worker.getAttributes(), HashMap.class);138phoneToWorker.put(attributes.get("contact_uri").toString(), worker);139} catch (IOException e) {140throw new TaskRouterException(141String.format("'%s' has a malformed json attributes", worker.getFriendlyName()));142}143});144}145return phoneToWorker;146}147148public Activity getIdleActivity() {149if (idleActivity == null) {150idleActivity = findActivityByName("Idle").get();151}152return idleActivity;153}154155public void updateWorkerStatus(Worker worker, String activityFriendlyName) {156Activity activity = findActivityByName(activityFriendlyName).orElseThrow(() ->157new TaskRouterException(158String.format("The activity '%s' doesn't exist in the workspace", activityFriendlyName)159)160);161162new WorkerUpdater(workspace.getSid(), worker.getSid())163.setActivitySid(activity.getSid())164.update(client);165}166}
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.
Creating the Workers
1package com.twilio.taskrouter.application;23import com.google.inject.Guice;4import com.google.inject.Injector;5import com.twilio.rest.taskrouter.v1.workspace.Activity;6import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;7import com.twilio.rest.taskrouter.v1.workspace.Workflow;8import com.twilio.taskrouter.WorkflowRule;9import com.twilio.taskrouter.WorkflowRuleTarget;10import com.twilio.taskrouter.domain.common.TwilioAppSettings;11import com.twilio.taskrouter.domain.common.Utils;12import com.twilio.taskrouter.domain.error.TaskRouterException;13import com.twilio.taskrouter.domain.model.WorkspaceFacade;14import org.apache.commons.lang3.StringUtils;15import org.apache.commons.lang3.text.StrSubstitutor;1617import javax.json.Json;18import javax.json.JsonArray;19import javax.json.JsonObject;20import javax.json.JsonReader;21import java.io.File;22import java.io.IOException;23import java.io.StringReader;24import java.net.URISyntaxException;25import java.net.URL;26import java.util.Arrays;27import java.util.HashMap;28import java.util.List;29import java.util.Map;30import java.util.Optional;31import java.util.Properties;32import java.util.logging.Logger;33import java.util.stream.Collectors;3435import static java.lang.System.exit;3637//import org.apache.commons.lang3.StringUtils;38//import org.apache.commons.lang3.text.StrSubstitutor;3940/**41* Creates a workspace42*/43class CreateWorkspaceTask {4445private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());4647public static void main(String[] args) {4849System.out.println("Creating workspace...");50if (args.length < 3) {51System.out.println("You must specify 3 parameters:");52System.out.println("- Server hostname. E.g, <hash>.ngrok.com");53System.out.println("- Phone of the first agent (Bob)");54System.out.println("- Phone of the secondary agent (Alice)");55exit(1);56}5758String hostname = args[0];59String bobPhone = args[1];60String alicePhone = args[2];61System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",62hostname, bobPhone, alicePhone));6364//Get the configuration65JsonObject workspaceConfig = createWorkspaceConfig(args);6667//Get or Create the Workspace68Injector injector = Guice.createInjector();69final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);7071String workspaceName = workspaceConfig.getString("name");72Map<String, String> workspaceParams = new HashMap<>();73workspaceParams.put("FriendlyName", workspaceName);74workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));7576try {77WorkspaceFacade workspaceFacade = WorkspaceFacade78.create(twilioSettings.getTwilioRestClient(), workspaceParams);7980addWorkersToWorkspace(workspaceFacade, workspaceConfig);81addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);82Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);8384printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);85} catch (TaskRouterException e) {86LOG.severe(e.getMessage());87exit(1);88}89}9091public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,92JsonObject workspaceJsonConfig) {93JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");94Activity idleActivity = workspaceFacade.getIdleActivity();9596workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {97Map<String, String> workerParams = new HashMap<>();98workerParams.put("FriendlyName", workerJson.getString("name"));99workerParams.put("ActivitySid", idleActivity.getSid());100workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());101102try {103workspaceFacade.addWorker(workerParams);104} catch (TaskRouterException e) {105LOG.warning(e.getMessage());106}107});108}109110public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,111JsonObject workspaceJsonConfig) {112JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");113Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->114new TaskRouterException("The activity for reservations 'Reserved' was not found. "115+ "TaskQueues cannot be added."));116Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->117new TaskRouterException("The activity for assignments 'Busy' was not found. "118+ "TaskQueues cannot be added."));119taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {120Map<String, String> taskQueueParams = new HashMap<>();121taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));122taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));123taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());124taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());125126try {127workspaceFacade.addTaskQueue(taskQueueParams);128} catch (TaskRouterException e) {129LOG.warning(e.getMessage());130}131});132}133134public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,135JsonObject workspaceConfig) {136JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");137String workflowName = workflowJson.getString("name");138return workspaceFacade.findWorkflowByName(workflowName)139.orElseGet(() -> {140Map<String, String> workflowParams = new HashMap<>();141workflowParams.put("FriendlyName", workflowName);142workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));143workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));144workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));145146String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);147workflowParams.put("Configuration", workflowConfigJson);148149return workspaceFacade.addWorkflow(workflowParams);150});151}152153public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,154Workflow workflow,155TwilioAppSettings twilioSettings) {156Activity idleActivity = workspaceFacade.getIdleActivity();157158Properties workspaceParams = new Properties();159workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());160workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());161workspaceParams.put("workspace.sid", workspaceFacade.getSid());162workspaceParams.put("workflow.sid", workflow.getSid());163workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());164workspaceParams.put("email", twilioSettings.getEmail());165workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());166167File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);168169try {170Utils.saveProperties(workspaceParams,171workspacePropertiesFile,172"Properties for last created Twilio TaskRouter workspace");173} catch (IOException e) {174LOG.severe("Could not save workspace.properties with current configuration");175exit(1);176}177178String successMsg = String.format("Workspace '%s' was created successfully.",179workspaceFacade.getFriendlyName());180final int lineLength = successMsg.length() + 2;181182System.out.println(StringUtils.repeat("#", lineLength));183System.out.println(String.format(" %s ", successMsg));184System.out.println(StringUtils.repeat("#", lineLength));185System.out.println("The following variables were registered:");186System.out.println("\n");187workspaceParams.entrySet().stream().forEach(propertyEntry -> {188System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));189});190System.out.println("\n");191System.out.println(StringUtils.repeat("#", lineLength));192}193194public static JsonObject createWorkspaceConfig(String[] args) {195final String configFileName = "workspace.json";196197Optional<URL> url =198Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));199return url.map(u -> {200try {201File workspaceConfigJsonFile = new File(u.toURI());202String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);203String parsedContent = parseWorkspaceJsonContent(jsonContent, args);204205try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {206return jsonReader.readObject();207}208} catch (URISyntaxException e) {209throw new TaskRouterException(String.format("Wrong uri to find %s: %s",210configFileName, e.getMessage()));211} catch (IOException e) {212throw new TaskRouterException(String.format("Error while reading %s: %s",213configFileName, e.getMessage()));214}215}).orElseThrow(216() -> new TaskRouterException("There's no valid configuration in " + configFileName));217}218219private static String parseWorkspaceJsonContent(final String unparsedContent,220final String... args) {221Map<String, String> values = new HashMap<>();222values.put("host", args[0]);223values.put("bob_number", args[1]);224values.put("alice_number", args[2]);225226StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");227return strSubstitutor.replace(unparsedContent);228}229230public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,231JsonObject workflowJson) {232try {233JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");234TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")235.orElseThrow(() -> new TaskRouterException("Default queue not found"));236WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())237.expression("1=1")238.priority(1)239.timeout(30)240.build();241242List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()243.map(ruleJson -> {244String ruleQueueName = ruleJson.getString("targetTaskQueue");245TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(246() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));247248WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())249.priority(5)250.timeout(30)251.build();252253List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);254255return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();256}).collect(Collectors.toList());257258com.twilio.taskrouter.Workflow config;259config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);260return config.toJson();261} catch (Exception ex) {262throw new TaskRouterException("Error while creating workflow json configuration", ex);263}264}265}
After creating our workers, let's set up the Task Queues.
Next, we set up the Task Queues. Each with a name and a TargetWorkers
property, 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\""
.All
- This queue targets all users and can be used when there are no specialist around for the chosen product. We can use the "1==1"
expression here.Creating the Task Queues
1package com.twilio.taskrouter.application;23import com.google.inject.Guice;4import com.google.inject.Injector;5import com.twilio.rest.taskrouter.v1.workspace.Activity;6import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;7import com.twilio.rest.taskrouter.v1.workspace.Workflow;8import com.twilio.taskrouter.WorkflowRule;9import com.twilio.taskrouter.WorkflowRuleTarget;10import com.twilio.taskrouter.domain.common.TwilioAppSettings;11import com.twilio.taskrouter.domain.common.Utils;12import com.twilio.taskrouter.domain.error.TaskRouterException;13import com.twilio.taskrouter.domain.model.WorkspaceFacade;14import org.apache.commons.lang3.StringUtils;15import org.apache.commons.lang3.text.StrSubstitutor;1617import javax.json.Json;18import javax.json.JsonArray;19import javax.json.JsonObject;20import javax.json.JsonReader;21import java.io.File;22import java.io.IOException;23import java.io.StringReader;24import java.net.URISyntaxException;25import java.net.URL;26import java.util.Arrays;27import java.util.HashMap;28import java.util.List;29import java.util.Map;30import java.util.Optional;31import java.util.Properties;32import java.util.logging.Logger;33import java.util.stream.Collectors;3435import static java.lang.System.exit;3637//import org.apache.commons.lang3.StringUtils;38//import org.apache.commons.lang3.text.StrSubstitutor;3940/**41* Creates a workspace42*/43class CreateWorkspaceTask {4445private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());4647public static void main(String[] args) {4849System.out.println("Creating workspace...");50if (args.length < 3) {51System.out.println("You must specify 3 parameters:");52System.out.println("- Server hostname. E.g, <hash>.ngrok.com");53System.out.println("- Phone of the first agent (Bob)");54System.out.println("- Phone of the secondary agent (Alice)");55exit(1);56}5758String hostname = args[0];59String bobPhone = args[1];60String alicePhone = args[2];61System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",62hostname, bobPhone, alicePhone));6364//Get the configuration65JsonObject workspaceConfig = createWorkspaceConfig(args);6667//Get or Create the Workspace68Injector injector = Guice.createInjector();69final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);7071String workspaceName = workspaceConfig.getString("name");72Map<String, String> workspaceParams = new HashMap<>();73workspaceParams.put("FriendlyName", workspaceName);74workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));7576try {77WorkspaceFacade workspaceFacade = WorkspaceFacade78.create(twilioSettings.getTwilioRestClient(), workspaceParams);7980addWorkersToWorkspace(workspaceFacade, workspaceConfig);81addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);82Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);8384printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);85} catch (TaskRouterException e) {86LOG.severe(e.getMessage());87exit(1);88}89}9091public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,92JsonObject workspaceJsonConfig) {93JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");94Activity idleActivity = workspaceFacade.getIdleActivity();9596workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {97Map<String, String> workerParams = new HashMap<>();98workerParams.put("FriendlyName", workerJson.getString("name"));99workerParams.put("ActivitySid", idleActivity.getSid());100workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());101102try {103workspaceFacade.addWorker(workerParams);104} catch (TaskRouterException e) {105LOG.warning(e.getMessage());106}107});108}109110public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,111JsonObject workspaceJsonConfig) {112JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");113Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->114new TaskRouterException("The activity for reservations 'Reserved' was not found. "115+ "TaskQueues cannot be added."));116Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->117new TaskRouterException("The activity for assignments 'Busy' was not found. "118+ "TaskQueues cannot be added."));119taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {120Map<String, String> taskQueueParams = new HashMap<>();121taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));122taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));123taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());124taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());125126try {127workspaceFacade.addTaskQueue(taskQueueParams);128} catch (TaskRouterException e) {129LOG.warning(e.getMessage());130}131});132}133134public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,135JsonObject workspaceConfig) {136JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");137String workflowName = workflowJson.getString("name");138return workspaceFacade.findWorkflowByName(workflowName)139.orElseGet(() -> {140Map<String, String> workflowParams = new HashMap<>();141workflowParams.put("FriendlyName", workflowName);142workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));143workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));144workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));145146String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);147workflowParams.put("Configuration", workflowConfigJson);148149return workspaceFacade.addWorkflow(workflowParams);150});151}152153public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,154Workflow workflow,155TwilioAppSettings twilioSettings) {156Activity idleActivity = workspaceFacade.getIdleActivity();157158Properties workspaceParams = new Properties();159workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());160workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());161workspaceParams.put("workspace.sid", workspaceFacade.getSid());162workspaceParams.put("workflow.sid", workflow.getSid());163workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());164workspaceParams.put("email", twilioSettings.getEmail());165workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());166167File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);168169try {170Utils.saveProperties(workspaceParams,171workspacePropertiesFile,172"Properties for last created Twilio TaskRouter workspace");173} catch (IOException e) {174LOG.severe("Could not save workspace.properties with current configuration");175exit(1);176}177178String successMsg = String.format("Workspace '%s' was created successfully.",179workspaceFacade.getFriendlyName());180final int lineLength = successMsg.length() + 2;181182System.out.println(StringUtils.repeat("#", lineLength));183System.out.println(String.format(" %s ", successMsg));184System.out.println(StringUtils.repeat("#", lineLength));185System.out.println("The following variables were registered:");186System.out.println("\n");187workspaceParams.entrySet().stream().forEach(propertyEntry -> {188System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));189});190System.out.println("\n");191System.out.println(StringUtils.repeat("#", lineLength));192}193194public static JsonObject createWorkspaceConfig(String[] args) {195final String configFileName = "workspace.json";196197Optional<URL> url =198Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));199return url.map(u -> {200try {201File workspaceConfigJsonFile = new File(u.toURI());202String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);203String parsedContent = parseWorkspaceJsonContent(jsonContent, args);204205try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {206return jsonReader.readObject();207}208} catch (URISyntaxException e) {209throw new TaskRouterException(String.format("Wrong uri to find %s: %s",210configFileName, e.getMessage()));211} catch (IOException e) {212throw new TaskRouterException(String.format("Error while reading %s: %s",213configFileName, e.getMessage()));214}215}).orElseThrow(216() -> new TaskRouterException("There's no valid configuration in " + configFileName));217}218219private static String parseWorkspaceJsonContent(final String unparsedContent,220final String... args) {221Map<String, String> values = new HashMap<>();222values.put("host", args[0]);223values.put("bob_number", args[1]);224values.put("alice_number", args[2]);225226StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");227return strSubstitutor.replace(unparsedContent);228}229230public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,231JsonObject workflowJson) {232try {233JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");234TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")235.orElseThrow(() -> new TaskRouterException("Default queue not found"));236WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())237.expression("1=1")238.priority(1)239.timeout(30)240.build();241242List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()243.map(ruleJson -> {244String ruleQueueName = ruleJson.getString("targetTaskQueue");245TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(246() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));247248WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())249.priority(5)250.timeout(30)251.build();252253List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);254255return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();256}).collect(Collectors.toList());257258com.twilio.taskrouter.Workflow config;259config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);260return config.toJson();261} catch (Exception ex) {262throw new TaskRouterException("Error while creating workflow json configuration", ex);263}264}265}
We have a Workspace, Workers and Task Queues... what's left? A Workflow. Let's see how to create one next!
Finally, we set up the Workflow using the following parameters:
FriendlyName
as the name of a Workflow.
AssignmentCallbackUrl
as the public URL where a request will be made when this Workflow assigns a Task to a Worker. We will learn how to implement it on the next steps.
TaskReservationTimeout
as the maximum time we want to wait until a Worker handles a Task.
configuration
which is a set of rules for placing Task 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.Creating a Workflow
1package com.twilio.taskrouter.application;23import com.google.inject.Guice;4import com.google.inject.Injector;5import com.twilio.rest.taskrouter.v1.workspace.Activity;6import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;7import com.twilio.rest.taskrouter.v1.workspace.Workflow;8import com.twilio.taskrouter.WorkflowRule;9import com.twilio.taskrouter.WorkflowRuleTarget;10import com.twilio.taskrouter.domain.common.TwilioAppSettings;11import com.twilio.taskrouter.domain.common.Utils;12import com.twilio.taskrouter.domain.error.TaskRouterException;13import com.twilio.taskrouter.domain.model.WorkspaceFacade;14import org.apache.commons.lang3.StringUtils;15import org.apache.commons.lang3.text.StrSubstitutor;1617import javax.json.Json;18import javax.json.JsonArray;19import javax.json.JsonObject;20import javax.json.JsonReader;21import java.io.File;22import java.io.IOException;23import java.io.StringReader;24import java.net.URISyntaxException;25import java.net.URL;26import java.util.Arrays;27import java.util.HashMap;28import java.util.List;29import java.util.Map;30import java.util.Optional;31import java.util.Properties;32import java.util.logging.Logger;33import java.util.stream.Collectors;3435import static java.lang.System.exit;3637//import org.apache.commons.lang3.StringUtils;38//import org.apache.commons.lang3.text.StrSubstitutor;3940/**41* Creates a workspace42*/43class CreateWorkspaceTask {4445private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());4647public static void main(String[] args) {4849System.out.println("Creating workspace...");50if (args.length < 3) {51System.out.println("You must specify 3 parameters:");52System.out.println("- Server hostname. E.g, <hash>.ngrok.com");53System.out.println("- Phone of the first agent (Bob)");54System.out.println("- Phone of the secondary agent (Alice)");55exit(1);56}5758String hostname = args[0];59String bobPhone = args[1];60String alicePhone = args[2];61System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",62hostname, bobPhone, alicePhone));6364//Get the configuration65JsonObject workspaceConfig = createWorkspaceConfig(args);6667//Get or Create the Workspace68Injector injector = Guice.createInjector();69final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);7071String workspaceName = workspaceConfig.getString("name");72Map<String, String> workspaceParams = new HashMap<>();73workspaceParams.put("FriendlyName", workspaceName);74workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));7576try {77WorkspaceFacade workspaceFacade = WorkspaceFacade78.create(twilioSettings.getTwilioRestClient(), workspaceParams);7980addWorkersToWorkspace(workspaceFacade, workspaceConfig);81addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);82Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);8384printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);85} catch (TaskRouterException e) {86LOG.severe(e.getMessage());87exit(1);88}89}9091public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,92JsonObject workspaceJsonConfig) {93JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");94Activity idleActivity = workspaceFacade.getIdleActivity();9596workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {97Map<String, String> workerParams = new HashMap<>();98workerParams.put("FriendlyName", workerJson.getString("name"));99workerParams.put("ActivitySid", idleActivity.getSid());100workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());101102try {103workspaceFacade.addWorker(workerParams);104} catch (TaskRouterException e) {105LOG.warning(e.getMessage());106}107});108}109110public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,111JsonObject workspaceJsonConfig) {112JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");113Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->114new TaskRouterException("The activity for reservations 'Reserved' was not found. "115+ "TaskQueues cannot be added."));116Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->117new TaskRouterException("The activity for assignments 'Busy' was not found. "118+ "TaskQueues cannot be added."));119taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {120Map<String, String> taskQueueParams = new HashMap<>();121taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));122taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));123taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());124taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());125126try {127workspaceFacade.addTaskQueue(taskQueueParams);128} catch (TaskRouterException e) {129LOG.warning(e.getMessage());130}131});132}133134public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,135JsonObject workspaceConfig) {136JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");137String workflowName = workflowJson.getString("name");138return workspaceFacade.findWorkflowByName(workflowName)139.orElseGet(() -> {140Map<String, String> workflowParams = new HashMap<>();141workflowParams.put("FriendlyName", workflowName);142workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));143workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));144workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));145146String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);147workflowParams.put("Configuration", workflowConfigJson);148149return workspaceFacade.addWorkflow(workflowParams);150});151}152153public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,154Workflow workflow,155TwilioAppSettings twilioSettings) {156Activity idleActivity = workspaceFacade.getIdleActivity();157158Properties workspaceParams = new Properties();159workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());160workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());161workspaceParams.put("workspace.sid", workspaceFacade.getSid());162workspaceParams.put("workflow.sid", workflow.getSid());163workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());164workspaceParams.put("email", twilioSettings.getEmail());165workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());166167File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);168169try {170Utils.saveProperties(workspaceParams,171workspacePropertiesFile,172"Properties for last created Twilio TaskRouter workspace");173} catch (IOException e) {174LOG.severe("Could not save workspace.properties with current configuration");175exit(1);176}177178String successMsg = String.format("Workspace '%s' was created successfully.",179workspaceFacade.getFriendlyName());180final int lineLength = successMsg.length() + 2;181182System.out.println(StringUtils.repeat("#", lineLength));183System.out.println(String.format(" %s ", successMsg));184System.out.println(StringUtils.repeat("#", lineLength));185System.out.println("The following variables were registered:");186System.out.println("\n");187workspaceParams.entrySet().stream().forEach(propertyEntry -> {188System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));189});190System.out.println("\n");191System.out.println(StringUtils.repeat("#", lineLength));192}193194public static JsonObject createWorkspaceConfig(String[] args) {195final String configFileName = "workspace.json";196197Optional<URL> url =198Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));199return url.map(u -> {200try {201File workspaceConfigJsonFile = new File(u.toURI());202String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);203String parsedContent = parseWorkspaceJsonContent(jsonContent, args);204205try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {206return jsonReader.readObject();207}208} catch (URISyntaxException e) {209throw new TaskRouterException(String.format("Wrong uri to find %s: %s",210configFileName, e.getMessage()));211} catch (IOException e) {212throw new TaskRouterException(String.format("Error while reading %s: %s",213configFileName, e.getMessage()));214}215}).orElseThrow(216() -> new TaskRouterException("There's no valid configuration in " + configFileName));217}218219private static String parseWorkspaceJsonContent(final String unparsedContent,220final String... args) {221Map<String, String> values = new HashMap<>();222values.put("host", args[0]);223values.put("bob_number", args[1]);224values.put("alice_number", args[2]);225226StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");227return strSubstitutor.replace(unparsedContent);228}229230public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,231JsonObject workflowJson) {232try {233JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");234TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")235.orElseThrow(() -> new TaskRouterException("Default queue not found"));236WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())237.expression("1=1")238.priority(1)239.timeout(30)240.build();241242List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()243.map(ruleJson -> {244String ruleQueueName = ruleJson.getString("targetTaskQueue");245TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(246() -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));247248WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())249.priority(5)250.timeout(30)251.build();252253List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);254255return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();256}).collect(Collectors.toList());257258com.twilio.taskrouter.Workflow config;259config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);260return config.toJson();261} catch (Exception ex) {262throw new TaskRouterException("Error while creating workflow json configuration", ex);263}264}265}
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.
src/main/java/com/twilio/taskrouter/application/servlet/IncomingCallServlet.java
1package com.twilio.taskrouter.application.servlet;234import com.twilio.twiml.Gather;5import com.twilio.twiml.Method;6import com.twilio.twiml.Say;7import com.twilio.twiml.TwiMLException;8import com.twilio.twiml.VoiceResponse;910import javax.inject.Singleton;11import javax.servlet.ServletException;12import javax.servlet.http.HttpServlet;13import javax.servlet.http.HttpServletRequest;14import javax.servlet.http.HttpServletResponse;15import java.io.IOException;16import java.util.logging.Level;17import java.util.logging.Logger;1819/**20* Returns TwiML instructions to TwilioAppSettings's POST requests21*/22@Singleton23public class IncomingCallServlet extends HttpServlet {2425private static final Logger LOG = Logger.getLogger(IncomingCallServlet.class.getName());2627@Override28public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,29IOException {30try {31final VoiceResponse twimlResponse = new VoiceResponse.Builder()32.gather(new Gather.Builder()33.action("/call/enqueue")34.numDigits(1)35.timeout(10)36.method(Method.POST)37.say(new Say38.Builder("For Programmable SMS, press one. For Voice, press any other key.")39.build()40)41.build()42).build();4344resp.setContentType("application/xml");45resp.getWriter().print(twimlResponse.toXml());46} catch (TwiMLException e) {47LOG.log(Level.SEVERE, "Unexpected error while creating incoming call response", e);48throw new RuntimeException(e);49}50}5152}
We just asked the caller to choose a product, next we will use their choice to create the appropriate Task.
This is the endpoint set as the action
URL on the Gather
verb on the previous step. A request is made to this endpoint when the user presses a key during the call. This request has a Digits
parameter that holds the pressed keys. A Task
will be created based on the pressed digit with the selected_product
as an attribute. The Workflow will take this Task's attributes and make a match with the configured expressions in order to find a corresponding Task Queue, 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.
src/main/java/com/twilio/taskrouter/application/servlet/EnqueueServlet.java
1package com.twilio.taskrouter.application.servlet;23import com.twilio.taskrouter.domain.common.TwilioAppSettings;4import com.twilio.twiml.EnqueueTask;5import com.twilio.twiml.Task;6import com.twilio.twiml.TwiMLException;7import com.twilio.twiml.VoiceResponse;89import javax.inject.Inject;10import javax.inject.Singleton;11import javax.servlet.ServletException;12import javax.servlet.http.HttpServlet;13import javax.servlet.http.HttpServletRequest;14import javax.servlet.http.HttpServletResponse;15import java.io.IOException;16import java.util.Optional;17import java.util.logging.Level;18import java.util.logging.Logger;1920import static java.lang.String.format;2122/**23* Selects a product by creating a Task on the TaskRouter Workflow24*/25@Singleton26public class EnqueueServlet extends HttpServlet {2728private static final Logger LOG = Logger.getLogger(EnqueueServlet.class.getName());2930private final String workflowSid;3132@Inject33public EnqueueServlet(TwilioAppSettings twilioSettings) {34this.workflowSid = twilioSettings.getWorkflowSid();35}3637@Override38public void doPost(HttpServletRequest req, HttpServletResponse resp)39throws ServletException, IOException {4041String selectedProduct = getSelectedProduct(req);42Task task = new Task.Builder()43.data(format("{\"selected_product\": \"%s\"}", selectedProduct))44.build();4546EnqueueTask enqueueTask = new EnqueueTask.Builder(task).workflowSid(workflowSid).build();4748VoiceResponse voiceResponse = new VoiceResponse.Builder().enqueue(enqueueTask).build();49resp.setContentType("application/xml");50try {51resp.getWriter().print(voiceResponse.toXml());52} catch (TwiMLException e) {53LOG.log(Level.SEVERE, e.getMessage(), e);54throw new RuntimeException(e);55}56}5758private String getSelectedProduct(HttpServletRequest request) {59return Optional.ofNullable(request.getParameter("Digits"))60.filter(x -> x.equals("1")).map((first) -> "ProgrammableSMS").orElse("ProgrammableVoice");61}62}
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:
reserved
POST
request is made to the Workflow's AssignmentCallbackURL, which was configured using the Gradle task createWorkspace
This request includes the full details of the Task, the selected Worker, and the Reservation.
Handling this Assignment Callback is a key component of building a TaskRouter application as we can instruct how the Worker will handle a Task. We could send a text, email, push notifications or make a call.
Since we created this Task during a voice call with an Enqueue
verb, lets instruct TaskRouter to dequeue the call and dial a Worker. If we do not specify a to
parameter with a phone number, TaskRouter will pick the Worker's contact_uri
attribute.
We also send a post_work_activity_sid
which will tell TaskRouter which Activity to assign this worker after the call ends.
src/main/java/com/twilio/taskrouter/application/servlet/AssignmentServlet.java
1package com.twilio.taskrouter.application.servlet;23import com.twilio.taskrouter.domain.common.TwilioAppSettings;45import javax.inject.Inject;6import javax.inject.Singleton;7import javax.json.Json;8import javax.servlet.ServletException;9import javax.servlet.http.HttpServlet;10import javax.servlet.http.HttpServletRequest;11import javax.servlet.http.HttpServletResponse;12import java.io.IOException;1314/**15* Servlet for Task assignments16*/17@Singleton18public class AssignmentServlet extends HttpServlet {1920private final String dequeueInstruction;2122@Inject23public AssignmentServlet(TwilioAppSettings twilioAppSettings) {24dequeueInstruction = Json.createObjectBuilder()25.add("instruction", "dequeue")26.add("post_work_activity_sid", twilioAppSettings.getPostWorkActivitySid())27.build().toString();28}2930@Override31public void doPost(HttpServletRequest req, HttpServletResponse resp)32throws ServletException, IOException {33resp.setContentType("application/json");34resp.getWriter().print(dequeueInstruction);35}36}
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.
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.
src/main/java/com/twilio/taskrouter/application/servlet/EventsServlet.java
1package com.twilio.taskrouter.application.servlet;23import com.google.inject.persist.Transactional;4import com.twilio.rest.api.v2010.account.MessageCreator;5import com.twilio.taskrouter.domain.common.TwilioAppSettings;6import com.twilio.taskrouter.domain.model.MissedCall;7import com.twilio.taskrouter.domain.repository.MissedCallRepository;8import com.twilio.type.PhoneNumber;910import javax.inject.Inject;11import javax.inject.Singleton;12import javax.json.Json;13import javax.json.JsonObject;14import javax.servlet.ServletException;15import javax.servlet.http.HttpServlet;16import javax.servlet.http.HttpServletRequest;17import javax.servlet.http.HttpServletResponse;18import java.io.IOException;19import java.io.StringReader;20import java.util.Optional;21import java.util.logging.Logger;2223/**24* Servlet for Events callback for missed calls25*/26@Singleton27public class EventsServlet extends HttpServlet {2829private static final String LEAVE_MSG = "Sorry, All agents are busy. Please leave a message. "30+ "We will call you as soon as possible";3132private static final String OFFLINE_MSG = "Your status has changed to Offline. "33+ "Reply with \"On\" to get back Online";3435private static final Logger LOG = Logger.getLogger(EventsServlet.class.getName());3637private final TwilioAppSettings twilioSettings;3839private final MissedCallRepository missedCallRepository;4041@Inject42public EventsServlet(TwilioAppSettings twilioSettings,43MissedCallRepository missedCallRepository) {44this.twilioSettings = twilioSettings;45this.missedCallRepository = missedCallRepository;46}4748@Override49public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,50IOException {51Optional.ofNullable(req.getParameter("EventType"))52.ifPresent(eventName -> {53switch (eventName) {54case "workflow.timeout":55case "task.canceled":56parseAttributes("TaskAttributes", req)57.ifPresent(this::addMissingCallAndLeaveMessage);58break;59case "worker.activity.update":60Optional.ofNullable(req.getParameter("WorkerActivityName"))61.filter("Offline"::equals)62.ifPresent(offlineEvent -> {63parseAttributes("WorkerAttributes", req)64.ifPresent(this::notifyOfflineStatusToWorker);65});66break;67default:68}69});70}7172private Optional<JsonObject> parseAttributes(String parameter, HttpServletRequest request) {73return Optional.ofNullable(request.getParameter(parameter))74.map(jsonRequest -> Json.createReader(new StringReader(jsonRequest)).readObject());75}7677@Transactional78private void addMissingCallAndLeaveMessage(JsonObject taskAttributesJson) {79String phoneNumber = taskAttributesJson.getString("from");80String selectedProduct = taskAttributesJson.getString("selected_product");8182MissedCall missedCall = new MissedCall(phoneNumber, selectedProduct);83missedCallRepository.add(missedCall);84LOG.info("Added Missing Call: " + missedCall);8586String callSid = taskAttributesJson.getString("call_sid");87twilioSettings.redirectToVoiceMail(callSid, LEAVE_MSG);88}8990private void notifyOfflineStatusToWorker(JsonObject workerAttributesJson) {91String workerPhone = workerAttributesJson.getString("contact_uri");92new MessageCreator(93new PhoneNumber(workerPhone),94new PhoneNumber(twilioSettings.getPhoneNumber().toString()),95OFFLINE_MSG96).create();9798}99100}
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.
src/main/java/com/twilio/taskrouter/application/servlet/MessageServlet.java
1package com.twilio.taskrouter.application.servlet;23import com.google.inject.Singleton;4import com.twilio.taskrouter.domain.model.WorkspaceFacade;5import com.twilio.twiml.Sms;6import com.twilio.twiml.TwiMLException;7import com.twilio.twiml.VoiceResponse;89import javax.inject.Inject;10import javax.servlet.ServletException;11import javax.servlet.http.HttpServlet;12import javax.servlet.http.HttpServletRequest;13import javax.servlet.http.HttpServletResponse;14import java.io.IOException;15import java.util.Optional;16import java.util.logging.Level;17import java.util.logging.Logger;1819/**20* Handles the messages sent by workers for activate/deactivate21* themselves for receiving calls from users22*/23@Singleton24public class MessageServlet extends HttpServlet {2526private static final Logger LOG = Logger.getLogger(MessageServlet.class.getName());2728private final WorkspaceFacade workspace;2930@Inject31public MessageServlet(WorkspaceFacade workspace) {32this.workspace = workspace;33}3435@Override36protected void doPost(HttpServletRequest req, HttpServletResponse resp)37throws ServletException, IOException {38final VoiceResponse twimlResponse;39final String newStatus = getNewWorkerStatus(req);40final String workerPhone = req.getParameter("From");4142try {43Sms responseSms = workspace.findWorkerByPhone(workerPhone).map(worker -> {44workspace.updateWorkerStatus(worker, newStatus);45return new Sms.Builder(String.format("Your status has changed to %s", newStatus)).build();46}).orElseGet(() -> new Sms.Builder("You are not a valid worker").build());4748twimlResponse = new VoiceResponse.Builder().sms(responseSms).build();49resp.setContentType("application/xml");50resp.getWriter().print(twimlResponse.toXml());5152} catch (TwiMLException e) {53LOG.log(Level.SEVERE, "Error while providing answer to a workers' sms", e);54}5556}5758private String getNewWorkerStatus(HttpServletRequest request) {59return Optional.ofNullable(request.getParameter("Body"))60.filter(x -> x.equals("off")).map((first) -> "Offline").orElse("Idle");61}6263}
Congratulations! You finished this tutorial. As you can see, using Twilio's TaskRouter is quite simple.
If you're a Java developer working with Twilio, you might enjoy these other tutorials:
An example application implementing Click to Call using Twilio.
Instantly collect structured data from your users with a survey conducted over a call or SMS text messages.