Tuesday, 14 June, 2022 UTC


Summary

As you may know, Twilio has released Flex Conversations. This is a big step in making orchestration more reliable and straightforward within the Flex ecosystem. In a blog post here, you can learn more about Flex Conversations's new perks. Introducing Flex Conversations also opens up possibilities for new cool features to be implemented, as the Conversations API provides a broad spectrum of tools for managing participants, addresses, and the conversation lifecycle.
One contact center feature that our customers often ask about is Inactivity Timeout. To keep your agents' productivity high, it's necessary to have an automated way to track abandoned tasks and clean them up. With the State Timers feature of the Conversations API along with Twilio Functions, it is possible now to implement an Inactivity Timeout in Flex. In this blog post, I will guide you through the configuration and code to achieve this.
Tutorial prerequisites
Before we can get started building, you need to make sure you have an account with Twilio. You can sign up here for free.
As soon as your Twilio account is created, you can proceed with creating a Twilio Flex Project.
You may have to make sure that Conversations is enabled. If you create a new project for this tutorial, Conversations should be enabled by default.
If you have an existing project that you want to migrate to Conversations, take a look at the Getting Started section of the blog post here.
To create a Flex account, navigate in your Twilio Console to Flex, then Overview, then click the "Create My Flex Account" button.
Once you've got your Flex account, note the following details – you will need them later:
  • Twilio Account SID: You can find your account SID and auth token in the admin console.
  • Twilio Auth Token: You can find your account SID and auth token in the admin console.
And with that, you're ready to start.
How the solution will work
We will be leveraging Twilio Functions and the Conversations API, namely for its State Timers. The goal here is to be able to configure a timeout – which resets every time there is a new message either from the customer or the agent – to complete a task in Flex in case no new messages are sent within a timeout period. Additionally, we would like to let the customer know that the conversation was timed out via sending a message.
Before we get to writing code, here is our high level plan:

Code

  • We will write a function named on_conversation_state_updated.ts. As the name suggests, this function will be invoked when the state of conversation changes via the onConversationStateUpdated event, which will serve as a trigger for cleaning up inactive tasks. The function will have the following responsibilities:
    • Set the task linked to conversation to completed state
    • Send a message to the customer that the session has timed out
    • Set the conversation state to closed (this is a necessary step in order to prepare a conversation to be reused for further communications)
  • We will write another function called on_reservation_accepted.ts. This function will be invoked first at the moment when the task is accepted by an agent, thus the name is chosen to reflect the TaskRouter event that the function will be invoked on: onReservationAccepted. The function will have two responsibilities:
    • Create a webhook on the conversation linked to the task to be fired on an onConversationStateUpdated event pointing to the URL of the on_conversation_state_updated.ts function.
    • Create an inactivity timer on the conversation linked to the task with the value defined in an environment variable.

Configuration

  • We will configure TaskRouter Workspace to send a webhook to the URL of the on_reservation_accepted.ts function on an onReservationAccepted event.
Here is a diagram of the solution we will be building:
Developer Environment Setup
Let's make sure you have the software you need:
  • node.js and npm
  • Twilio CLI
  • Twilio Serverless Toolkit
I will use Typescript for this tutorial, but it should work just as well with JavaScript.
Now we can start coding!
Create project
We will start by creating a project using the Twilio Serverless Toolkit. For this run, issue the following command in your shell:
twilio serverless:init flex-chat-inactivity-timeout --typescript 
A couple of notes here:
  • in the command, I used flex-chat-inactivity-timeout as my project name, feel free to use a different name
  • adding the --typescript parameter will create a project ready for Typescript. You can omit this parameter if you prefer JavaScript
With this step completed, you have a project that you can run in your local environment or deploy directly to Twilio Functions.
You will find a couple of function examples under the src/functions folder and a couple of asset examples under src/assets, you can safely remove them. (Or you can ignore them, whatever you prefer.)
Configure your environment
In order to run the code, we'll need environment variables to be in place. Go to .env in the root folder of the project, and update the file to have the following keys and values. Make sure to replace my placeholders with the values you collected in previous steps.
The timeout in the configuration below corresponds to 1 minute, and is formatted according to the ISO 8601 duration standard.
You can find the TaskRouter Workspace SID in Twilio Console under TaskRouter in the Workspaces section. Look for a Workspace named "Flex Task Assignment".
ACCOUNT_SID=<Twilio Account SID> AUTH_TOKEN=<Twilio Auth Token> TIMEOUT=PT1M TASK_ROUTER_WORKSPACE=<TaskRouter Workspace SID> 
Create functions
Now it is time to create the files you will be working inside. Navigate to src/functions, and create two files named on_reservation_accepted.protected.ts and on_conversation_state_updated.protected.ts.
While you are free to choose any other name for your function, make sure that the name ends with .protected.ts (or .protected.js) because this defines the visibility level of the function. Protected functions can only be called from the Twilio platform.
Here is the complete code for the on_reservation_accepted.protected.ts function, augmented with comments explaining the code:
// Imports global types import '@twilio-labs/serverless-runtime-types' // Fetches specific types import { Context, ServerlessCallback, ServerlessFunctionSignature, } from '@twilio-labs/serverless-runtime-types/types' // Environment variables type Env = { TIMEOUT: string } // Webhook event type with needed fields type OnReservationAccepted = { TaskAttributes: string, } export const handler: ServerlessFunctionSignature<Env, OnReservationAccepted> = async function ( context: Context<Env>, event: OnReservationAccepted, callback: ServerlessCallback, ) { console.log(event) // Parse task attributes JSON string into object let taskAttributes = JSON.parse(event.TaskAttributes) // Get Conversation SID for task attributes let conversationSid = taskAttributes["conversationSid"] // Create a TwilioResponse object const response = new Twilio.Response() response.appendHeader('Content-Type', 'application/json') // Create Conversation Context object const conversationContext = context .getTwilioClient() .conversations .conversations(conversationSid) // Create a webhook on the Conversation to be fired on onConversationStateUpdated // targeting on_conversation_state_updated function try { await conversationContext .webhooks .create({ target: 'webhook', configuration: { url: `https://${context.DOMAIN_NAME}/on_conversation_state_updated`, method: 'POST', filters: ['onConversationStateUpdated'], }, }) } catch (err) { console.error(err) response.setStatusCode(500) return callback(err, response) } // Create an inactivity timeout on the Conversation using timeout from environment variable try { await conversationContext .update({ timers: { inactive: context.TIMEOUT, }, }) } catch (err) { console.error(err) response.setStatusCode(500) return callback(err, response) } // Return success response console.log('OK') response.setStatusCode(200) response.setBody({ ConversationSid: conversationSid, }) return callback(null, response) } 
And here is the complete code for the on_conversation_state_updated.protected.ts function, augmented with comments explaining the code:
// Imports global types import '@twilio-labs/serverless-runtime-types' // Fetches specific types import { Context, ServerlessCallback, ServerlessFunctionSignature, } from '@twilio-labs/serverless-runtime-types/types' // Environment variables type Env = { TASK_ROUTER_WORKSPACE: string } // Webhook event type with needed fields type OnConversationStateUpdated = { ConversationSid: string, ChatServiceSid: string, StateTo: State, Reason: string } // Conversation state type type State = 'active' | 'inactive' export const handler: ServerlessFunctionSignature<Env, OnConversationStateUpdated> = async function ( context: Context<Env>, event: OnConversationStateUpdated, callback: ServerlessCallback, ) { console.log(event) // Create a TwilioResponse object const response = new Twilio.Response() response.appendHeader('Content-Type', 'application/json') // We proceed only if both conditions are met event.StateTo === 'inactive' AND event.Reason === 'TIMER' if (event.StateTo !== 'inactive' || event.Reason !== 'TIMER') { // Nothing to do response.setStatusCode(200) return callback(null, response) } // Initialize Twilio client const client = context.getTwilioClient() // Fetch tasks for Conversation SID from the event let tasks try { tasks = await client.taskrouter .workspaces(context.TASK_ROUTER_WORKSPACE) .tasks .list({ evaluateTaskAttributes: `conversationSid="${event.ConversationSid}"`, }, ) } catch (err) { console.error(err) response.setStatusCode(500) return callback(err, response) } // No tasks found or more than one task found, but both should never happen. if (tasks.length != 1) { response.setStatusCode(200) response.setBody({message: `Tasks found ${tasks.length}`}) return callback(null, response) } // Variable to store task instance let task = tasks[0] // Completing task in TaskRouter try { await client.taskrouter .workspaces(context.TASK_ROUTER_WORKSPACE) .tasks(task.sid) .update({ assignmentStatus: 'completed', }) } catch (err) { console.error(err) response.setStatusCode(500) return callback(err, response) } // Send timeout notification to the customer try { await client.conversations .conversations(event.ConversationSid) .messages .create({ body: 'Your session is timed out' }) } catch (err) { console.error(err) response.setStatusCode(500) return callback(err, response) } // Update Conversation state to closed try { await client.conversations .conversations(event.ConversationSid) .update({ state: 'closed', }) } catch (err) { console.error(err) response.setStatusCode(500) return callback(err, response) } // Return success response console.log('OK') response.setStatusCode(200) response.setBody({ ConversationSid: event.ConversationSid, TaskSid: task.sid, }) return callback(null, response) } 
Deploy to Twilio Functions
The next step is to deploy our code to Twilio Functions. To do this, execute the following command in the root of your project:
npm run deploy 
If you have problems deploying your code, check that the ACCOUNT_SID and AUTH_TOKEN environment variables are properly configured in your .env file.
Once deployment is complete, you will see the URLs of your newly created functions. It will look like the following:
Functions: [protected] https://flex-chat-inactivity-timeout-0000-dev.twil.io/on_conversation_state_updated [protected] https://flex-chat-inactivity-timeout-0000-dev.twil.io/on_reservation_accepted 
Copy the URL of the on_reservation_accepted function – we are now ready to configure the TaskRouter Workspace.
Configuring TaskRouter Workspace
In your Twilio Console, navigate to TaskRouter then the Workspaces section, and then find a Workspace named "Flex Task Assignment". Open the workspace and go to Settings. On the settings page, find the "Event Callbacks" section and select the "SPECIFIC EVENTS" radio button. Make sure in the events list only the "Reservation Accepted" checkbox is selected.
Testing
To test our solution do the following:
  1. Login to Flex as an agent and make sure you are in Available status.
  2. Send a SMS message to one of the numbers that you have configured in your Flex Project (to see configured phone numbers go to Twilio Console -> Flex -> Messaging and click the "Conversations Addresses" tab).
  3. Accept the incoming task in Flex and start chatting with the "customer".
  4. Wait for one minute and check that the task is automatically completed and your "customer" got a message saying that the session is timed out.
If you get all that: voilà! It's working! Now you're free to build it out for your own needs.
Conclusion
Using Twilio Functions and the Conversations API, we were able to build an automated way to handle inactive task cleanup to make sure we free up our agents for new customer chats.
The full code of the project is available on GitHub at https://github.com/kuschanton/flex-chat-inactivity-timeout.
Like what you built? See some of our other Functions and serverless tutorials on the blog.
Anton Kushch is a Senior Solutions Engineer at Twilio. He is helping companies in Emerging Markets in EMEA build powerful customer engagement solutions powered by Twilio. He can be reached at akushch [at] twilio.com, or you can collaborate with him on GitHub at https://github.com/kuschanton.