Integrating Twilio Agent Connect with Twilio Flex for Human Handoff

June 09, 2026
Written by
Reviewed by

Integrating Twilio Agent Connect with Twilio Flex for Human Handoff

In the "agentic" era of customer service, the objective isn’t simply to automate interactions with AI it’s to ensure that when a human agent steps in, they have complete context from every prior interaction. By leveraging Twilio Agent Connect (TAC), Conversation Memory, and Twilio Flex, you can create seamless transitions where no information is lost between AI and human support.

In this tutorial, you’ll learn how to build an intelligent escalation workflow that uses Conversation Memory to retain key user details including custom traits across sessions.

We’ll demonstrate how to manually add a trait (such as a preferred language) to a user’s memory profile, and show how this trait can be utilized by the bot to personalize interactions.

When escalation to a live agent is required, the system will transfer the conversation to Twilio Flex, preserving the full chat history and providing a summary including any relevant traits to the human agent.

We’ll orchestrate this solution using Twilio Agent Connect (TAC), Conversation Intelligence V3, Twilio Flex, and OpenAI to deliver smart, context-aware bot interactions, and ensure a smooth, informed handoff to live support when needed.

Prerequisites

To successfully deploy this project, you will need:

Step 1: Project Setup & Initialization

Open your terminal, navigate to your desired project directory, and execute the following commands to install the required SDKs and dependencies:

pip install twilio-agent-connect openai python-dotenv

In your project directory create a .env file, then open the .env file in your preferred text editor and fill out below fields for now:

# Twilio Core Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here

# Twilio API Credentials (Generate via 1Console >Settings > Account Settings > API keys & auth tokens > create a standard API key.)
TWILIO_API_KEY=SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_API_SECRET=your_api_secret_here

# Third-Party Integrations
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Application Configurations
TWILIO_PHONE_NUMBER=+1XXXXXXXXXX
TWILIO_LOG_LEVEL=DEBUG

# We will fill these fields in sometime
TWILIO_CONVERSATION_CONFIGURATION_ID=
TWILIO_VOICE_PUBLIC_DOMAIN= 
TWILIO_STUDIO_HANDOFF_FLOW_SID=

Step 2: Provision Flex and Configure Conversations (Classic)

Step 1: If you don't have a Flex instance running yet, follow the Twilio Flex Account Setup Guide to set up Flex in your account.

This automatically creates a default Flex Conversation Service behind the scenes.

Tip : If you want Flex to access your existing phone numbers and resources, review the public beta terms here, check the agreement box, and click Add Flex.

Step 2: Now, you need to create an address rule so Twilio knows what to do when a text hits your phone number.

  1. In Twilio Console navigate to Conversations (classic) and select Addresses.
  2. Click Create Address (or select your active Twilio number from the list).
  3. Under Address Configuration, select Auto-create a conversation to Yes.
  4. Choose your Conversation Service: Select Flex Conversation Service from the dropdown.
    1. Quick sanity check: Make sure this matches the service listed under Conversations (classic) > Settings > Default, or your routing will break.
  5. For the "Want to set up an integration?" prompt, select No. (We don't want a hardcoded webhook here because our Studio flow and Orchestrator will handle the handoff logic dynamically).
  6. Save your changes.

Step 3: Configure the Twilio Conversation Orchestrator

The Conversation Orchestrator handles interaction traffic and acts as the brains behind our routing and profiling.

  1. Open your Twilio Console and navigate to the Conversation Orchestrator dashboard.
  2. Create a new configuration and provide a distinct Conversation configuration name.
  3. For Conversation grouping, select Group by profile.
  4. Under the Webhook section, temporarily leave the Webhook URL and HTTP Method empty. We will return to update these once our tunnel is live.
  5. Do not Select any Twilio phone numbers in Messaging & Chat traffic.
  6. Check the box to enable Connect Conversations (classic) service or Flex and select Flex Conversation Service from the dropdown menu. This establishes the routing pipeline to your Flex instance for human agent handoffs.
Dropdown menu to select either Conversations (classic) service or Flex, with Flex Conversation Service selected.

7.    Do not enable the option to Set up automatic capture for Voice traffic.

8.    Enable Conversation memory, For initial implementations, click + Create new memory store and ensure both Observations and Summaries are toggled ON.

Interface to create a new memory store with a limit of 5 memory stores.

 

9.  Once you reach the Summary page, click on Create Conversation Configuration.

10.  The TWILIO_CONVERSATION_CONFIGURATION_ID is the Conversations configuration ID of our Conversation Orchestrator. Copy the Conversations configuration ID of Conversation orchestrator and fill in this field in .env file:

TWILIO_CONVERSATION_CONFIGURATION_ID=conv_configuration_xxxxxxxxxxxxxxxxxxxxxxxxxx

For TAC: If you have an existing Conversation Orchestrator setup, and if you have enabled phone numbers in the conversation orchestrator’s Channel Traffic then leaving your Twilio number assigned in both places causes a conflict because the Conversation Orchestrator tries to process the incoming SMS traffic twice.

To make this work seamlessly with Twilio Agent Connect (TAC), remove the Twilio phone number as a sender from all channels under Automatically captured traffic in Channel Traffic.

Screenshot showing option to remove communication traffic from a phone number in a configuration interface.

Step 4: Establish Conversation Intelligence Rules

Next, we need to bind a Conversation Intelligence configuration to our orchestrator to synthesize summaries and suggest actions.

  1. Go to Products & Services > Conversation Orchestrator > Conversation Intelligence > Intelligence configurations.
  2. Link your new intelligence profile directly with the Conversation Orchestrator you built in Step 2.
  3. Click on your newly created Intelligence configuration to view its parameters.
  4. Inside the Details tab, locate the Rules section and click Create Rule.
  5. Select Summary & Next-Best-Response as your primary language operator and hit next.
  6. Set the activation trigger to At conversation end and leave the subsequent Webhook action blank.
  7. In the Add Context section, select Enable Conversation Memory for this rule.
  8. Save changes
  9. Now, create a second rule within the same configuration profile. Set its parameters identical to the first, but change the trigger to Conversation moved to inactive. This ensures you capture summaries if a chat simply timeouts or gets closed silently.
Screenshot of a rule creation page showing trigger and action settings in a web interface.

10. Now you will have 2 rules under your Intelligence Configurations service.

Step 5: Expose Local Environment via ngrok

Twilio Agent Connect (TAC) runs on port 8000 locally. We must expose this specific port to the web so Twilio can send incoming event payload data.

Open a fresh terminal window and initialize an HTTP tunnel on port 8000:

ngrok http 8000

(Note: Ensure your ngrok configuration targets port 8000 to match the TAC environment runtime port)

ngrok will output a dynamic forwarding address similar to: https://a1b2-34-56-78.ngrok-free.app

Update Configuration Endpoints:

Return to your local .env configuration file and pass your public ngrok URL to the TWILIO_VOICE_PUBLIC_DOMAIN variable without the https:// prefix:

TWILIO_VOICE_PUBLIC_DOMAIN=a1b2-34-56-78.ngrok-free.app

Next,

For Voice traffic: Navigate to Numbers and Senders > Overview, select your number, and set the Voice webhook to your ngrok URL with the /twiml endpoint.

  • Example: https://a1b2-34-56-78.ngrok-free.app/twiml

For the Orchestrator: Go to Conversation Orchestrator, edit your configuration, and set the webhook URL using the /webhook endpoint.

  • Example: https://a1b2-34-56-78.ngrok-free.app/webhook

Step 6: Linking the Studio Flow (Human Handoff)

This step ensures that when a handoff is triggered, the call or message is correctly routed through Twilio Studio to a Flex agent.

  1. Create Flow from Template: In the Twilio Console, navigate to Studio > Flows. Click Create flow, then select From template from the dropdown.
  2. Select the Handoff Template: Scroll to the bottom and select Twilio Agent Connect - Human Handoff. This template contains the pre-configured logic for TAC escalations.
  3. Configure Flex Routing: Open the flow editor and click on the send_to_flex widget. Under the "Workflow" settings, select your desired Flex workflow (e.g., Assign to Anyone).
  4. Capture Flow SID: Save and Publish the flow. Note the Flow SID (it starts with FW...).
  5. Finalize .env: Add this SID to your environment file:
TWILIO_STUDIO_HANDOFF_FLOW_SID=FWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Step 7: Create custom trait in Conversation Memory Store

To track specific user preferences like a language preference across multiple sessions, we need to define a custom trait in our Conversation Memory Store.

Configure the Custom Trait in the Twilio Console

  1. In the Twilio Console, navigate to the Memory Store you created earlier within the Conversation Memory.
  2. Click on the Traits tab.
  3. You will see the default trait group ( Contact traits). Scroll to the bottom of this section and click + Add trait.
  4. Configure the new trait with the following settings:
    1. Trait Name: Language
    2. Data Type: String
  5. Click Save.

Step 8: Implementing the Core TAC Backend Logic

Now that our Twilio infrastructure is fully mapped out, let's look at the Python backend script that coordinates the AI runner and the handoff execution loop.

The base code is available in this Github Repo. We are going to modify this base code in such a way that it will handle Conversations with memory retrieval enabled and it will retrieve added traits (ex: language) from customer memory.

Create a new file in your folder and name it agent_handoff.py, and copy paste the code given below:

from typing import Any
from agents import Agent, Runner, set_tracing_disabled
from dotenv import load_dotenv
from tac import TAC, TACConfig
from tac.channels.sms import SMSChannel, SMSChannelConfig
from tac.channels.voice import VoiceChannel, VoiceChannelConfig
from tac.models.session import ConversationSession
from tac.models.tac import TACMemoryResponse
from tac.server import TACFastAPIServer
from tac.tools.handoff import create_studio_handoff_tool
load_dotenv()
set_tracing_disabled(True)
tac = TAC(config=TACConfig.from_env())
# Verify the handoff-specific env var is set.
if not tac.config.studio_handoff_flow_sid:
    raise RuntimeError(
        "TWILIO_STUDIO_HANDOFF_FLOW_SID is required to run the handoff example. "
        "Set it in your .env (see .env.example)."
    )
SYSTEM_INSTRUCTIONS = (
    "You are a customer service agent speaking with a user over voice or SMS. "
    "Keep responses short and conversational — a sentence or two. "
    "Do not use markdown, asterisks, bullets, or emojis; your words will be "
    "spoken aloud or sent as plain text. "
    "If the user asks to speak with a human, or if you cannot resolve their issue, "
    "use the handoff tool to transfer them to a human agent."
)
# Example app-defined routing metadata attached to every handoff. Keys and
# values are arbitrary — pick whatever your downstream system expects. For
# Flex, these surface as TaskRouter task attributes.
HANDOFF_ATTRIBUTES = {
    "department": "support",
    "priority": "normal",
}
conversation_history: dict[str, list[Any]] = {}
async def handle_message_ready(
    user_message: str,
    context: ConversationSession,
    memory_response: TACMemoryResponse | None,
) -> str:
    #from app.py 
    conv_id = context.conversation_id
    from tac.tools.base import function_tool
    if conv_id not in conversation_history:
    #inital initilization : 
        conversation_history[conv_id] = [{"role": "system", "content": SYSTEM_INSTRUCTIONS}]
   #adding user message in conversation
    conversation_history[conv_id].append({"role": "user", "content": user_message})
    handoff_tool = create_studio_handoff_tool(tac, context, attributes=HANDOFF_ATTRIBUTES)
     # Access memory (observations, summaries, communications) @@@@@@@@@
    if memory_response:
            for obs in memory_response.observations:
                 print(obs.content)
                #  print(f"Observations @@@@@@@@@@: {memory_response.observations}")
    if (memory_response):
        # print(f"User traits: {context.profile.traits}")  
        profile = context.profile
        if profile and profile.traits:
             contact_group = profile.traits.get("Contact", {})
             language = contact_group.get("language") 
             print(f"User preferred language:  {language}")
             uselang = (f"Switch to language {language} for further communication,you can change the language to desired language if you are asked to change language")
             SYSTEM_INSTRUCTIONS_modified = SYSTEM_INSTRUCTIONS + uselang
        else :
               SYSTEM_INSTRUCTIONS_modified = SYSTEM_INSTRUCTIONS
    agent = Agent(
        name="Customer Service Agent",
        instructions=SYSTEM_INSTRUCTIONS_modified,
        tools=[handoff_tool.to_openai_agents_sdk_tool()],
    )
    history = conversation_history.get(context.conversation_id, [])
    agent_input = history + [{"role": "user", "content": user_message}]
#sending it to openAI, result is returned from openAI
    result = await Runner.run(agent, agent_input)
#result is added in conv history : 
    conversation_history[context.conversation_id] = result.to_input_list()
    return result.final_output_as(str)
voice_channel = VoiceChannel(tac, config=VoiceChannelConfig(memory_mode="always"))
sms_channel = SMSChannel(tac, config=SMSChannelConfig(memory_mode="always"))
tac.on_message_ready(handle_message_ready)
if __name__ == "__main__":
    server = TACFastAPIServer(
        tac=tac, voice_channel=voice_channel, messaging_channels=[sms_channel]
    )
    server.start()

Time to Test

Run the above code in terminal with command:

python agent_handoff.py
  1. Call or Text: Grab your phone and call or text your active Twilio number.
  2. Trigger Handoff: Interact with your bot, then ask to speak with a human.
  3. The Payload: The TAC SDK handles the request, packaging a payload containing the conversationSid and memoryStoreId directly to your Studio Flow.
  4. Flex Dashboard Access: In your Flex instance console, an agent will receive the incoming task request. Rather than seeing a blank chat window, the agent instantly receives the automated AI Summary generated by Conversation Intelligence. They know the customer's problem before they even say hello.

💡 How it Works

Every time a new Conversation is created, the system first checks if a Conversation Memory profile already exists for the participant (e.g., the customer’s phone number). If a profile exists, the Conversation is linked to that existing customer memory. If no profile exists, a new Conversation Memory profile is created for the participant.

For testing purposes, we will manually add a custom trait to the Conversation Memory profile.

This allows us to simulate and verify how traits are stored and retrieved within the memory system. By manually inserting a trait (such as language), we can observe how the Conversation will utilize this information during communication.

After initiating a Conversation, we will get the memory profile Id from Twilio console > Conversation memory > Memory store > Customer Profiles, copy both the Memory Store ID and the Customer Profile ID.

Update the Profile Trait via Python Code

Once the trait is defined in your console, you can programmatically update a customer's profile using the Twilio Python code.

import os
from twilio.rest import Client
from twilio.rest.memory.v1 import ProfileList
account_sid = os.environ["TWILIO_ACCOUNT_SID"]
auth_token =  os.environ["TWILIO_AUTH_TOKEN"]
client = Client(account_sid, auth_token)
profile_traits = client.memory.v1.profiles(
   "mem_store_00000000000000000000000000",
   "mem_profile_00000000000000000000000000",
).patch(
   profile_patch=ProfileList.ProfilePatch(
       {"traits": {"Contact": {"language": "spanish"}}}
   )
)
print(profile_traits.message)

After updating the language trait to Spanish in the Conversation Memory profile, the bot will communicate with the user in Spanish. When the user requests a handoff to a human agent, the chat will seamlessly transfer to a Human Agent in Flex. The conversation history is preserved, and a summary of the interaction will be provided to the agent.

Chat conversation showing messages between a user and a virtual assistant, ending with a transfer to a human agent.
Twilio Flex chat interface showing a conversation with a virtual agent transferring to a human agent

Conclusion

I hope that you’ve enjoyed this article and learned something new that could help your current or next project! If you’re curious what else you can build with Twilio Conversations dive deep into the developer docs.

We can’t wait to see what you build!

Simran Aishwarya is a Developer Support Engineer at Twilio, who specializes in communication platforms, backend systems, and technical problem solving. She works extensively with Twilio products including Voice, Flex, TaskRouter, Conversations, and Segment, and enjoys building practical solutions using Python, JavaScript, APIs, and real-time customer engagement technologies.