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
_71{_71 "name": "Twilio Workspace",_71 "event_callback": "%(host)s/events",_71 "workers": [_71 {_71 "name": "Bob",_71 "attributes": {_71 "products": [_71 "ProgrammableSMS"_71 ],_71 "contact_uri": "%(bob_number)s"_71 }_71 },_71 {_71 "name": "Alice",_71 "attributes": {_71 "products": [_71 "ProgrammableVoice"_71 ],_71 "contact_uri": "%(alice_number)s"_71 }_71 }_71 ],_71 "activities": [_71 {_71 "name": "Offline",_71 "availability": "false"_71 },_71 {_71 "name": "Idle",_71 "availability": "true"_71 },_71 {_71 "name": "Busy",_71 "availability": "false"_71 },_71 {_71 "name": "Reserved",_71 "availability": "false"_71 }_71 ],_71 "task_queues": [_71 {_71 "name": "Default",_71 "targetWorkers": "1==1"_71 },_71 {_71 "name": "SMS",_71 "targetWorkers": "products HAS \"ProgrammableSMS\""_71 },_71 {_71 "name": "Voice",_71 "targetWorkers": "products HAS \"ProgrammableVoice\""_71 }_71 ],_71 "workflow": {_71 "name": "Sales",_71 "callback": "%(host)s/assignment",_71 "timeout": "15",_71 "routingConfiguration": [_71 {_71 "expression": "selected_product==\"ProgrammableSMS\"",_71 "targetTaskQueue": "SMS"_71 },_71 {_71 "expression": "selected_product==\"ProgrammableVoice\"",_71 "targetTaskQueue": "Voice"_71 }_71 ]_71 }_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
_265package com.twilio.taskrouter.application;_265_265import com.google.inject.Guice;_265import com.google.inject.Injector;_265import com.twilio.rest.taskrouter.v1.workspace.Activity;_265import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;_265import com.twilio.rest.taskrouter.v1.workspace.Workflow;_265import com.twilio.taskrouter.WorkflowRule;_265import com.twilio.taskrouter.WorkflowRuleTarget;_265import com.twilio.taskrouter.domain.common.TwilioAppSettings;_265import com.twilio.taskrouter.domain.common.Utils;_265import com.twilio.taskrouter.domain.error.TaskRouterException;_265import com.twilio.taskrouter.domain.model.WorkspaceFacade;_265import org.apache.commons.lang3.StringUtils;_265import org.apache.commons.lang3.text.StrSubstitutor;_265_265import javax.json.Json;_265import javax.json.JsonArray;_265import javax.json.JsonObject;_265import javax.json.JsonReader;_265import java.io.File;_265import java.io.IOException;_265import java.io.StringReader;_265import java.net.URISyntaxException;_265import java.net.URL;_265import java.util.Arrays;_265import java.util.HashMap;_265import java.util.List;_265import java.util.Map;_265import java.util.Optional;_265import java.util.Properties;_265import java.util.logging.Logger;_265import java.util.stream.Collectors;_265_265import static java.lang.System.exit;_265_265//import org.apache.commons.lang3.StringUtils;_265//import org.apache.commons.lang3.text.StrSubstitutor;_265_265/**_265 * Creates a workspace_265 */_265class CreateWorkspaceTask {_265_265 private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());_265_265 public static void main(String[] args) {_265_265 System.out.println("Creating workspace...");_265 if (args.length < 3) {_265 System.out.println("You must specify 3 parameters:");_265 System.out.println("- Server hostname. E.g, <hash>.ngrok.com");_265 System.out.println("- Phone of the first agent (Bob)");_265 System.out.println("- Phone of the secondary agent (Alice)");_265 exit(1);_265 }_265_265 String hostname = args[0];_265 String bobPhone = args[1];_265 String alicePhone = args[2];_265 System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",_265 hostname, bobPhone, alicePhone));_265_265 //Get the configuration_265 JsonObject workspaceConfig = createWorkspaceConfig(args);_265_265 //Get or Create the Workspace_265 Injector injector = Guice.createInjector();_265 final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);_265_265 String workspaceName = workspaceConfig.getString("name");_265 Map<String, String> workspaceParams = new HashMap<>();_265 workspaceParams.put("FriendlyName", workspaceName);_265 workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));_265_265 try {_265 WorkspaceFacade workspaceFacade = WorkspaceFacade_265 .create(twilioSettings.getTwilioRestClient(), workspaceParams);_265_265 addWorkersToWorkspace(workspaceFacade, workspaceConfig);_265 addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);_265 Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);_265_265 printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);_265 } catch (TaskRouterException e) {_265 LOG.severe(e.getMessage());_265 exit(1);_265 }_265 }_265_265 public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {_265 Map<String, String> workerParams = new HashMap<>();_265 workerParams.put("FriendlyName", workerJson.getString("name"));_265 workerParams.put("ActivitySid", idleActivity.getSid());_265 workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());_265_265 try {_265 workspaceFacade.addWorker(workerParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");_265 Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->_265 new TaskRouterException("The activity for reservations 'Reserved' was not found. "_265 + "TaskQueues cannot be added."));_265 Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->_265 new TaskRouterException("The activity for assignments 'Busy' was not found. "_265 + "TaskQueues cannot be added."));_265 taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {_265 Map<String, String> taskQueueParams = new HashMap<>();_265 taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));_265 taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));_265 taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());_265 taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());_265_265 try {_265 workspaceFacade.addTaskQueue(taskQueueParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceConfig) {_265 JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");_265 String workflowName = workflowJson.getString("name");_265 return workspaceFacade.findWorkflowByName(workflowName)_265 .orElseGet(() -> {_265 Map<String, String> workflowParams = new HashMap<>();_265 workflowParams.put("FriendlyName", workflowName);_265 workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));_265_265 String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);_265 workflowParams.put("Configuration", workflowConfigJson);_265_265 return workspaceFacade.addWorkflow(workflowParams);_265 });_265 }_265_265 public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,_265 Workflow workflow,_265 TwilioAppSettings twilioSettings) {_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 Properties workspaceParams = new Properties();_265 workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());_265 workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());_265 workspaceParams.put("workspace.sid", workspaceFacade.getSid());_265 workspaceParams.put("workflow.sid", workflow.getSid());_265 workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());_265 workspaceParams.put("email", twilioSettings.getEmail());_265 workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());_265_265 File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);_265_265 try {_265 Utils.saveProperties(workspaceParams,_265 workspacePropertiesFile,_265 "Properties for last created Twilio TaskRouter workspace");_265 } catch (IOException e) {_265 LOG.severe("Could not save workspace.properties with current configuration");_265 exit(1);_265 }_265_265 String successMsg = String.format("Workspace '%s' was created successfully.",_265 workspaceFacade.getFriendlyName());_265 final int lineLength = successMsg.length() + 2;_265_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println(String.format(" %s ", successMsg));_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println("The following variables were registered:");_265 System.out.println("\n");_265 workspaceParams.entrySet().stream().forEach(propertyEntry -> {_265 System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));_265 });_265 System.out.println("\n");_265 System.out.println(StringUtils.repeat("#", lineLength));_265 }_265_265 public static JsonObject createWorkspaceConfig(String[] args) {_265 final String configFileName = "workspace.json";_265_265 Optional<URL> url =_265 Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));_265 return url.map(u -> {_265 try {_265 File workspaceConfigJsonFile = new File(u.toURI());_265 String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);_265 String parsedContent = parseWorkspaceJsonContent(jsonContent, args);_265_265 try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {_265 return jsonReader.readObject();_265 }_265 } catch (URISyntaxException e) {_265 throw new TaskRouterException(String.format("Wrong uri to find %s: %s",_265 configFileName, e.getMessage()));_265 } catch (IOException e) {_265 throw new TaskRouterException(String.format("Error while reading %s: %s",_265 configFileName, e.getMessage()));_265 }_265 }).orElseThrow(_265 () -> new TaskRouterException("There's no valid configuration in " + configFileName));_265 }_265_265 private static String parseWorkspaceJsonContent(final String unparsedContent,_265 final String... args) {_265 Map<String, String> values = new HashMap<>();_265 values.put("host", args[0]);_265 values.put("bob_number", args[1]);_265 values.put("alice_number", args[2]);_265_265 StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");_265 return strSubstitutor.replace(unparsedContent);_265 }_265_265 public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,_265 JsonObject workflowJson) {_265 try {_265 JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");_265 TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")_265 .orElseThrow(() -> new TaskRouterException("Default queue not found"));_265 WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())_265 .expression("1=1")_265 .priority(1)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()_265 .map(ruleJson -> {_265 String ruleQueueName = ruleJson.getString("targetTaskQueue");_265 TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(_265 () -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));_265_265 WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())_265 .priority(5)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);_265_265 return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();_265 }).collect(Collectors.toList());_265_265 com.twilio.taskrouter.Workflow config;_265 config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);_265 return config.toJson();_265 } catch (Exception ex) {_265 throw new TaskRouterException("Error while creating workflow json configuration", ex);_265 }_265 }_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
_166package com.twilio.taskrouter.domain.model;_166_166import com.fasterxml.jackson.databind.ObjectMapper;_166import com.twilio.base.ResourceSet;_166import com.twilio.http.TwilioRestClient;_166import com.twilio.rest.taskrouter.v1.Workspace;_166import com.twilio.rest.taskrouter.v1.WorkspaceCreator;_166import com.twilio.rest.taskrouter.v1.WorkspaceDeleter;_166import com.twilio.rest.taskrouter.v1.WorkspaceFetcher;_166import com.twilio.rest.taskrouter.v1.WorkspaceReader;_166import com.twilio.rest.taskrouter.v1.workspace.Activity;_166import com.twilio.rest.taskrouter.v1.workspace.ActivityReader;_166import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;_166import com.twilio.rest.taskrouter.v1.workspace.TaskQueueCreator;_166import com.twilio.rest.taskrouter.v1.workspace.TaskQueueReader;_166import com.twilio.rest.taskrouter.v1.workspace.Worker;_166import com.twilio.rest.taskrouter.v1.workspace.WorkerCreator;_166import com.twilio.rest.taskrouter.v1.workspace.WorkerReader;_166import com.twilio.rest.taskrouter.v1.workspace.WorkerUpdater;_166import com.twilio.rest.taskrouter.v1.workspace.Workflow;_166import com.twilio.rest.taskrouter.v1.workspace.WorkflowCreator;_166import com.twilio.rest.taskrouter.v1.workspace.WorkflowReader;_166import com.twilio.taskrouter.domain.error.TaskRouterException;_166_166import java.io.IOException;_166import java.util.HashMap;_166import java.util.Map;_166import java.util.Optional;_166import java.util.stream.StreamSupport;_166_166public class WorkspaceFacade {_166_166 private final TwilioRestClient client;_166_166 private final Workspace workspace;_166_166 private Activity idleActivity;_166_166 private Map<String, Worker> phoneToWorker;_166_166 public WorkspaceFacade(TwilioRestClient client, Workspace workspace) {_166 this.client = client;_166 this.workspace = workspace;_166 }_166_166 public static WorkspaceFacade create(TwilioRestClient client,_166 Map<String, String> params) {_166 String workspaceName = params.get("FriendlyName");_166 String eventCallbackUrl = params.get("EventCallbackUrl");_166_166 ResourceSet<Workspace> execute = new WorkspaceReader()_166 .setFriendlyName(workspaceName)_166 .read(client);_166 StreamSupport.stream(execute.spliterator(), false)_166 .findFirst()_166 .ifPresent(workspace -> new WorkspaceDeleter(workspace.getSid()).delete(client));_166_166 Workspace workspace = new WorkspaceCreator(workspaceName)_166 .setEventCallbackUrl(eventCallbackUrl)_166 .create(client);_166_166 return new WorkspaceFacade(client, workspace);_166 }_166_166 public static Optional<WorkspaceFacade> findBySid(String workspaceSid,_166 TwilioRestClient client) {_166 Workspace workspace = new WorkspaceFetcher(workspaceSid).fetch(client);_166 return Optional.of(new WorkspaceFacade(client, workspace));_166 }_166_166 public String getFriendlyName() {_166 return workspace.getFriendlyName();_166 }_166_166 public String getSid() {_166 return workspace.getSid();_166 }_166_166 public Worker addWorker(Map<String, String> workerParams) {_166 return new WorkerCreator(workspace.getSid(), workerParams.get("FriendlyName"))_166 .setActivitySid(workerParams.get("ActivitySid"))_166 .setAttributes(workerParams.get("Attributes"))_166 .create(client);_166 }_166_166 public void addTaskQueue(Map<String, String> taskQueueParams) {_166 new TaskQueueCreator(this.workspace.getSid(),_166 taskQueueParams.get("FriendlyName"),_166 taskQueueParams.get("ReservationActivitySid"),_166 taskQueueParams.get("AssignmentActivitySid"))_166 .create(client);_166 }_166_166 public Workflow addWorkflow(Map<String, String> workflowParams) {_166 return new WorkflowCreator(workspace.getSid(),_166 workflowParams.get("FriendlyName"),_166 workflowParams.get("Configuration"))_166 .setAssignmentCallbackUrl(workflowParams.get("AssignmentCallbackUrl"))_166 .setFallbackAssignmentCallbackUrl(workflowParams.get("FallbackAssignmentCallbackUrl"))_166 .setTaskReservationTimeout(Integer.valueOf(workflowParams.get("TaskReservationTimeout")))_166 .create(client);_166 }_166_166 public Optional<Activity> findActivityByName(String activityName) {_166 return StreamSupport.stream(new ActivityReader(this.workspace.getSid())_166 .setFriendlyName(activityName)_166 .read(client).spliterator(), false_166 ).findFirst();_166 }_166_166 public Optional<TaskQueue> findTaskQueueByName(String queueName) {_166 return StreamSupport.stream(new TaskQueueReader(this.workspace.getSid())_166 .setFriendlyName(queueName)_166 .read(client).spliterator(), false_166 ).findFirst();_166 }_166_166 public Optional<Workflow> findWorkflowByName(String workflowName) {_166 return StreamSupport.stream(new WorkflowReader(this.workspace.getSid())_166 .setFriendlyName(workflowName)_166 .read(client).spliterator(), false_166 ).findFirst();_166 }_166_166 public Optional<Worker> findWorkerByPhone(String workerPhone) {_166 return Optional.ofNullable(getPhoneToWorker().get(workerPhone));_166 }_166_166 public Map<String, Worker> getPhoneToWorker() {_166 if (phoneToWorker == null) {_166 phoneToWorker = new HashMap<>();_166 StreamSupport.stream(_166 new WorkerReader(this.workspace.getSid()).read(client).spliterator(), false_166 ).forEach(worker -> {_166 try {_166 HashMap<String, Object> attributes = new ObjectMapper()_166 .readValue(worker.getAttributes(), HashMap.class);_166 phoneToWorker.put(attributes.get("contact_uri").toString(), worker);_166 } catch (IOException e) {_166 throw new TaskRouterException(_166 String.format("'%s' has a malformed json attributes", worker.getFriendlyName()));_166 }_166 });_166 }_166 return phoneToWorker;_166 }_166_166 public Activity getIdleActivity() {_166 if (idleActivity == null) {_166 idleActivity = findActivityByName("Idle").get();_166 }_166 return idleActivity;_166 }_166_166 public void updateWorkerStatus(Worker worker, String activityFriendlyName) {_166 Activity activity = findActivityByName(activityFriendlyName).orElseThrow(() ->_166 new TaskRouterException(_166 String.format("The activity '%s' doesn't exist in the workspace", activityFriendlyName)_166 )_166 );_166_166 new WorkerUpdater(workspace.getSid(), worker.getSid())_166 .setActivitySid(activity.getSid())_166 .update(client);_166 }_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
_265package com.twilio.taskrouter.application;_265_265import com.google.inject.Guice;_265import com.google.inject.Injector;_265import com.twilio.rest.taskrouter.v1.workspace.Activity;_265import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;_265import com.twilio.rest.taskrouter.v1.workspace.Workflow;_265import com.twilio.taskrouter.WorkflowRule;_265import com.twilio.taskrouter.WorkflowRuleTarget;_265import com.twilio.taskrouter.domain.common.TwilioAppSettings;_265import com.twilio.taskrouter.domain.common.Utils;_265import com.twilio.taskrouter.domain.error.TaskRouterException;_265import com.twilio.taskrouter.domain.model.WorkspaceFacade;_265import org.apache.commons.lang3.StringUtils;_265import org.apache.commons.lang3.text.StrSubstitutor;_265_265import javax.json.Json;_265import javax.json.JsonArray;_265import javax.json.JsonObject;_265import javax.json.JsonReader;_265import java.io.File;_265import java.io.IOException;_265import java.io.StringReader;_265import java.net.URISyntaxException;_265import java.net.URL;_265import java.util.Arrays;_265import java.util.HashMap;_265import java.util.List;_265import java.util.Map;_265import java.util.Optional;_265import java.util.Properties;_265import java.util.logging.Logger;_265import java.util.stream.Collectors;_265_265import static java.lang.System.exit;_265_265//import org.apache.commons.lang3.StringUtils;_265//import org.apache.commons.lang3.text.StrSubstitutor;_265_265/**_265 * Creates a workspace_265 */_265class CreateWorkspaceTask {_265_265 private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());_265_265 public static void main(String[] args) {_265_265 System.out.println("Creating workspace...");_265 if (args.length < 3) {_265 System.out.println("You must specify 3 parameters:");_265 System.out.println("- Server hostname. E.g, <hash>.ngrok.com");_265 System.out.println("- Phone of the first agent (Bob)");_265 System.out.println("- Phone of the secondary agent (Alice)");_265 exit(1);_265 }_265_265 String hostname = args[0];_265 String bobPhone = args[1];_265 String alicePhone = args[2];_265 System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",_265 hostname, bobPhone, alicePhone));_265_265 //Get the configuration_265 JsonObject workspaceConfig = createWorkspaceConfig(args);_265_265 //Get or Create the Workspace_265 Injector injector = Guice.createInjector();_265 final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);_265_265 String workspaceName = workspaceConfig.getString("name");_265 Map<String, String> workspaceParams = new HashMap<>();_265 workspaceParams.put("FriendlyName", workspaceName);_265 workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));_265_265 try {_265 WorkspaceFacade workspaceFacade = WorkspaceFacade_265 .create(twilioSettings.getTwilioRestClient(), workspaceParams);_265_265 addWorkersToWorkspace(workspaceFacade, workspaceConfig);_265 addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);_265 Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);_265_265 printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);_265 } catch (TaskRouterException e) {_265 LOG.severe(e.getMessage());_265 exit(1);_265 }_265 }_265_265 public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {_265 Map<String, String> workerParams = new HashMap<>();_265 workerParams.put("FriendlyName", workerJson.getString("name"));_265 workerParams.put("ActivitySid", idleActivity.getSid());_265 workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());_265_265 try {_265 workspaceFacade.addWorker(workerParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");_265 Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->_265 new TaskRouterException("The activity for reservations 'Reserved' was not found. "_265 + "TaskQueues cannot be added."));_265 Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->_265 new TaskRouterException("The activity for assignments 'Busy' was not found. "_265 + "TaskQueues cannot be added."));_265 taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {_265 Map<String, String> taskQueueParams = new HashMap<>();_265 taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));_265 taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));_265 taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());_265 taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());_265_265 try {_265 workspaceFacade.addTaskQueue(taskQueueParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceConfig) {_265 JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");_265 String workflowName = workflowJson.getString("name");_265 return workspaceFacade.findWorkflowByName(workflowName)_265 .orElseGet(() -> {_265 Map<String, String> workflowParams = new HashMap<>();_265 workflowParams.put("FriendlyName", workflowName);_265 workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));_265_265 String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);_265 workflowParams.put("Configuration", workflowConfigJson);_265_265 return workspaceFacade.addWorkflow(workflowParams);_265 });_265 }_265_265 public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,_265 Workflow workflow,_265 TwilioAppSettings twilioSettings) {_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 Properties workspaceParams = new Properties();_265 workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());_265 workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());_265 workspaceParams.put("workspace.sid", workspaceFacade.getSid());_265 workspaceParams.put("workflow.sid", workflow.getSid());_265 workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());_265 workspaceParams.put("email", twilioSettings.getEmail());_265 workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());_265_265 File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);_265_265 try {_265 Utils.saveProperties(workspaceParams,_265 workspacePropertiesFile,_265 "Properties for last created Twilio TaskRouter workspace");_265 } catch (IOException e) {_265 LOG.severe("Could not save workspace.properties with current configuration");_265 exit(1);_265 }_265_265 String successMsg = String.format("Workspace '%s' was created successfully.",_265 workspaceFacade.getFriendlyName());_265 final int lineLength = successMsg.length() + 2;_265_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println(String.format(" %s ", successMsg));_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println("The following variables were registered:");_265 System.out.println("\n");_265 workspaceParams.entrySet().stream().forEach(propertyEntry -> {_265 System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));_265 });_265 System.out.println("\n");_265 System.out.println(StringUtils.repeat("#", lineLength));_265 }_265_265 public static JsonObject createWorkspaceConfig(String[] args) {_265 final String configFileName = "workspace.json";_265_265 Optional<URL> url =_265 Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));_265 return url.map(u -> {_265 try {_265 File workspaceConfigJsonFile = new File(u.toURI());_265 String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);_265 String parsedContent = parseWorkspaceJsonContent(jsonContent, args);_265_265 try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {_265 return jsonReader.readObject();_265 }_265 } catch (URISyntaxException e) {_265 throw new TaskRouterException(String.format("Wrong uri to find %s: %s",_265 configFileName, e.getMessage()));_265 } catch (IOException e) {_265 throw new TaskRouterException(String.format("Error while reading %s: %s",_265 configFileName, e.getMessage()));_265 }_265 }).orElseThrow(_265 () -> new TaskRouterException("There's no valid configuration in " + configFileName));_265 }_265_265 private static String parseWorkspaceJsonContent(final String unparsedContent,_265 final String... args) {_265 Map<String, String> values = new HashMap<>();_265 values.put("host", args[0]);_265 values.put("bob_number", args[1]);_265 values.put("alice_number", args[2]);_265_265 StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");_265 return strSubstitutor.replace(unparsedContent);_265 }_265_265 public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,_265 JsonObject workflowJson) {_265 try {_265 JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");_265 TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")_265 .orElseThrow(() -> new TaskRouterException("Default queue not found"));_265 WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())_265 .expression("1=1")_265 .priority(1)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()_265 .map(ruleJson -> {_265 String ruleQueueName = ruleJson.getString("targetTaskQueue");_265 TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(_265 () -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));_265_265 WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())_265 .priority(5)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);_265_265 return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();_265 }).collect(Collectors.toList());_265_265 com.twilio.taskrouter.Workflow config;_265 config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);_265 return config.toJson();_265 } catch (Exception ex) {_265 throw new TaskRouterException("Error while creating workflow json configuration", ex);_265 }_265 }_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
_265package com.twilio.taskrouter.application;_265_265import com.google.inject.Guice;_265import com.google.inject.Injector;_265import com.twilio.rest.taskrouter.v1.workspace.Activity;_265import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;_265import com.twilio.rest.taskrouter.v1.workspace.Workflow;_265import com.twilio.taskrouter.WorkflowRule;_265import com.twilio.taskrouter.WorkflowRuleTarget;_265import com.twilio.taskrouter.domain.common.TwilioAppSettings;_265import com.twilio.taskrouter.domain.common.Utils;_265import com.twilio.taskrouter.domain.error.TaskRouterException;_265import com.twilio.taskrouter.domain.model.WorkspaceFacade;_265import org.apache.commons.lang3.StringUtils;_265import org.apache.commons.lang3.text.StrSubstitutor;_265_265import javax.json.Json;_265import javax.json.JsonArray;_265import javax.json.JsonObject;_265import javax.json.JsonReader;_265import java.io.File;_265import java.io.IOException;_265import java.io.StringReader;_265import java.net.URISyntaxException;_265import java.net.URL;_265import java.util.Arrays;_265import java.util.HashMap;_265import java.util.List;_265import java.util.Map;_265import java.util.Optional;_265import java.util.Properties;_265import java.util.logging.Logger;_265import java.util.stream.Collectors;_265_265import static java.lang.System.exit;_265_265//import org.apache.commons.lang3.StringUtils;_265//import org.apache.commons.lang3.text.StrSubstitutor;_265_265/**_265 * Creates a workspace_265 */_265class CreateWorkspaceTask {_265_265 private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());_265_265 public static void main(String[] args) {_265_265 System.out.println("Creating workspace...");_265 if (args.length < 3) {_265 System.out.println("You must specify 3 parameters:");_265 System.out.println("- Server hostname. E.g, <hash>.ngrok.com");_265 System.out.println("- Phone of the first agent (Bob)");_265 System.out.println("- Phone of the secondary agent (Alice)");_265 exit(1);_265 }_265_265 String hostname = args[0];_265 String bobPhone = args[1];_265 String alicePhone = args[2];_265 System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",_265 hostname, bobPhone, alicePhone));_265_265 //Get the configuration_265 JsonObject workspaceConfig = createWorkspaceConfig(args);_265_265 //Get or Create the Workspace_265 Injector injector = Guice.createInjector();_265 final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);_265_265 String workspaceName = workspaceConfig.getString("name");_265 Map<String, String> workspaceParams = new HashMap<>();_265 workspaceParams.put("FriendlyName", workspaceName);_265 workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));_265_265 try {_265 WorkspaceFacade workspaceFacade = WorkspaceFacade_265 .create(twilioSettings.getTwilioRestClient(), workspaceParams);_265_265 addWorkersToWorkspace(workspaceFacade, workspaceConfig);_265 addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);_265 Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);_265_265 printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);_265 } catch (TaskRouterException e) {_265 LOG.severe(e.getMessage());_265 exit(1);_265 }_265 }_265_265 public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {_265 Map<String, String> workerParams = new HashMap<>();_265 workerParams.put("FriendlyName", workerJson.getString("name"));_265 workerParams.put("ActivitySid", idleActivity.getSid());_265 workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());_265_265 try {_265 workspaceFacade.addWorker(workerParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");_265 Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->_265 new TaskRouterException("The activity for reservations 'Reserved' was not found. "_265 + "TaskQueues cannot be added."));_265 Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->_265 new TaskRouterException("The activity for assignments 'Busy' was not found. "_265 + "TaskQueues cannot be added."));_265 taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {_265 Map<String, String> taskQueueParams = new HashMap<>();_265 taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));_265 taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));_265 taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());_265 taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());_265_265 try {_265 workspaceFacade.addTaskQueue(taskQueueParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceConfig) {_265 JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");_265 String workflowName = workflowJson.getString("name");_265 return workspaceFacade.findWorkflowByName(workflowName)_265 .orElseGet(() -> {_265 Map<String, String> workflowParams = new HashMap<>();_265 workflowParams.put("FriendlyName", workflowName);_265 workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));_265_265 String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);_265 workflowParams.put("Configuration", workflowConfigJson);_265_265 return workspaceFacade.addWorkflow(workflowParams);_265 });_265 }_265_265 public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,_265 Workflow workflow,_265 TwilioAppSettings twilioSettings) {_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 Properties workspaceParams = new Properties();_265 workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());_265 workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());_265 workspaceParams.put("workspace.sid", workspaceFacade.getSid());_265 workspaceParams.put("workflow.sid", workflow.getSid());_265 workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());_265 workspaceParams.put("email", twilioSettings.getEmail());_265 workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());_265_265 File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);_265_265 try {_265 Utils.saveProperties(workspaceParams,_265 workspacePropertiesFile,_265 "Properties for last created Twilio TaskRouter workspace");_265 } catch (IOException e) {_265 LOG.severe("Could not save workspace.properties with current configuration");_265 exit(1);_265 }_265_265 String successMsg = String.format("Workspace '%s' was created successfully.",_265 workspaceFacade.getFriendlyName());_265 final int lineLength = successMsg.length() + 2;_265_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println(String.format(" %s ", successMsg));_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println("The following variables were registered:");_265 System.out.println("\n");_265 workspaceParams.entrySet().stream().forEach(propertyEntry -> {_265 System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));_265 });_265 System.out.println("\n");_265 System.out.println(StringUtils.repeat("#", lineLength));_265 }_265_265 public static JsonObject createWorkspaceConfig(String[] args) {_265 final String configFileName = "workspace.json";_265_265 Optional<URL> url =_265 Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));_265 return url.map(u -> {_265 try {_265 File workspaceConfigJsonFile = new File(u.toURI());_265 String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);_265 String parsedContent = parseWorkspaceJsonContent(jsonContent, args);_265_265 try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {_265 return jsonReader.readObject();_265 }_265 } catch (URISyntaxException e) {_265 throw new TaskRouterException(String.format("Wrong uri to find %s: %s",_265 configFileName, e.getMessage()));_265 } catch (IOException e) {_265 throw new TaskRouterException(String.format("Error while reading %s: %s",_265 configFileName, e.getMessage()));_265 }_265 }).orElseThrow(_265 () -> new TaskRouterException("There's no valid configuration in " + configFileName));_265 }_265_265 private static String parseWorkspaceJsonContent(final String unparsedContent,_265 final String... args) {_265 Map<String, String> values = new HashMap<>();_265 values.put("host", args[0]);_265 values.put("bob_number", args[1]);_265 values.put("alice_number", args[2]);_265_265 StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");_265 return strSubstitutor.replace(unparsedContent);_265 }_265_265 public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,_265 JsonObject workflowJson) {_265 try {_265 JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");_265 TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")_265 .orElseThrow(() -> new TaskRouterException("Default queue not found"));_265 WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())_265 .expression("1=1")_265 .priority(1)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()_265 .map(ruleJson -> {_265 String ruleQueueName = ruleJson.getString("targetTaskQueue");_265 TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(_265 () -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));_265_265 WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())_265 .priority(5)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);_265_265 return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();_265 }).collect(Collectors.toList());_265_265 com.twilio.taskrouter.Workflow config;_265 config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);_265 return config.toJson();_265 } catch (Exception ex) {_265 throw new TaskRouterException("Error while creating workflow json configuration", ex);_265 }_265 }_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
_265package com.twilio.taskrouter.application;_265_265import com.google.inject.Guice;_265import com.google.inject.Injector;_265import com.twilio.rest.taskrouter.v1.workspace.Activity;_265import com.twilio.rest.taskrouter.v1.workspace.TaskQueue;_265import com.twilio.rest.taskrouter.v1.workspace.Workflow;_265import com.twilio.taskrouter.WorkflowRule;_265import com.twilio.taskrouter.WorkflowRuleTarget;_265import com.twilio.taskrouter.domain.common.TwilioAppSettings;_265import com.twilio.taskrouter.domain.common.Utils;_265import com.twilio.taskrouter.domain.error.TaskRouterException;_265import com.twilio.taskrouter.domain.model.WorkspaceFacade;_265import org.apache.commons.lang3.StringUtils;_265import org.apache.commons.lang3.text.StrSubstitutor;_265_265import javax.json.Json;_265import javax.json.JsonArray;_265import javax.json.JsonObject;_265import javax.json.JsonReader;_265import java.io.File;_265import java.io.IOException;_265import java.io.StringReader;_265import java.net.URISyntaxException;_265import java.net.URL;_265import java.util.Arrays;_265import java.util.HashMap;_265import java.util.List;_265import java.util.Map;_265import java.util.Optional;_265import java.util.Properties;_265import java.util.logging.Logger;_265import java.util.stream.Collectors;_265_265import static java.lang.System.exit;_265_265//import org.apache.commons.lang3.StringUtils;_265//import org.apache.commons.lang3.text.StrSubstitutor;_265_265/**_265 * Creates a workspace_265 */_265class CreateWorkspaceTask {_265_265 private static final Logger LOG = Logger.getLogger(CreateWorkspaceTask.class.getName());_265_265 public static void main(String[] args) {_265_265 System.out.println("Creating workspace...");_265 if (args.length < 3) {_265 System.out.println("You must specify 3 parameters:");_265 System.out.println("- Server hostname. E.g, <hash>.ngrok.com");_265 System.out.println("- Phone of the first agent (Bob)");_265 System.out.println("- Phone of the secondary agent (Alice)");_265 exit(1);_265 }_265_265 String hostname = args[0];_265 String bobPhone = args[1];_265 String alicePhone = args[2];_265 System.out.println(String.format("Server: %s\nBob phone: %s\nAlice phone: %s\n",_265 hostname, bobPhone, alicePhone));_265_265 //Get the configuration_265 JsonObject workspaceConfig = createWorkspaceConfig(args);_265_265 //Get or Create the Workspace_265 Injector injector = Guice.createInjector();_265 final TwilioAppSettings twilioSettings = injector.getInstance(TwilioAppSettings.class);_265_265 String workspaceName = workspaceConfig.getString("name");_265 Map<String, String> workspaceParams = new HashMap<>();_265 workspaceParams.put("FriendlyName", workspaceName);_265 workspaceParams.put("EventCallbackUrl", workspaceConfig.getString("event_callback"));_265_265 try {_265 WorkspaceFacade workspaceFacade = WorkspaceFacade_265 .create(twilioSettings.getTwilioRestClient(), workspaceParams);_265_265 addWorkersToWorkspace(workspaceFacade, workspaceConfig);_265 addTaskQueuesToWorkspace(workspaceFacade, workspaceConfig);_265 Workflow workflow = addWorkflowToWorkspace(workspaceFacade, workspaceConfig);_265_265 printSuccessAndExportVariables(workspaceFacade, workflow, twilioSettings);_265 } catch (TaskRouterException e) {_265 LOG.severe(e.getMessage());_265 exit(1);_265 }_265 }_265_265 public static void addWorkersToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray workersJson = workspaceJsonConfig.getJsonArray("workers");_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 workersJson.getValuesAs(JsonObject.class).forEach(workerJson -> {_265 Map<String, String> workerParams = new HashMap<>();_265 workerParams.put("FriendlyName", workerJson.getString("name"));_265 workerParams.put("ActivitySid", idleActivity.getSid());_265 workerParams.put("Attributes", workerJson.getJsonObject("attributes").toString());_265_265 try {_265 workspaceFacade.addWorker(workerParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static void addTaskQueuesToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceJsonConfig) {_265 JsonArray taskQueuesJson = workspaceJsonConfig.getJsonArray("task_queues");_265 Activity reservationActivity = workspaceFacade.findActivityByName("Reserved").orElseThrow(() ->_265 new TaskRouterException("The activity for reservations 'Reserved' was not found. "_265 + "TaskQueues cannot be added."));_265 Activity assignmentActivity = workspaceFacade.findActivityByName("Busy").orElseThrow(() ->_265 new TaskRouterException("The activity for assignments 'Busy' was not found. "_265 + "TaskQueues cannot be added."));_265 taskQueuesJson.getValuesAs(JsonObject.class).forEach(taskQueueJson -> {_265 Map<String, String> taskQueueParams = new HashMap<>();_265 taskQueueParams.put("FriendlyName", taskQueueJson.getString("name"));_265 taskQueueParams.put("TargetWorkers", taskQueueJson.getString("targetWorkers"));_265 taskQueueParams.put("ReservationActivitySid", reservationActivity.getSid());_265 taskQueueParams.put("AssignmentActivitySid", assignmentActivity.getSid());_265_265 try {_265 workspaceFacade.addTaskQueue(taskQueueParams);_265 } catch (TaskRouterException e) {_265 LOG.warning(e.getMessage());_265 }_265 });_265 }_265_265 public static Workflow addWorkflowToWorkspace(WorkspaceFacade workspaceFacade,_265 JsonObject workspaceConfig) {_265 JsonObject workflowJson = workspaceConfig.getJsonObject("workflow");_265 String workflowName = workflowJson.getString("name");_265 return workspaceFacade.findWorkflowByName(workflowName)_265 .orElseGet(() -> {_265 Map<String, String> workflowParams = new HashMap<>();_265 workflowParams.put("FriendlyName", workflowName);_265 workflowParams.put("AssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("FallbackAssignmentCallbackUrl", workflowJson.getString("callback"));_265 workflowParams.put("TaskReservationTimeout", workflowJson.getString("timeout"));_265_265 String workflowConfigJson = createWorkFlowJsonConfig(workspaceFacade, workflowJson);_265 workflowParams.put("Configuration", workflowConfigJson);_265_265 return workspaceFacade.addWorkflow(workflowParams);_265 });_265 }_265_265 public static void printSuccessAndExportVariables(WorkspaceFacade workspaceFacade,_265 Workflow workflow,_265 TwilioAppSettings twilioSettings) {_265 Activity idleActivity = workspaceFacade.getIdleActivity();_265_265 Properties workspaceParams = new Properties();_265 workspaceParams.put("account.sid", twilioSettings.getTwilioAccountSid());_265 workspaceParams.put("auth.token", twilioSettings.getTwilioAuthToken());_265 workspaceParams.put("workspace.sid", workspaceFacade.getSid());_265 workspaceParams.put("workflow.sid", workflow.getSid());_265 workspaceParams.put("postWorkActivity.sid", idleActivity.getSid());_265 workspaceParams.put("email", twilioSettings.getEmail());_265 workspaceParams.put("phoneNumber", twilioSettings.getPhoneNumber().toString());_265_265 File workspacePropertiesFile = new File(TwilioAppSettings.WORKSPACE_PROPERTIES_FILE_PATH);_265_265 try {_265 Utils.saveProperties(workspaceParams,_265 workspacePropertiesFile,_265 "Properties for last created Twilio TaskRouter workspace");_265 } catch (IOException e) {_265 LOG.severe("Could not save workspace.properties with current configuration");_265 exit(1);_265 }_265_265 String successMsg = String.format("Workspace '%s' was created successfully.",_265 workspaceFacade.getFriendlyName());_265 final int lineLength = successMsg.length() + 2;_265_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println(String.format(" %s ", successMsg));_265 System.out.println(StringUtils.repeat("#", lineLength));_265 System.out.println("The following variables were registered:");_265 System.out.println("\n");_265 workspaceParams.entrySet().stream().forEach(propertyEntry -> {_265 System.out.println(String.format("%s=%s", propertyEntry.getKey(), propertyEntry.getValue()));_265 });_265 System.out.println("\n");_265 System.out.println(StringUtils.repeat("#", lineLength));_265 }_265_265 public static JsonObject createWorkspaceConfig(String[] args) {_265 final String configFileName = "workspace.json";_265_265 Optional<URL> url =_265 Optional.ofNullable(CreateWorkspaceTask.class.getResource(File.separator + configFileName));_265 return url.map(u -> {_265 try {_265 File workspaceConfigJsonFile = new File(u.toURI());_265 String jsonContent = Utils.readFileContent(workspaceConfigJsonFile);_265 String parsedContent = parseWorkspaceJsonContent(jsonContent, args);_265_265 try (JsonReader jsonReader = Json.createReader(new StringReader(parsedContent))) {_265 return jsonReader.readObject();_265 }_265 } catch (URISyntaxException e) {_265 throw new TaskRouterException(String.format("Wrong uri to find %s: %s",_265 configFileName, e.getMessage()));_265 } catch (IOException e) {_265 throw new TaskRouterException(String.format("Error while reading %s: %s",_265 configFileName, e.getMessage()));_265 }_265 }).orElseThrow(_265 () -> new TaskRouterException("There's no valid configuration in " + configFileName));_265 }_265_265 private static String parseWorkspaceJsonContent(final String unparsedContent,_265 final String... args) {_265 Map<String, String> values = new HashMap<>();_265 values.put("host", args[0]);_265 values.put("bob_number", args[1]);_265 values.put("alice_number", args[2]);_265_265 StrSubstitutor strSubstitutor = new StrSubstitutor(values, "%(", ")s");_265 return strSubstitutor.replace(unparsedContent);_265 }_265_265 public static String createWorkFlowJsonConfig(WorkspaceFacade workspaceFacade,_265 JsonObject workflowJson) {_265 try {_265 JsonArray routingConfigRules = workflowJson.getJsonArray("routingConfiguration");_265 TaskQueue defaultQueue = workspaceFacade.findTaskQueueByName("Default")_265 .orElseThrow(() -> new TaskRouterException("Default queue not found"));_265 WorkflowRuleTarget defaultRuleTarget = new WorkflowRuleTarget.Builder(defaultQueue.getSid())_265 .expression("1=1")_265 .priority(1)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRule> rules = routingConfigRules.getValuesAs(JsonObject.class).stream()_265 .map(ruleJson -> {_265 String ruleQueueName = ruleJson.getString("targetTaskQueue");_265 TaskQueue ruleQueue = workspaceFacade.findTaskQueueByName(ruleQueueName).orElseThrow(_265 () -> new TaskRouterException(String.format("%s queue not found", ruleQueueName)));_265_265 WorkflowRuleTarget queueRuleTarget = new WorkflowRuleTarget.Builder(ruleQueue.getSid())_265 .priority(5)_265 .timeout(30)_265 .build();_265_265 List<WorkflowRuleTarget> ruleTargets = Arrays.asList(queueRuleTarget, defaultRuleTarget);_265_265 return new WorkflowRule.Builder(ruleJson.getString("expression"), ruleTargets).build();_265 }).collect(Collectors.toList());_265_265 com.twilio.taskrouter.Workflow config;_265 config = new com.twilio.taskrouter.Workflow(rules, defaultRuleTarget);_265 return config.toJson();_265 } catch (Exception ex) {_265 throw new TaskRouterException("Error while creating workflow json configuration", ex);_265 }_265 }_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
_52package com.twilio.taskrouter.application.servlet;_52_52_52import com.twilio.twiml.Gather;_52import com.twilio.twiml.Method;_52import com.twilio.twiml.Say;_52import com.twilio.twiml.TwiMLException;_52import com.twilio.twiml.VoiceResponse;_52_52import javax.inject.Singleton;_52import javax.servlet.ServletException;_52import javax.servlet.http.HttpServlet;_52import javax.servlet.http.HttpServletRequest;_52import javax.servlet.http.HttpServletResponse;_52import java.io.IOException;_52import java.util.logging.Level;_52import java.util.logging.Logger;_52_52/**_52 * Returns TwiML instructions to TwilioAppSettings's POST requests_52 */_52@Singleton_52public class IncomingCallServlet extends HttpServlet {_52_52 private static final Logger LOG = Logger.getLogger(IncomingCallServlet.class.getName());_52_52 @Override_52 public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,_52 IOException {_52 try {_52 final VoiceResponse twimlResponse = new VoiceResponse.Builder()_52 .gather(new Gather.Builder()_52 .action("/call/enqueue")_52 .numDigits(1)_52 .timeout(10)_52 .method(Method.POST)_52 .say(new Say_52 .Builder("For Programmable SMS, press one. For Voice, press any other key.")_52 .build()_52 )_52 .build()_52 ).build();_52_52 resp.setContentType("application/xml");_52 resp.getWriter().print(twimlResponse.toXml());_52 } catch (TwiMLException e) {_52 LOG.log(Level.SEVERE, "Unexpected error while creating incoming call response", e);_52 throw new RuntimeException(e);_52 }_52 }_52_52}
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 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
_62package com.twilio.taskrouter.application.servlet;_62_62import com.twilio.taskrouter.domain.common.TwilioAppSettings;_62import com.twilio.twiml.EnqueueTask;_62import com.twilio.twiml.Task;_62import com.twilio.twiml.TwiMLException;_62import com.twilio.twiml.VoiceResponse;_62_62import javax.inject.Inject;_62import javax.inject.Singleton;_62import javax.servlet.ServletException;_62import javax.servlet.http.HttpServlet;_62import javax.servlet.http.HttpServletRequest;_62import javax.servlet.http.HttpServletResponse;_62import java.io.IOException;_62import java.util.Optional;_62import java.util.logging.Level;_62import java.util.logging.Logger;_62_62import static java.lang.String.format;_62_62/**_62 * Selects a product by creating a Task on the TaskRouter Workflow_62 */_62@Singleton_62public class EnqueueServlet extends HttpServlet {_62_62 private static final Logger LOG = Logger.getLogger(EnqueueServlet.class.getName());_62_62 private final String workflowSid;_62_62 @Inject_62 public EnqueueServlet(TwilioAppSettings twilioSettings) {_62 this.workflowSid = twilioSettings.getWorkflowSid();_62 }_62_62 @Override_62 public void doPost(HttpServletRequest req, HttpServletResponse resp)_62 throws ServletException, IOException {_62_62 String selectedProduct = getSelectedProduct(req);_62 Task task = new Task.Builder()_62 .data(format("{\"selected_product\": \"%s\"}", selectedProduct))_62 .build();_62_62 EnqueueTask enqueueTask = new EnqueueTask.Builder(task).workflowSid(workflowSid).build();_62_62 VoiceResponse voiceResponse = new VoiceResponse.Builder().enqueue(enqueueTask).build();_62 resp.setContentType("application/xml");_62 try {_62 resp.getWriter().print(voiceResponse.toXml());_62 } catch (TwiMLException e) {_62 LOG.log(Level.SEVERE, e.getMessage(), e);_62 throw new RuntimeException(e);_62 }_62 }_62_62 private String getSelectedProduct(HttpServletRequest request) {_62 return Optional.ofNullable(request.getParameter("Digits"))_62 .filter(x -> x.equals("1")).map((first) -> "ProgrammableSMS").orElse("ProgrammableVoice");_62 }_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
_36package com.twilio.taskrouter.application.servlet;_36_36import com.twilio.taskrouter.domain.common.TwilioAppSettings;_36_36import javax.inject.Inject;_36import javax.inject.Singleton;_36import javax.json.Json;_36import javax.servlet.ServletException;_36import javax.servlet.http.HttpServlet;_36import javax.servlet.http.HttpServletRequest;_36import javax.servlet.http.HttpServletResponse;_36import java.io.IOException;_36_36/**_36 * Servlet for Task assignments_36 */_36@Singleton_36public class AssignmentServlet extends HttpServlet {_36_36 private final String dequeueInstruction;_36_36 @Inject_36 public AssignmentServlet(TwilioAppSettings twilioAppSettings) {_36 dequeueInstruction = Json.createObjectBuilder()_36 .add("instruction", "dequeue")_36 .add("post_work_activity_sid", twilioAppSettings.getPostWorkActivitySid())_36 .build().toString();_36 }_36_36 @Override_36 public void doPost(HttpServletRequest req, HttpServletResponse resp)_36 throws ServletException, IOException {_36 resp.setContentType("application/json");_36 resp.getWriter().print(dequeueInstruction);_36 }_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
_100package com.twilio.taskrouter.application.servlet;_100_100import com.google.inject.persist.Transactional;_100import com.twilio.rest.api.v2010.account.MessageCreator;_100import com.twilio.taskrouter.domain.common.TwilioAppSettings;_100import com.twilio.taskrouter.domain.model.MissedCall;_100import com.twilio.taskrouter.domain.repository.MissedCallRepository;_100import com.twilio.type.PhoneNumber;_100_100import javax.inject.Inject;_100import javax.inject.Singleton;_100import javax.json.Json;_100import javax.json.JsonObject;_100import javax.servlet.ServletException;_100import javax.servlet.http.HttpServlet;_100import javax.servlet.http.HttpServletRequest;_100import javax.servlet.http.HttpServletResponse;_100import java.io.IOException;_100import java.io.StringReader;_100import java.util.Optional;_100import java.util.logging.Logger;_100_100/**_100 * Servlet for Events callback for missed calls_100 */_100@Singleton_100public class EventsServlet extends HttpServlet {_100_100 private static final String LEAVE_MSG = "Sorry, All agents are busy. Please leave a message. "_100 + "We will call you as soon as possible";_100_100 private static final String OFFLINE_MSG = "Your status has changed to Offline. "_100 + "Reply with \"On\" to get back Online";_100_100 private static final Logger LOG = Logger.getLogger(EventsServlet.class.getName());_100_100 private final TwilioAppSettings twilioSettings;_100_100 private final MissedCallRepository missedCallRepository;_100_100 @Inject_100 public EventsServlet(TwilioAppSettings twilioSettings,_100 MissedCallRepository missedCallRepository) {_100 this.twilioSettings = twilioSettings;_100 this.missedCallRepository = missedCallRepository;_100 }_100_100 @Override_100 public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,_100 IOException {_100 Optional.ofNullable(req.getParameter("EventType"))_100 .ifPresent(eventName -> {_100 switch (eventName) {_100 case "workflow.timeout":_100 case "task.canceled":_100 parseAttributes("TaskAttributes", req)_100 .ifPresent(this::addMissingCallAndLeaveMessage);_100 break;_100 case "worker.activity.update":_100 Optional.ofNullable(req.getParameter("WorkerActivityName"))_100 .filter("Offline"::equals)_100 .ifPresent(offlineEvent -> {_100 parseAttributes("WorkerAttributes", req)_100 .ifPresent(this::notifyOfflineStatusToWorker);_100 });_100 break;_100 default:_100 }_100 });_100 }_100_100 private Optional<JsonObject> parseAttributes(String parameter, HttpServletRequest request) {_100 return Optional.ofNullable(request.getParameter(parameter))_100 .map(jsonRequest -> Json.createReader(new StringReader(jsonRequest)).readObject());_100 }_100_100 @Transactional_100 private void addMissingCallAndLeaveMessage(JsonObject taskAttributesJson) {_100 String phoneNumber = taskAttributesJson.getString("from");_100 String selectedProduct = taskAttributesJson.getString("selected_product");_100_100 MissedCall missedCall = new MissedCall(phoneNumber, selectedProduct);_100 missedCallRepository.add(missedCall);_100 LOG.info("Added Missing Call: " + missedCall);_100_100 String callSid = taskAttributesJson.getString("call_sid");_100 twilioSettings.redirectToVoiceMail(callSid, LEAVE_MSG);_100 }_100_100 private void notifyOfflineStatusToWorker(JsonObject workerAttributesJson) {_100 String workerPhone = workerAttributesJson.getString("contact_uri");_100 new MessageCreator(_100 new PhoneNumber(workerPhone),_100 new PhoneNumber(twilioSettings.getPhoneNumber().toString()),_100 OFFLINE_MSG_100 ).create();_100_100 }_100_100}
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
_63package com.twilio.taskrouter.application.servlet;_63_63import com.google.inject.Singleton;_63import com.twilio.taskrouter.domain.model.WorkspaceFacade;_63import com.twilio.twiml.Sms;_63import com.twilio.twiml.TwiMLException;_63import com.twilio.twiml.VoiceResponse;_63_63import javax.inject.Inject;_63import javax.servlet.ServletException;_63import javax.servlet.http.HttpServlet;_63import javax.servlet.http.HttpServletRequest;_63import javax.servlet.http.HttpServletResponse;_63import java.io.IOException;_63import java.util.Optional;_63import java.util.logging.Level;_63import java.util.logging.Logger;_63_63/**_63 * Handles the messages sent by workers for activate/deactivate_63 * themselves for receiving calls from users_63 */_63@Singleton_63public class MessageServlet extends HttpServlet {_63_63 private static final Logger LOG = Logger.getLogger(MessageServlet.class.getName());_63_63 private final WorkspaceFacade workspace;_63_63 @Inject_63 public MessageServlet(WorkspaceFacade workspace) {_63 this.workspace = workspace;_63 }_63_63 @Override_63 protected void doPost(HttpServletRequest req, HttpServletResponse resp)_63 throws ServletException, IOException {_63 final VoiceResponse twimlResponse;_63 final String newStatus = getNewWorkerStatus(req);_63 final String workerPhone = req.getParameter("From");_63_63 try {_63 Sms responseSms = workspace.findWorkerByPhone(workerPhone).map(worker -> {_63 workspace.updateWorkerStatus(worker, newStatus);_63 return new Sms.Builder(String.format("Your status has changed to %s", newStatus)).build();_63 }).orElseGet(() -> new Sms.Builder("You are not a valid worker").build());_63_63 twimlResponse = new VoiceResponse.Builder().sms(responseSms).build();_63 resp.setContentType("application/xml");_63 resp.getWriter().print(twimlResponse.toXml());_63_63 } catch (TwiMLException e) {_63 LOG.log(Level.SEVERE, "Error while providing answer to a workers' sms", e);_63 }_63_63 }_63_63 private String getNewWorkerStatus(HttpServletRequest request) {_63 return Optional.ofNullable(request.getParameter("Body"))_63 .filter(x -> x.equals("off")).map((first) -> "Offline").orElse("Idle");_63 }_63_63}
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.
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!