Most customer communication tools treat email, SMS, and voice as separate channels with separate contexts. A customer emails about a billing dispute, then gets a text that has no idea about the email, then receives a call that starts from scratch. The context is lost at every channel boundary.
This sample builds an AI agent that treats all three channels as one conversation. Claude API decides which channel to use via tool-calling. Telnyx delivers the email, SMS, and voice call. SQLite stores every interaction across all channels so the agent always has the full picture.
What the sample does
The Omnichannel AI Agent receives a customer scenario, retrieves the cross-channel conversation history from SQLite, and passes it to Claude along with four tool definitions: send_email, send_sms, make_call, and resolve_issue. Claude decides which tool to call based on context — email for formal acknowledgments, SMS for quick updates, voice for complex resolution.
Each tool call hits a Telnyx API endpoint. Every message sent or received is stored in SQLite, so the next tool call has the full cross-channel history. The agent references previous interactions: the SMS mentions the email, the voice call mentions both.
A demo server (demo/demo_server.py) runs the full pipeline without Telnyx or Claude credentials, walking through a billing dispute scenario with mocked API responses.
Architecture
Claude API (AI Brain)
│
Tool-calling decides
which channel to use
│
┌────┴────┐
│ Flask │
│ app.py │
│ │
┌─────┤ Context ├─────┐
│ │ (SQLite)│ │
│ └────┬────┘ │
│ │ │
Email API Messaging Call Control
POST /v2/ POST /v2/ POST /v2/
email_messages messages calls
│ │ │
▼ ▼ ▼
Customer Customer Customer
inbox phone phone
The AI agent loop works like any Claude tool-calling loop:
- Send the conversation history and tool definitions to Claude
- Claude responds with a
tool_useblock (e.g.,send_email) - Execute the tool (hit the Telnyx API, store the message)
- Send the tool result back to Claude
- Repeat until Claude stops calling tools (
stop_reason: "end_turn")
How the AI brain works
Claude receives four tools and the full conversation history. The tool definitions guide the AI toward appropriate channel selection:
TOOLS = [
{
"name": "send_email",
"description": (
"Send a detailed email to the customer. Use for formal "
"acknowledgments, detailed explanations, or when a written "
"record is needed."
),
"input_schema": {
"type": "object",
"properties": {
"subject": {"type": "string", "description": "Email subject line"},
"body": {"type": "string", "description": "Email body text"},
},
"required": ["subject", "body"],
},
},
{
"name": "send_sms",
"description": (
"Send a short SMS text message to the customer. Use for quick "
"status updates, confirmations, or time-sensitive notifications."
),
"input_schema": {
"type": "object",
"properties": {
"text": {"type": "string", "description": "SMS message text"},
},
"required": ["text"],
},
},
{
"name": "make_call",
"description": (
"Call the customer and speak a message. Use for complex "
"resolution, urgent matters, or when a personal touch is needed."
),
"input_schema": {
"type": "object",
"properties": {
"speak_text": {"type": "string", "description": "Text to speak when answered"},
},
"required": ["speak_text"],
},
},
{
"name": "resolve_issue",
"description": "Mark the customer issue as resolved.",
"input_schema": {
"type": "object",
"properties": {
"summary": {"type": "string", "description": "Resolution summary"},
},
"required": ["summary"],
},
},
]
The tool descriptions act as channel routing rules. Claude reads "formal acknowledgments" and knows to email. It reads "quick status updates" and sends an SMS. The AI does not follow hardcoded routing logic — it reasons about the best channel for each interaction.
Sending email via the Telnyx Email API
The Telnyx Email API (GA August 2026) uses a simple REST endpoint. No SMTP configuration, no third-party email service — it is part of the same Telnyx platform that handles voice and messaging.
def send_email(to_email, subject, body):
resp = requests.post(
"https://api.telnyx.com/v2/email_messages",
headers={
"Authorization": f"Bearer {TELNYX_API_KEY}",
"Content-Type": "application/json",
},
json={
"from": {"email": TELNYX_EMAIL_FROM},
"to": [{"email": to_email}],
"subject": subject,
"body": body,
},
timeout=15,
)
resp.raise_for_status()
return resp.json()
One API key. Same authentication as messaging and voice. This is the architectural advantage of running email, SMS, and voice on one platform — the agent does not need separate credentials or SDKs for each channel.
Persistent context in SQLite
Every interaction is stored with the customer ID, channel, role, content, and timestamp:
def store_message(customer_id, channel, role, content):
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute(
"INSERT INTO conversations (customer_id, channel, role, content, timestamp) "
"VALUES (?, ?, ?, ?, ?)",
(customer_id, channel, role, content,
datetime.now(timezone.utc).isoformat()),
)
conn.commit()
conn.close()
Before each agent run, the full history is retrieved and included in the Claude prompt:
history = get_conversation_history(customer["id"])
This is what makes it omnichannel. The agent has a single timeline of every email, text, and call with each customer. When Claude generates an SMS, it can reference the email it sent earlier because the email is in the conversation history.
The agentic loop
The agent runs a standard Claude tool-calling loop:
def run_agent(customer, scenario):
messages = [{"role": "user", "content": f"Customer: {customer['name']}...\nIssue: {scenario}"}]
actions = []
while True:
response = claude_client.messages.create(
model="claude-opus-4-6",
max_tokens=4096,
system=SYSTEM_PROMPT,
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
break
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool in tool_use_blocks:
result = execute_tool(tool.name, tool.input, customer)
actions.append({"type": "tool_call", "tool": tool.name, "result": result})
tool_results.append({
"type": "tool_result",
"tool_use_id": tool.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
return actions
Claude keeps calling tools until it decides the issue is resolved. For a billing dispute, a typical sequence is: email acknowledgment → SMS confirmation → voice call for resolution → resolve_issue.
Run the demo locally
No credentials needed. The demo server mocks both Telnyx and Claude APIs:
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/omnichannel-ai-agent-python
pip install -r requirements.txt
python demo/demo_server.py
The demo automatically triggers the agent with a billing dispute scenario. Watch the console as each channel fires in sequence — email, SMS, voice. Then check the conversation history:
curl http://localhost:5555/conversations | python -m json.tool
Connect to live Telnyx and Claude APIs
Set these environment variables and run python app.py:
| Variable | Description |
|---|---|
TELNYX_API_KEY | Telnyx API v2 key |
ANTHROPIC_API_KEY | Claude API key |
TELNYX_FROM_NUMBER | Telnyx phone number for SMS + Voice |
TELNYX_EMAIL_FROM | Verified sender email address |
CONNECTION_ID | Call Control Application connection ID |
Then trigger the agent:
curl -X POST http://localhost:5000/agent/run \
-H "Content-Type: application/json" \
-d '{
"customer": {"id": "cust_001", "name": "Sarah Chen", "email": "sarah@example.com", "phone": "+15551234567"},
"scenario": "Billing dispute for $147.50"
}'
What I would extend next
The current agent is outbound-only — it proactively contacts customers. Webhook endpoints for inbound SMS, email, and voice replies are wired up but do not re-trigger the agent. A natural extension is closing the loop: when a customer replies to the SMS, the agent processes the response and takes the next action.
The SQLite context store works for demos and single-server deployments. For production multi-instance deployments, replace it with a shared store — PostgreSQL, Redis, or the Telnyx KV API.
Testing
11 smoke tests cover module imports, route registration, database operations, tool definitions, and API endpoints:
python -m pytest smoke_test.py -v
Related Examples
- Omnichannel AI Receptionist (Python)
- AI Email Agent (Python)
- AI Voice Agent with Function Calling (Python)