Build an Inactivity Timeout with Flex Conversations

June 14, 2022
Written by
Reviewed by

Build Inactivity Timeout Flex Conversations

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:

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:

Inactivity timeout with Flex Conversations architecture diagram

Developer Environment Setup

Let's make sure you have the software you need:

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.