Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Dynamic Call Center with Python and Django


In this tutorial we will show how to automate the routing of calls from customers to your support agents. In this example customers would select a product, then be connected to a specialist for that product. If no one is available our customer's number will be saved so that our agent can call them back.


This is what the application does at a high level

this-is-what-the-application-does-at-a-high-level page anchor
  • Configure a workspace using the Twilio TaskRouter REST API .
  • Listen for incoming calls and let the user select a product with the dial pad.
  • Create a Task with the selected product and let TaskRouter handle it.
  • Store missed calls so agents can return the call to customers.
  • Redirect users to a voice mail when no one answers the call.
  • Allow agents to change their status (Available/Offline) via SMS.

In order to instruct TaskRouter to handle the Tasks, we need to configure a Workspace. We can do this in the TaskRouter Console(link takes you to an external page) or programmatically using the TaskRouter REST API.

In this Django application we'll do this setup when we start up the app.

A Workspace is the container element for any TaskRouter application. The elements are:

  • Tasks - Represents a customer trying to contact an agent
  • Workers - The agents responsible for handling Tasks
  • Task Queues - Holds Tasks to be consumed by a set of Workers
  • Workflows - Responsible for placing Tasks into Task Queues
  • Activities - Possible states of a Worker. Eg: idle, offline, busy

In order to build a client for this API, we need a TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN which you can find on Twilio Console. The function build_client configures and returns a TwilioTaskRouterClient, which is provided by the Twilio Python library.

Create, Setup and Configure the Workspace

create-setup-and-configure-the-workspace page anchor

task_router/workspace.py


_163
import json
_163
_163
from django.conf import settings
_163
from twilio.rest import Client
_163
_163
HOST = settings.HOST
_163
ALICE_NUMBER = settings.ALICE_NUMBER
_163
BOB_NUMBER = settings.BOB_NUMBER
_163
WORKSPACE_NAME = 'Twilio Workspace'
_163
_163
_163
def first(items):
_163
return items[0] if items else None
_163
_163
_163
def build_client():
_163
account_sid = settings.TWILIO_ACCOUNT_SID
_163
auth_token = settings.TWILIO_AUTH_TOKEN
_163
return Client(account_sid, auth_token)
_163
_163
_163
CACHE = {}
_163
_163
_163
def activities_dict(client, workspace_sid):
_163
activities = client.taskrouter.workspaces(workspace_sid)\
_163
.activities.list()
_163
_163
return {activity.friendly_name: activity for activity in activities}
_163
_163
_163
class WorkspaceInfo:
_163
_163
def __init__(self, workspace, workflow, activities, workers):
_163
self.workflow_sid = workflow.sid
_163
self.workspace_sid = workspace.sid
_163
self.activities = activities
_163
self.post_work_activity_sid = activities['Available'].sid
_163
self.workers = workers
_163
_163
_163
def setup():
_163
client = build_client()
_163
if 'WORKSPACE_INFO' not in CACHE:
_163
workspace = create_workspace(client)
_163
activities = activities_dict(client, workspace.sid)
_163
workers = create_workers(client, workspace, activities)
_163
queues = create_task_queues(client, workspace, activities)
_163
workflow = create_workflow(client, workspace, queues)
_163
CACHE['WORKSPACE_INFO'] = WorkspaceInfo(workspace, workflow, activities, workers)
_163
return CACHE['WORKSPACE_INFO']
_163
_163
_163
def create_workspace(client):
_163
try:
_163
workspace = first(client.taskrouter.workspaces.list(friendly_name=WORKSPACE_NAME))
_163
client.taskrouter.workspaces(workspace.sid).delete()
_163
except Exception:
_163
pass
_163
_163
events_callback = HOST + '/events/'
_163
_163
return client.taskrouter.workspaces.create(
_163
friendly_name=WORKSPACE_NAME,
_163
event_callback_url=events_callback,
_163
template=None)
_163
_163
_163
def create_workers(client, workspace, activities):
_163
alice_attributes = {
_163
"products": ["ProgrammableVoice"],
_163
"contact_uri": ALICE_NUMBER
_163
}
_163
_163
alice = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Alice',
_163
attributes=json.dumps(alice_attributes))
_163
_163
bob_attributes = {
_163
"products": ["ProgrammableSMS"],
_163
"contact_uri": BOB_NUMBER
_163
}
_163
_163
bob = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Bob',
_163
attributes=json.dumps(bob_attributes))
_163
_163
return {BOB_NUMBER: bob.sid, ALICE_NUMBER: alice.sid}
_163
_163
_163
def create_task_queues(client, workspace, activities):
_163
default_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Default',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='1==1')
_163
_163
sms_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='SMS',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableSMS" in products')
_163
_163
voice_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Voice',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableVoice" in products')
_163
_163
return {'sms': sms_queue, 'voice': voice_queue, 'default': default_queue}
_163
_163
_163
def create_workflow(client, workspace, queues):
_163
defaultTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
smsTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
voiceTarget = {
_163
'queue': queues['voice'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
default_filter = {
_163
'filter_friendly_name': 'Default Filter',
_163
'queue': queues['default'].sid,
_163
'Expression': '1==1',
_163
'priority': 1,
_163
'timeout': 30
_163
}
_163
_163
voiceFilter = {
_163
'filter_friendly_name': 'Voice Filter',
_163
'expression': 'selected_product=="ProgrammableVoice"',
_163
'targets': [voiceTarget, defaultTarget]
_163
}
_163
_163
smsFilter = {
_163
'filter_friendly_name': 'SMS Filter',
_163
'expression': 'selected_product=="ProgrammableSMS"',
_163
'targets': [smsTarget, defaultTarget]
_163
}
_163
_163
config = {
_163
'task_routing': {
_163
'filters': [voiceFilter, smsFilter],
_163
'default_filter': default_filter
_163
}
_163
}
_163
_163
callback_url = HOST + '/assignment/'
_163
_163
return client.taskrouter.workspaces(workspace.sid)\
_163
.workflows.create(friendly_name='Sales',
_163
assignment_callback_url=callback_url,
_163
fallback_assignment_callback_url=callback_url,
_163
task_reservation_timeout=15,
_163
configuration=json.dumps(config))

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 friendly_name as the one we are trying to create. In order to create a workspace we need to provide a friendly_name and a event_callback_url where a requests will be made every time an event is triggered in our workspace.

task_router/workspace.py


_163
import json
_163
_163
from django.conf import settings
_163
from twilio.rest import Client
_163
_163
HOST = settings.HOST
_163
ALICE_NUMBER = settings.ALICE_NUMBER
_163
BOB_NUMBER = settings.BOB_NUMBER
_163
WORKSPACE_NAME = 'Twilio Workspace'
_163
_163
_163
def first(items):
_163
return items[0] if items else None
_163
_163
_163
def build_client():
_163
account_sid = settings.TWILIO_ACCOUNT_SID
_163
auth_token = settings.TWILIO_AUTH_TOKEN
_163
return Client(account_sid, auth_token)
_163
_163
_163
CACHE = {}
_163
_163
_163
def activities_dict(client, workspace_sid):
_163
activities = client.taskrouter.workspaces(workspace_sid)\
_163
.activities.list()
_163
_163
return {activity.friendly_name: activity for activity in activities}
_163
_163
_163
class WorkspaceInfo:
_163
_163
def __init__(self, workspace, workflow, activities, workers):
_163
self.workflow_sid = workflow.sid
_163
self.workspace_sid = workspace.sid
_163
self.activities = activities
_163
self.post_work_activity_sid = activities['Available'].sid
_163
self.workers = workers
_163
_163
_163
def setup():
_163
client = build_client()
_163
if 'WORKSPACE_INFO' not in CACHE:
_163
workspace = create_workspace(client)
_163
activities = activities_dict(client, workspace.sid)
_163
workers = create_workers(client, workspace, activities)
_163
queues = create_task_queues(client, workspace, activities)
_163
workflow = create_workflow(client, workspace, queues)
_163
CACHE['WORKSPACE_INFO'] = WorkspaceInfo(workspace, workflow, activities, workers)
_163
return CACHE['WORKSPACE_INFO']
_163
_163
_163
def create_workspace(client):
_163
try:
_163
workspace = first(client.taskrouter.workspaces.list(friendly_name=WORKSPACE_NAME))
_163
client.taskrouter.workspaces(workspace.sid).delete()
_163
except Exception:
_163
pass
_163
_163
events_callback = HOST + '/events/'
_163
_163
return client.taskrouter.workspaces.create(
_163
friendly_name=WORKSPACE_NAME,
_163
event_callback_url=events_callback,
_163
template=None)
_163
_163
_163
def create_workers(client, workspace, activities):
_163
alice_attributes = {
_163
"products": ["ProgrammableVoice"],
_163
"contact_uri": ALICE_NUMBER
_163
}
_163
_163
alice = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Alice',
_163
attributes=json.dumps(alice_attributes))
_163
_163
bob_attributes = {
_163
"products": ["ProgrammableSMS"],
_163
"contact_uri": BOB_NUMBER
_163
}
_163
_163
bob = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Bob',
_163
attributes=json.dumps(bob_attributes))
_163
_163
return {BOB_NUMBER: bob.sid, ALICE_NUMBER: alice.sid}
_163
_163
_163
def create_task_queues(client, workspace, activities):
_163
default_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Default',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='1==1')
_163
_163
sms_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='SMS',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableSMS" in products')
_163
_163
voice_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Voice',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableVoice" in products')
_163
_163
return {'sms': sms_queue, 'voice': voice_queue, 'default': default_queue}
_163
_163
_163
def create_workflow(client, workspace, queues):
_163
defaultTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
smsTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
voiceTarget = {
_163
'queue': queues['voice'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
default_filter = {
_163
'filter_friendly_name': 'Default Filter',
_163
'queue': queues['default'].sid,
_163
'Expression': '1==1',
_163
'priority': 1,
_163
'timeout': 30
_163
}
_163
_163
voiceFilter = {
_163
'filter_friendly_name': 'Voice Filter',
_163
'expression': 'selected_product=="ProgrammableVoice"',
_163
'targets': [voiceTarget, defaultTarget]
_163
}
_163
_163
smsFilter = {
_163
'filter_friendly_name': 'SMS Filter',
_163
'expression': 'selected_product=="ProgrammableSMS"',
_163
'targets': [smsTarget, defaultTarget]
_163
}
_163
_163
config = {
_163
'task_routing': {
_163
'filters': [voiceFilter, smsFilter],
_163
'default_filter': default_filter
_163
}
_163
}
_163
_163
callback_url = HOST + '/assignment/'
_163
_163
return client.taskrouter.workspaces(workspace.sid)\
_163
.workflows.create(friendly_name='Sales',
_163
assignment_callback_url=callback_url,
_163
fallback_assignment_callback_url=callback_url,
_163
task_reservation_timeout=15,
_163
configuration=json.dumps(config))

We have a brand new workspace, now we need workers. Let's create them on the next step.


We'll create two workers, Bob and Alice. They each have two attributes: contact_uri a phone number and products, a list of products each worker is specialized in. We also need to specify an activity_sid and a name for each worker. The selected activity will define the status of the worker.

A set of default activities is created with your workspace. We use the Idle activity to make a worker available for incoming calls.

task_router/workspace.py


_163
import json
_163
_163
from django.conf import settings
_163
from twilio.rest import Client
_163
_163
HOST = settings.HOST
_163
ALICE_NUMBER = settings.ALICE_NUMBER
_163
BOB_NUMBER = settings.BOB_NUMBER
_163
WORKSPACE_NAME = 'Twilio Workspace'
_163
_163
_163
def first(items):
_163
return items[0] if items else None
_163
_163
_163
def build_client():
_163
account_sid = settings.TWILIO_ACCOUNT_SID
_163
auth_token = settings.TWILIO_AUTH_TOKEN
_163
return Client(account_sid, auth_token)
_163
_163
_163
CACHE = {}
_163
_163
_163
def activities_dict(client, workspace_sid):
_163
activities = client.taskrouter.workspaces(workspace_sid)\
_163
.activities.list()
_163
_163
return {activity.friendly_name: activity for activity in activities}
_163
_163
_163
class WorkspaceInfo:
_163
_163
def __init__(self, workspace, workflow, activities, workers):
_163
self.workflow_sid = workflow.sid
_163
self.workspace_sid = workspace.sid
_163
self.activities = activities
_163
self.post_work_activity_sid = activities['Available'].sid
_163
self.workers = workers
_163
_163
_163
def setup():
_163
client = build_client()
_163
if 'WORKSPACE_INFO' not in CACHE:
_163
workspace = create_workspace(client)
_163
activities = activities_dict(client, workspace.sid)
_163
workers = create_workers(client, workspace, activities)
_163
queues = create_task_queues(client, workspace, activities)
_163
workflow = create_workflow(client, workspace, queues)
_163
CACHE['WORKSPACE_INFO'] = WorkspaceInfo(workspace, workflow, activities, workers)
_163
return CACHE['WORKSPACE_INFO']
_163
_163
_163
def create_workspace(client):
_163
try:
_163
workspace = first(client.taskrouter.workspaces.list(friendly_name=WORKSPACE_NAME))
_163
client.taskrouter.workspaces(workspace.sid).delete()
_163
except Exception:
_163
pass
_163
_163
events_callback = HOST + '/events/'
_163
_163
return client.taskrouter.workspaces.create(
_163
friendly_name=WORKSPACE_NAME,
_163
event_callback_url=events_callback,
_163
template=None)
_163
_163
_163
def create_workers(client, workspace, activities):
_163
alice_attributes = {
_163
"products": ["ProgrammableVoice"],
_163
"contact_uri": ALICE_NUMBER
_163
}
_163
_163
alice = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Alice',
_163
attributes=json.dumps(alice_attributes))
_163
_163
bob_attributes = {
_163
"products": ["ProgrammableSMS"],
_163
"contact_uri": BOB_NUMBER
_163
}
_163
_163
bob = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Bob',
_163
attributes=json.dumps(bob_attributes))
_163
_163
return {BOB_NUMBER: bob.sid, ALICE_NUMBER: alice.sid}
_163
_163
_163
def create_task_queues(client, workspace, activities):
_163
default_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Default',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='1==1')
_163
_163
sms_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='SMS',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableSMS" in products')
_163
_163
voice_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Voice',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableVoice" in products')
_163
_163
return {'sms': sms_queue, 'voice': voice_queue, 'default': default_queue}
_163
_163
_163
def create_workflow(client, workspace, queues):
_163
defaultTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
smsTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
voiceTarget = {
_163
'queue': queues['voice'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
default_filter = {
_163
'filter_friendly_name': 'Default Filter',
_163
'queue': queues['default'].sid,
_163
'Expression': '1==1',
_163
'priority': 1,
_163
'timeout': 30
_163
}
_163
_163
voiceFilter = {
_163
'filter_friendly_name': 'Voice Filter',
_163
'expression': 'selected_product=="ProgrammableVoice"',
_163
'targets': [voiceTarget, defaultTarget]
_163
}
_163
_163
smsFilter = {
_163
'filter_friendly_name': 'SMS Filter',
_163
'expression': 'selected_product=="ProgrammableSMS"',
_163
'targets': [smsTarget, defaultTarget]
_163
}
_163
_163
config = {
_163
'task_routing': {
_163
'filters': [voiceFilter, smsFilter],
_163
'default_filter': default_filter
_163
}
_163
}
_163
_163
callback_url = HOST + '/assignment/'
_163
_163
return client.taskrouter.workspaces(workspace.sid)\
_163
.workflows.create(friendly_name='Sales',
_163
assignment_callback_url=callback_url,
_163
fallback_assignment_callback_url=callback_url,
_163
task_reservation_timeout=15,
_163
configuration=json.dumps(config))

After creating our workers, let's set up the Task Queues.


Next, we set up the Task Queues. Each with a friendly_name and a targetWorkers, which is an expression to match Workers. Our Task Queues are:

  1. SMS - Will target Workers specialized in Programmable SMS, such as Bob, using the expression '"ProgrammableSMS" in products' .
  2. Voice - Will do the same for Programmable Voice Workers, such as Alice, using the expression '"ProgrammableVoice" in products' .
  3. Default - 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.

task_router/workspace.py


_163
import json
_163
_163
from django.conf import settings
_163
from twilio.rest import Client
_163
_163
HOST = settings.HOST
_163
ALICE_NUMBER = settings.ALICE_NUMBER
_163
BOB_NUMBER = settings.BOB_NUMBER
_163
WORKSPACE_NAME = 'Twilio Workspace'
_163
_163
_163
def first(items):
_163
return items[0] if items else None
_163
_163
_163
def build_client():
_163
account_sid = settings.TWILIO_ACCOUNT_SID
_163
auth_token = settings.TWILIO_AUTH_TOKEN
_163
return Client(account_sid, auth_token)
_163
_163
_163
CACHE = {}
_163
_163
_163
def activities_dict(client, workspace_sid):
_163
activities = client.taskrouter.workspaces(workspace_sid)\
_163
.activities.list()
_163
_163
return {activity.friendly_name: activity for activity in activities}
_163
_163
_163
class WorkspaceInfo:
_163
_163
def __init__(self, workspace, workflow, activities, workers):
_163
self.workflow_sid = workflow.sid
_163
self.workspace_sid = workspace.sid
_163
self.activities = activities
_163
self.post_work_activity_sid = activities['Available'].sid
_163
self.workers = workers
_163
_163
_163
def setup():
_163
client = build_client()
_163
if 'WORKSPACE_INFO' not in CACHE:
_163
workspace = create_workspace(client)
_163
activities = activities_dict(client, workspace.sid)
_163
workers = create_workers(client, workspace, activities)
_163
queues = create_task_queues(client, workspace, activities)
_163
workflow = create_workflow(client, workspace, queues)
_163
CACHE['WORKSPACE_INFO'] = WorkspaceInfo(workspace, workflow, activities, workers)
_163
return CACHE['WORKSPACE_INFO']
_163
_163
_163
def create_workspace(client):
_163
try:
_163
workspace = first(client.taskrouter.workspaces.list(friendly_name=WORKSPACE_NAME))
_163
client.taskrouter.workspaces(workspace.sid).delete()
_163
except Exception:
_163
pass
_163
_163
events_callback = HOST + '/events/'
_163
_163
return client.taskrouter.workspaces.create(
_163
friendly_name=WORKSPACE_NAME,
_163
event_callback_url=events_callback,
_163
template=None)
_163
_163
_163
def create_workers(client, workspace, activities):
_163
alice_attributes = {
_163
"products": ["ProgrammableVoice"],
_163
"contact_uri": ALICE_NUMBER
_163
}
_163
_163
alice = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Alice',
_163
attributes=json.dumps(alice_attributes))
_163
_163
bob_attributes = {
_163
"products": ["ProgrammableSMS"],
_163
"contact_uri": BOB_NUMBER
_163
}
_163
_163
bob = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Bob',
_163
attributes=json.dumps(bob_attributes))
_163
_163
return {BOB_NUMBER: bob.sid, ALICE_NUMBER: alice.sid}
_163
_163
_163
def create_task_queues(client, workspace, activities):
_163
default_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Default',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='1==1')
_163
_163
sms_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='SMS',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableSMS" in products')
_163
_163
voice_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Voice',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableVoice" in products')
_163
_163
return {'sms': sms_queue, 'voice': voice_queue, 'default': default_queue}
_163
_163
_163
def create_workflow(client, workspace, queues):
_163
defaultTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
smsTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
voiceTarget = {
_163
'queue': queues['voice'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
default_filter = {
_163
'filter_friendly_name': 'Default Filter',
_163
'queue': queues['default'].sid,
_163
'Expression': '1==1',
_163
'priority': 1,
_163
'timeout': 30
_163
}
_163
_163
voiceFilter = {
_163
'filter_friendly_name': 'Voice Filter',
_163
'expression': 'selected_product=="ProgrammableVoice"',
_163
'targets': [voiceTarget, defaultTarget]
_163
}
_163
_163
smsFilter = {
_163
'filter_friendly_name': 'SMS Filter',
_163
'expression': 'selected_product=="ProgrammableSMS"',
_163
'targets': [smsTarget, defaultTarget]
_163
}
_163
_163
config = {
_163
'task_routing': {
_163
'filters': [voiceFilter, smsFilter],
_163
'default_filter': default_filter
_163
}
_163
}
_163
_163
callback_url = HOST + '/assignment/'
_163
_163
return client.taskrouter.workspaces(workspace.sid)\
_163
.workflows.create(friendly_name='Sales',
_163
assignment_callback_url=callback_url,
_163
fallback_assignment_callback_url=callback_url,
_163
task_reservation_timeout=15,
_163
configuration=json.dumps(config))

We have a Workspace, Workers and Task Queues... what's left? A Workflow. Let's see how to create one next!


Finally, we create the Workflow using the following parameters:

  1. friendly_name as the name of a Workflow.
  2. assignment_callback_url and fallback_assignment_callback_url 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.
  3. task_reservation_timeout as the maximum time we want to wait until a Worker is available for handling a Task.
  4. configuration which is a set of rules for placing Tasks into Task Queues. The routing configuration will take a Task attribute and match it 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.

task_router/workspace.py


_163
import json
_163
_163
from django.conf import settings
_163
from twilio.rest import Client
_163
_163
HOST = settings.HOST
_163
ALICE_NUMBER = settings.ALICE_NUMBER
_163
BOB_NUMBER = settings.BOB_NUMBER
_163
WORKSPACE_NAME = 'Twilio Workspace'
_163
_163
_163
def first(items):
_163
return items[0] if items else None
_163
_163
_163
def build_client():
_163
account_sid = settings.TWILIO_ACCOUNT_SID
_163
auth_token = settings.TWILIO_AUTH_TOKEN
_163
return Client(account_sid, auth_token)
_163
_163
_163
CACHE = {}
_163
_163
_163
def activities_dict(client, workspace_sid):
_163
activities = client.taskrouter.workspaces(workspace_sid)\
_163
.activities.list()
_163
_163
return {activity.friendly_name: activity for activity in activities}
_163
_163
_163
class WorkspaceInfo:
_163
_163
def __init__(self, workspace, workflow, activities, workers):
_163
self.workflow_sid = workflow.sid
_163
self.workspace_sid = workspace.sid
_163
self.activities = activities
_163
self.post_work_activity_sid = activities['Available'].sid
_163
self.workers = workers
_163
_163
_163
def setup():
_163
client = build_client()
_163
if 'WORKSPACE_INFO' not in CACHE:
_163
workspace = create_workspace(client)
_163
activities = activities_dict(client, workspace.sid)
_163
workers = create_workers(client, workspace, activities)
_163
queues = create_task_queues(client, workspace, activities)
_163
workflow = create_workflow(client, workspace, queues)
_163
CACHE['WORKSPACE_INFO'] = WorkspaceInfo(workspace, workflow, activities, workers)
_163
return CACHE['WORKSPACE_INFO']
_163
_163
_163
def create_workspace(client):
_163
try:
_163
workspace = first(client.taskrouter.workspaces.list(friendly_name=WORKSPACE_NAME))
_163
client.taskrouter.workspaces(workspace.sid).delete()
_163
except Exception:
_163
pass
_163
_163
events_callback = HOST + '/events/'
_163
_163
return client.taskrouter.workspaces.create(
_163
friendly_name=WORKSPACE_NAME,
_163
event_callback_url=events_callback,
_163
template=None)
_163
_163
_163
def create_workers(client, workspace, activities):
_163
alice_attributes = {
_163
"products": ["ProgrammableVoice"],
_163
"contact_uri": ALICE_NUMBER
_163
}
_163
_163
alice = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Alice',
_163
attributes=json.dumps(alice_attributes))
_163
_163
bob_attributes = {
_163
"products": ["ProgrammableSMS"],
_163
"contact_uri": BOB_NUMBER
_163
}
_163
_163
bob = client.taskrouter.workspaces(workspace.sid)\
_163
.workers.create(friendly_name='Bob',
_163
attributes=json.dumps(bob_attributes))
_163
_163
return {BOB_NUMBER: bob.sid, ALICE_NUMBER: alice.sid}
_163
_163
_163
def create_task_queues(client, workspace, activities):
_163
default_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Default',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='1==1')
_163
_163
sms_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='SMS',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableSMS" in products')
_163
_163
voice_queue = client.taskrouter.workspaces(workspace.sid).task_queues\
_163
.create(friendly_name='Voice',
_163
assignment_activity_sid=activities['Unavailable'].sid,
_163
target_workers='"ProgrammableVoice" in products')
_163
_163
return {'sms': sms_queue, 'voice': voice_queue, 'default': default_queue}
_163
_163
_163
def create_workflow(client, workspace, queues):
_163
defaultTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
smsTarget = {
_163
'queue': queues['sms'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
voiceTarget = {
_163
'queue': queues['voice'].sid,
_163
'priority': 5,
_163
'timeout': 30
_163
}
_163
_163
default_filter = {
_163
'filter_friendly_name': 'Default Filter',
_163
'queue': queues['default'].sid,
_163
'Expression': '1==1',
_163
'priority': 1,
_163
'timeout': 30
_163
}
_163
_163
voiceFilter = {
_163
'filter_friendly_name': 'Voice Filter',
_163
'expression': 'selected_product=="ProgrammableVoice"',
_163
'targets': [voiceTarget, defaultTarget]
_163
}
_163
_163
smsFilter = {
_163
'filter_friendly_name': 'SMS Filter',
_163
'expression': 'selected_product=="ProgrammableSMS"',
_163
'targets': [smsTarget, defaultTarget]
_163
}
_163
_163
config = {
_163
'task_routing': {
_163
'filters': [voiceFilter, smsFilter],
_163
'default_filter': default_filter
_163
}
_163
}
_163
_163
callback_url = HOST + '/assignment/'
_163
_163
return client.taskrouter.workspaces(workspace.sid)\
_163
.workflows.create(friendly_name='Sales',
_163
assignment_callback_url=callback_url,
_163
fallback_assignment_callback_url=callback_url,
_163
task_reservation_timeout=15,
_163
configuration=json.dumps(config))

Our workspace is completely setup. Now it's time to see how we use it to route calls.


Handle Twilio's Request

handle-twilios-request page anchor

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.

Handling Twilio's Requests

handling-twilios-requests page anchor

task_router/views.py


_128
import json
_128
from urllib.parse import quote_plus
_128
_128
from django.conf import settings
_128
from django.http import HttpResponse, JsonResponse
_128
from django.shortcuts import render
_128
from django.urls import reverse
_128
from django.views.decorators.csrf import csrf_exempt
_128
from twilio.rest import Client
_128
from twilio.twiml.messaging_response import MessagingResponse
_128
from twilio.twiml.voice_response import VoiceResponse
_128
_128
from . import sms_sender, workspace
_128
from .models import MissedCall
_128
_128
if not getattr(settings, 'TESTING', False):
_128
WORKSPACE_INFO = workspace.setup()
_128
else:
_128
WORKSPACE_INFO = None
_128
_128
ACCOUNT_SID = settings.TWILIO_ACCOUNT_SID
_128
AUTH_TOKEN = settings.TWILIO_AUTH_TOKEN
_128
TWILIO_NUMBER = settings.TWILIO_NUMBER
_128
EMAIL = settings.MISSED_CALLS_EMAIL_ADDRESS
_128
_128
_128
def root(request):
_128
""" Renders a missed calls list, with product and phone number """
_128
missed_calls = MissedCall.objects.order_by('-created')
_128
return render(request, 'index.html', {
_128
'missed_calls': missed_calls
_128
})
_128
_128
_128
@csrf_exempt
_128
def incoming_sms(request):
_128
""" Changes worker activity and returns a confirmation """
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
activity = 'Available' if request.POST['Body'].lower().strip() == 'on' else 'Offline'
_128
activity_sid = WORKSPACE_INFO.activities[activity].sid
_128
worker_sid = WORKSPACE_INFO.workers[request.POST['From']]
_128
workspace_sid = WORKSPACE_INFO.workspace_sid
_128
_128
client.workspaces(workspace_sid)\
_128
.workers(worker_sid)\
_128
.update(activity_sid=activity_sid)
_128
_128
resp = MessagingResponse()
_128
message = 'Your status has changed to ' + activity
_128
resp.message(message)
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def incoming_call(request):
_128
""" Returns TwiML instructions to Twilio's POST requests """
_128
resp = VoiceResponse()
_128
gather = resp.gather(numDigits=1, action=reverse('enqueue'), method="POST")
_128
gather.say("For Programmable SMS, press one. For Voice, press any other key.")
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def enqueue(request):
_128
""" Parses a selected product, creating a Task on Task Router Workflow """
_128
resp = VoiceResponse()
_128
digits = request.POST['Digits']
_128
selected_product = 'ProgrammableSMS' if digits == '1' else 'ProgrammableVoice'
_128
task = {'selected_product': selected_product}
_128
_128
enqueue = resp.enqueue(None, workflowSid=WORKSPACE_INFO.workflow_sid)
_128
enqueue.task(json.dumps(task))
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def assignment(request):
_128
""" Task assignment """
_128
response = {'instruction': 'dequeue',
_128
'post_work_activity_sid': WORKSPACE_INFO.post_work_activity_sid}
_128
return JsonResponse(response)
_128
_128
_128
@csrf_exempt
_128
def events(request):
_128
""" Events callback for missed calls """
_128
POST = request.POST
_128
event_type = POST.get('EventType')
_128
task_events = ['workflow.timeout', 'task.canceled']
_128
worker_event = 'worker.activity.update'
_128
_128
if event_type in task_events:
_128
task_attributes = json.loads(POST['TaskAttributes'])
_128
_save_missed_call(task_attributes)
_128
if event_type == 'workflow.timeout':
_128
_voicemail(task_attributes['call_sid'])
_128
elif event_type == worker_event and POST['WorkerActivityName'] == 'Offline':
_128
message = 'Your status has changed to Offline. Reply with '\
_128
'"On" to get back Online'
_128
worker_number = json.loads(POST['WorkerAttributes'])['contact_uri']
_128
sms_sender.send(to=worker_number, from_=TWILIO_NUMBER, body=message)
_128
_128
return HttpResponse('')
_128
_128
_128
def _voicemail(call_sid):
_128
msg = 'Sorry, All agents are busy. Please leave a message. We will call you as soon as possible'
_128
route_url = 'http://twimlets.com/voicemail?Email=' + EMAIL + '&Message=' + quote_plus(msg)
_128
route_call(call_sid, route_url)
_128
_128
_128
def route_call(call_sid, route_url):
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
client.api.calls(call_sid).update(url=route_url)
_128
_128
_128
def _save_missed_call(task_attributes):
_128
MissedCall.objects.create(
_128
phone_number=task_attributes['from'],
_128
selected_product=task_attributes['selected_product'])
_128
_128
_128
# Only used by the tests in order to patch requests before any call is made
_128
def setup_workspace():
_128
global WORKSPACE_INFO
_128
WORKSPACE_INFO = workspace.setup()

We just asked the caller to choose a product, next we will use their choice to create the appropiate Task.


This is the endpoint set as the action URL on the Gather verb on the previous step. A request is made to this endpoint when the user presses a key during the call. This request has a Digits parameter that holds the pressed keys. A Task will be created based on the pressed digit with the selected_product as an attribute. The Workflow will take this Task's attributes and match with the configured expressions in order to find a Task Queue for this Task, so an appropriate available Worker can be assigned to handle it.

We use the Enqueue verb with a WorkflowSid attribute to integrate with TaskRouter. Then the voice call will be put on hold while TaskRouter tries to find an available Worker to handle this Task.

task_router/views.py


_128
import json
_128
from urllib.parse import quote_plus
_128
_128
from django.conf import settings
_128
from django.http import HttpResponse, JsonResponse
_128
from django.shortcuts import render
_128
from django.urls import reverse
_128
from django.views.decorators.csrf import csrf_exempt
_128
from twilio.rest import Client
_128
from twilio.twiml.messaging_response import MessagingResponse
_128
from twilio.twiml.voice_response import VoiceResponse
_128
_128
from . import sms_sender, workspace
_128
from .models import MissedCall
_128
_128
if not getattr(settings, 'TESTING', False):
_128
WORKSPACE_INFO = workspace.setup()
_128
else:
_128
WORKSPACE_INFO = None
_128
_128
ACCOUNT_SID = settings.TWILIO_ACCOUNT_SID
_128
AUTH_TOKEN = settings.TWILIO_AUTH_TOKEN
_128
TWILIO_NUMBER = settings.TWILIO_NUMBER
_128
EMAIL = settings.MISSED_CALLS_EMAIL_ADDRESS
_128
_128
_128
def root(request):
_128
""" Renders a missed calls list, with product and phone number """
_128
missed_calls = MissedCall.objects.order_by('-created')
_128
return render(request, 'index.html', {
_128
'missed_calls': missed_calls
_128
})
_128
_128
_128
@csrf_exempt
_128
def incoming_sms(request):
_128
""" Changes worker activity and returns a confirmation """
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
activity = 'Available' if request.POST['Body'].lower().strip() == 'on' else 'Offline'
_128
activity_sid = WORKSPACE_INFO.activities[activity].sid
_128
worker_sid = WORKSPACE_INFO.workers[request.POST['From']]
_128
workspace_sid = WORKSPACE_INFO.workspace_sid
_128
_128
client.workspaces(workspace_sid)\
_128
.workers(worker_sid)\
_128
.update(activity_sid=activity_sid)
_128
_128
resp = MessagingResponse()
_128
message = 'Your status has changed to ' + activity
_128
resp.message(message)
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def incoming_call(request):
_128
""" Returns TwiML instructions to Twilio's POST requests """
_128
resp = VoiceResponse()
_128
gather = resp.gather(numDigits=1, action=reverse('enqueue'), method="POST")
_128
gather.say("For Programmable SMS, press one. For Voice, press any other key.")
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def enqueue(request):
_128
""" Parses a selected product, creating a Task on Task Router Workflow """
_128
resp = VoiceResponse()
_128
digits = request.POST['Digits']
_128
selected_product = 'ProgrammableSMS' if digits == '1' else 'ProgrammableVoice'
_128
task = {'selected_product': selected_product}
_128
_128
enqueue = resp.enqueue(None, workflowSid=WORKSPACE_INFO.workflow_sid)
_128
enqueue.task(json.dumps(task))
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def assignment(request):
_128
""" Task assignment """
_128
response = {'instruction': 'dequeue',
_128
'post_work_activity_sid': WORKSPACE_INFO.post_work_activity_sid}
_128
return JsonResponse(response)
_128
_128
_128
@csrf_exempt
_128
def events(request):
_128
""" Events callback for missed calls """
_128
POST = request.POST
_128
event_type = POST.get('EventType')
_128
task_events = ['workflow.timeout', 'task.canceled']
_128
worker_event = 'worker.activity.update'
_128
_128
if event_type in task_events:
_128
task_attributes = json.loads(POST['TaskAttributes'])
_128
_save_missed_call(task_attributes)
_128
if event_type == 'workflow.timeout':
_128
_voicemail(task_attributes['call_sid'])
_128
elif event_type == worker_event and POST['WorkerActivityName'] == 'Offline':
_128
message = 'Your status has changed to Offline. Reply with '\
_128
'"On" to get back Online'
_128
worker_number = json.loads(POST['WorkerAttributes'])['contact_uri']
_128
sms_sender.send(to=worker_number, from_=TWILIO_NUMBER, body=message)
_128
_128
return HttpResponse('')
_128
_128
_128
def _voicemail(call_sid):
_128
msg = 'Sorry, All agents are busy. Please leave a message. We will call you as soon as possible'
_128
route_url = 'http://twimlets.com/voicemail?Email=' + EMAIL + '&Message=' + quote_plus(msg)
_128
route_call(call_sid, route_url)
_128
_128
_128
def route_call(call_sid, route_url):
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
client.api.calls(call_sid).update(url=route_url)
_128
_128
_128
def _save_missed_call(task_attributes):
_128
MissedCall.objects.create(
_128
phone_number=task_attributes['from'],
_128
selected_product=task_attributes['selected_product'])
_128
_128
_128
# Only used by the tests in order to patch requests before any call is made
_128
def setup_workspace():
_128
global WORKSPACE_INFO
_128
WORKSPACE_INFO = workspace.setup()

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:

  1. The Task's Assignment Status is set to 'reserved'.
  2. A Reservation instance is generated, linking the Task to the selected Worker.
  3. At the same time the Reservation is created, a POST request is made to the Workflow's AssignmentCallbackURL, which was configured while creating the Workflow. This request includes the full details of the Task, the selected Worker, and the Reservation.

Handling this Assignment Callback is a key component of building a TaskRouter application as we can instruct how the Worker will handle a Task. We could send a text, email, push notifications or make a call.

Since we created this Task during a voice call with an Enqueue verb, let's instruct TaskRouter to dequeue the call and dial a Worker. If we do not specify a to parameter with a phone number, TaskRouter will pick the Worker's contact_uri attribute.

We also send a post_work_activity_sid which will tell TaskRouter which Activity to assign this worker after the call ends.

task_router/views.py


_128
import json
_128
from urllib.parse import quote_plus
_128
_128
from django.conf import settings
_128
from django.http import HttpResponse, JsonResponse
_128
from django.shortcuts import render
_128
from django.urls import reverse
_128
from django.views.decorators.csrf import csrf_exempt
_128
from twilio.rest import Client
_128
from twilio.twiml.messaging_response import MessagingResponse
_128
from twilio.twiml.voice_response import VoiceResponse
_128
_128
from . import sms_sender, workspace
_128
from .models import MissedCall
_128
_128
if not getattr(settings, 'TESTING', False):
_128
WORKSPACE_INFO = workspace.setup()
_128
else:
_128
WORKSPACE_INFO = None
_128
_128
ACCOUNT_SID = settings.TWILIO_ACCOUNT_SID
_128
AUTH_TOKEN = settings.TWILIO_AUTH_TOKEN
_128
TWILIO_NUMBER = settings.TWILIO_NUMBER
_128
EMAIL = settings.MISSED_CALLS_EMAIL_ADDRESS
_128
_128
_128
def root(request):
_128
""" Renders a missed calls list, with product and phone number """
_128
missed_calls = MissedCall.objects.order_by('-created')
_128
return render(request, 'index.html', {
_128
'missed_calls': missed_calls
_128
})
_128
_128
_128
@csrf_exempt
_128
def incoming_sms(request):
_128
""" Changes worker activity and returns a confirmation """
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
activity = 'Available' if request.POST['Body'].lower().strip() == 'on' else 'Offline'
_128
activity_sid = WORKSPACE_INFO.activities[activity].sid
_128
worker_sid = WORKSPACE_INFO.workers[request.POST['From']]
_128
workspace_sid = WORKSPACE_INFO.workspace_sid
_128
_128
client.workspaces(workspace_sid)\
_128
.workers(worker_sid)\
_128
.update(activity_sid=activity_sid)
_128
_128
resp = MessagingResponse()
_128
message = 'Your status has changed to ' + activity
_128
resp.message(message)
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def incoming_call(request):
_128
""" Returns TwiML instructions to Twilio's POST requests """
_128
resp = VoiceResponse()
_128
gather = resp.gather(numDigits=1, action=reverse('enqueue'), method="POST")
_128
gather.say("For Programmable SMS, press one. For Voice, press any other key.")
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def enqueue(request):
_128
""" Parses a selected product, creating a Task on Task Router Workflow """
_128
resp = VoiceResponse()
_128
digits = request.POST['Digits']
_128
selected_product = 'ProgrammableSMS' if digits == '1' else 'ProgrammableVoice'
_128
task = {'selected_product': selected_product}
_128
_128
enqueue = resp.enqueue(None, workflowSid=WORKSPACE_INFO.workflow_sid)
_128
enqueue.task(json.dumps(task))
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def assignment(request):
_128
""" Task assignment """
_128
response = {'instruction': 'dequeue',
_128
'post_work_activity_sid': WORKSPACE_INFO.post_work_activity_sid}
_128
return JsonResponse(response)
_128
_128
_128
@csrf_exempt
_128
def events(request):
_128
""" Events callback for missed calls """
_128
POST = request.POST
_128
event_type = POST.get('EventType')
_128
task_events = ['workflow.timeout', 'task.canceled']
_128
worker_event = 'worker.activity.update'
_128
_128
if event_type in task_events:
_128
task_attributes = json.loads(POST['TaskAttributes'])
_128
_save_missed_call(task_attributes)
_128
if event_type == 'workflow.timeout':
_128
_voicemail(task_attributes['call_sid'])
_128
elif event_type == worker_event and POST['WorkerActivityName'] == 'Offline':
_128
message = 'Your status has changed to Offline. Reply with '\
_128
'"On" to get back Online'
_128
worker_number = json.loads(POST['WorkerAttributes'])['contact_uri']
_128
sms_sender.send(to=worker_number, from_=TWILIO_NUMBER, body=message)
_128
_128
return HttpResponse('')
_128
_128
_128
def _voicemail(call_sid):
_128
msg = 'Sorry, All agents are busy. Please leave a message. We will call you as soon as possible'
_128
route_url = 'http://twimlets.com/voicemail?Email=' + EMAIL + '&Message=' + quote_plus(msg)
_128
route_call(call_sid, route_url)
_128
_128
_128
def route_call(call_sid, route_url):
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
client.api.calls(call_sid).update(url=route_url)
_128
_128
_128
def _save_missed_call(task_attributes):
_128
MissedCall.objects.create(
_128
phone_number=task_attributes['from'],
_128
selected_product=task_attributes['selected_product'])
_128
_128
_128
# Only used by the tests in order to patch requests before any call is made
_128
def setup_workspace():
_128
global WORKSPACE_INFO
_128
WORKSPACE_INFO = workspace.setup()

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(link takes you to an external page). Twimlets are tiny web applications for voice. This one will generate a TwiML response using Say verb and record a message using Record verb. The recorded message will then be transcribed and sent to the email address configured.

Note that we are also listening for task.canceled. This is triggered when the customer hangs up before being assigned to an agent, therefore canceling the task. Capturing this event allows us to collect the information from the customers that hang up before the Workflow times out.

task_router/views.py


_128
import json
_128
from urllib.parse import quote_plus
_128
_128
from django.conf import settings
_128
from django.http import HttpResponse, JsonResponse
_128
from django.shortcuts import render
_128
from django.urls import reverse
_128
from django.views.decorators.csrf import csrf_exempt
_128
from twilio.rest import Client
_128
from twilio.twiml.messaging_response import MessagingResponse
_128
from twilio.twiml.voice_response import VoiceResponse
_128
_128
from . import sms_sender, workspace
_128
from .models import MissedCall
_128
_128
if not getattr(settings, 'TESTING', False):
_128
WORKSPACE_INFO = workspace.setup()
_128
else:
_128
WORKSPACE_INFO = None
_128
_128
ACCOUNT_SID = settings.TWILIO_ACCOUNT_SID
_128
AUTH_TOKEN = settings.TWILIO_AUTH_TOKEN
_128
TWILIO_NUMBER = settings.TWILIO_NUMBER
_128
EMAIL = settings.MISSED_CALLS_EMAIL_ADDRESS
_128
_128
_128
def root(request):
_128
""" Renders a missed calls list, with product and phone number """
_128
missed_calls = MissedCall.objects.order_by('-created')
_128
return render(request, 'index.html', {
_128
'missed_calls': missed_calls
_128
})
_128
_128
_128
@csrf_exempt
_128
def incoming_sms(request):
_128
""" Changes worker activity and returns a confirmation """
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
activity = 'Available' if request.POST['Body'].lower().strip() == 'on' else 'Offline'
_128
activity_sid = WORKSPACE_INFO.activities[activity].sid
_128
worker_sid = WORKSPACE_INFO.workers[request.POST['From']]
_128
workspace_sid = WORKSPACE_INFO.workspace_sid
_128
_128
client.workspaces(workspace_sid)\
_128
.workers(worker_sid)\
_128
.update(activity_sid=activity_sid)
_128
_128
resp = MessagingResponse()
_128
message = 'Your status has changed to ' + activity
_128
resp.message(message)
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def incoming_call(request):
_128
""" Returns TwiML instructions to Twilio's POST requests """
_128
resp = VoiceResponse()
_128
gather = resp.gather(numDigits=1, action=reverse('enqueue'), method="POST")
_128
gather.say("For Programmable SMS, press one. For Voice, press any other key.")
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def enqueue(request):
_128
""" Parses a selected product, creating a Task on Task Router Workflow """
_128
resp = VoiceResponse()
_128
digits = request.POST['Digits']
_128
selected_product = 'ProgrammableSMS' if digits == '1' else 'ProgrammableVoice'
_128
task = {'selected_product': selected_product}
_128
_128
enqueue = resp.enqueue(None, workflowSid=WORKSPACE_INFO.workflow_sid)
_128
enqueue.task(json.dumps(task))
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def assignment(request):
_128
""" Task assignment """
_128
response = {'instruction': 'dequeue',
_128
'post_work_activity_sid': WORKSPACE_INFO.post_work_activity_sid}
_128
return JsonResponse(response)
_128
_128
_128
@csrf_exempt
_128
def events(request):
_128
""" Events callback for missed calls """
_128
POST = request.POST
_128
event_type = POST.get('EventType')
_128
task_events = ['workflow.timeout', 'task.canceled']
_128
worker_event = 'worker.activity.update'
_128
_128
if event_type in task_events:
_128
task_attributes = json.loads(POST['TaskAttributes'])
_128
_save_missed_call(task_attributes)
_128
if event_type == 'workflow.timeout':
_128
_voicemail(task_attributes['call_sid'])
_128
elif event_type == worker_event and POST['WorkerActivityName'] == 'Offline':
_128
message = 'Your status has changed to Offline. Reply with '\
_128
'"On" to get back Online'
_128
worker_number = json.loads(POST['WorkerAttributes'])['contact_uri']
_128
sms_sender.send(to=worker_number, from_=TWILIO_NUMBER, body=message)
_128
_128
return HttpResponse('')
_128
_128
_128
def _voicemail(call_sid):
_128
msg = 'Sorry, All agents are busy. Please leave a message. We will call you as soon as possible'
_128
route_url = 'http://twimlets.com/voicemail?Email=' + EMAIL + '&Message=' + quote_plus(msg)
_128
route_call(call_sid, route_url)
_128
_128
_128
def route_call(call_sid, route_url):
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
client.api.calls(call_sid).update(url=route_url)
_128
_128
_128
def _save_missed_call(task_attributes):
_128
MissedCall.objects.create(
_128
phone_number=task_attributes['from'],
_128
selected_product=task_attributes['selected_product'])
_128
_128
_128
# Only used by the tests in order to patch requests before any call is made
_128
def setup_workspace():
_128
global WORKSPACE_INFO
_128
WORKSPACE_INFO = workspace.setup()

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.


Change a Worker's Activity

change-a-workers-activity page anchor

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.

Changing a Worker's Activity

changing-a-workers-activity page anchor

task_router/views.py


_128
import json
_128
from urllib.parse import quote_plus
_128
_128
from django.conf import settings
_128
from django.http import HttpResponse, JsonResponse
_128
from django.shortcuts import render
_128
from django.urls import reverse
_128
from django.views.decorators.csrf import csrf_exempt
_128
from twilio.rest import Client
_128
from twilio.twiml.messaging_response import MessagingResponse
_128
from twilio.twiml.voice_response import VoiceResponse
_128
_128
from . import sms_sender, workspace
_128
from .models import MissedCall
_128
_128
if not getattr(settings, 'TESTING', False):
_128
WORKSPACE_INFO = workspace.setup()
_128
else:
_128
WORKSPACE_INFO = None
_128
_128
ACCOUNT_SID = settings.TWILIO_ACCOUNT_SID
_128
AUTH_TOKEN = settings.TWILIO_AUTH_TOKEN
_128
TWILIO_NUMBER = settings.TWILIO_NUMBER
_128
EMAIL = settings.MISSED_CALLS_EMAIL_ADDRESS
_128
_128
_128
def root(request):
_128
""" Renders a missed calls list, with product and phone number """
_128
missed_calls = MissedCall.objects.order_by('-created')
_128
return render(request, 'index.html', {
_128
'missed_calls': missed_calls
_128
})
_128
_128
_128
@csrf_exempt
_128
def incoming_sms(request):
_128
""" Changes worker activity and returns a confirmation """
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
activity = 'Available' if request.POST['Body'].lower().strip() == 'on' else 'Offline'
_128
activity_sid = WORKSPACE_INFO.activities[activity].sid
_128
worker_sid = WORKSPACE_INFO.workers[request.POST['From']]
_128
workspace_sid = WORKSPACE_INFO.workspace_sid
_128
_128
client.workspaces(workspace_sid)\
_128
.workers(worker_sid)\
_128
.update(activity_sid=activity_sid)
_128
_128
resp = MessagingResponse()
_128
message = 'Your status has changed to ' + activity
_128
resp.message(message)
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def incoming_call(request):
_128
""" Returns TwiML instructions to Twilio's POST requests """
_128
resp = VoiceResponse()
_128
gather = resp.gather(numDigits=1, action=reverse('enqueue'), method="POST")
_128
gather.say("For Programmable SMS, press one. For Voice, press any other key.")
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def enqueue(request):
_128
""" Parses a selected product, creating a Task on Task Router Workflow """
_128
resp = VoiceResponse()
_128
digits = request.POST['Digits']
_128
selected_product = 'ProgrammableSMS' if digits == '1' else 'ProgrammableVoice'
_128
task = {'selected_product': selected_product}
_128
_128
enqueue = resp.enqueue(None, workflowSid=WORKSPACE_INFO.workflow_sid)
_128
enqueue.task(json.dumps(task))
_128
_128
return HttpResponse(resp)
_128
_128
_128
@csrf_exempt
_128
def assignment(request):
_128
""" Task assignment """
_128
response = {'instruction': 'dequeue',
_128
'post_work_activity_sid': WORKSPACE_INFO.post_work_activity_sid}
_128
return JsonResponse(response)
_128
_128
_128
@csrf_exempt
_128
def events(request):
_128
""" Events callback for missed calls """
_128
POST = request.POST
_128
event_type = POST.get('EventType')
_128
task_events = ['workflow.timeout', 'task.canceled']
_128
worker_event = 'worker.activity.update'
_128
_128
if event_type in task_events:
_128
task_attributes = json.loads(POST['TaskAttributes'])
_128
_save_missed_call(task_attributes)
_128
if event_type == 'workflow.timeout':
_128
_voicemail(task_attributes['call_sid'])
_128
elif event_type == worker_event and POST['WorkerActivityName'] == 'Offline':
_128
message = 'Your status has changed to Offline. Reply with '\
_128
'"On" to get back Online'
_128
worker_number = json.loads(POST['WorkerAttributes'])['contact_uri']
_128
sms_sender.send(to=worker_number, from_=TWILIO_NUMBER, body=message)
_128
_128
return HttpResponse('')
_128
_128
_128
def _voicemail(call_sid):
_128
msg = 'Sorry, All agents are busy. Please leave a message. We will call you as soon as possible'
_128
route_url = 'http://twimlets.com/voicemail?Email=' + EMAIL + '&Message=' + quote_plus(msg)
_128
route_call(call_sid, route_url)
_128
_128
_128
def route_call(call_sid, route_url):
_128
client = Client(ACCOUNT_SID, AUTH_TOKEN)
_128
client.api.calls(call_sid).update(url=route_url)
_128
_128
_128
def _save_missed_call(task_attributes):
_128
MissedCall.objects.create(
_128
phone_number=task_attributes['from'],
_128
selected_product=task_attributes['selected_product'])
_128
_128
_128
# Only used by the tests in order to patch requests before any call is made
_128
def setup_workspace():
_128
global WORKSPACE_INFO
_128
WORKSPACE_INFO = workspace.setup()

Congratulations! You finished this tutorial. As you can see, using Twilio's TaskRouter is quite simple.


If you're a Python developer working with Twilio, you might enjoy these other tutorials:

Appointment-Reminders

Automate the process of reaching out to your customers prior to an upcoming appointment.

Automated-Survey-Django(link takes you to an external page)

Instantly collect structured data from your users with a survey conducted over a call or SMS text messages.

Did this help?

did-this-help page anchor

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio(link takes you to an external page) to let us know what you think!


Rate this page: