Support inboxes get messy fast.
One customer asks about an invoice. Another reports a bug. Someone else wants pricing. A fourth person sends a vague "can someone help me?" message that still needs to land somewhere useful.
Most teams solve this with layers:
- an SMS webhook receiver
- a classifier service
- a database for routing state
- a queue or ticketing integration
- a notification path back to the customer
That architecture works, but it can feel heavy for a simple first version.
This example shows a smaller pattern: use a Telnyx Edge Compute function plus an Agent SDK actor to receive, classify, route, reply, and remember SMS triage state in one deployable app.
Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/agent-sms-triage-bot
What the app does
agent-sms-triage-bot is a TypeScript example that runs on Telnyx Edge Compute.
When a customer sends an SMS, Telnyx sends a message.received webhook to the app. The app passes the sender and message text into a TriageAgent actor.
That actor does four things:
- classifies the message as
billing,support,sales, orgeneral - looks up the queue for that topic from durable actor state
- replies to the customer over SMS
- stores the triage entry and updates topic counts
At a high level, the flow looks like this:
Inbound SMS
-> Messaging webhook
-> TriageAgent actor
-> AI topic classification
-> durable route table lookup
-> SMS reply
-> triage history and topic counts
The important bit is ownership.
The actor owns the route table. The actor owns the triage history. The actor owns the topic counts. You do not need to reconstruct that context from a separate app server every time a message arrives.
Why an Agent SDK actor?
The sample uses one actor instance per inbound number.
That means the support number itself gets durable state:
- route table
- recent triage history
- total message count
- per-topic counts
This is a nice fit for SMS because the channel is asynchronous. Customers can message at any time, and the app needs to remember routing state across requests.
The actor state starts with a default route table:
billing -> billing-queue
support -> support-queue
sales -> sales-queue
general -> general-queue
Those routes can be changed at runtime with POST /routes.
That is the part I like: the bot is not just stateless glue code around a model. It has an operational memory for how messages should be routed.
The classification step
The TriageAgent classifies each inbound message with Telnyx AI Inference through the [telnyx] binding:
const completion = await this.env.TELNYX.ai.openai.chat.createCompletion({
model: this.env.AI_MODEL || "moonshotai/Kimi-K2.6",
messages: [
{ role: "system", content: CLASSIFY_SYSTEM_PROMPT },
{ role: "user", content: `Customer message: "${text}"` },
],
max_tokens: 2000,
temperature: 0.2,
});
The system prompt asks the model to return JSON only:
{
"topic": "billing",
"confidence": 0.95,
"reason": "The customer is asking about a charge."
}
The code validates the topic against the allowed set and falls back to general if parsing fails or the model returns something unexpected.
That fallback matters. Triage systems should fail into a safe queue, not drop the message.
The route table
Once the topic is known, the actor checks its route table:
const route = state.routeTable[topic] || state.routeTable["general"] || "general-queue";
Then it replies to the customer:
await this.env.TELNYX.messages.send({
from: state.fromNumber || state.phoneNumber,
to: from,
text: replyText,
});
The app uses the Telnyx binding for messaging and inference. In the function code, you call this.env.TELNYX.messages.send() and this.env.TELNYX.ai.openai.chat.createCompletion() without hardcoding an API key.
That keeps the example focused on application logic instead of credential plumbing.
The API surface
The app exposes a small set of routes:
POST /webhooks/smsreceives Telnyxmessage.receivedeventsPOST /debug/triagesimulates inbound SMS without using a real phone numberPOST /routesupdates the route tableGET /routesreturns the current route tableGET /historyreturns recent triage entries and topic countsGET /debug/stateinspects actor stateGET /health/livenessandGET /health/readinesssupport health checks
For local testing after deploy, the debug endpoint is the easiest path:
curl -X POST https://agent-sms-triage-bot-<id>.telnyxcompute.com/debug/triage \
-H "Content-Type: application/json" \
-d '{"from":"<customer-number>","to":"<triage-number>","text":"Why was I charged twice this month?"}'
You should get back a topic, route, and confidence value.
Then you can inspect history:
curl "https://agent-sms-triage-bot-<id>.telnyxcompute.com/history?number=<triage-number>"
Where this pattern goes next
The sample routes to queue names, but the same pattern could call a real downstream system:
- create a Zendesk ticket for support messages
- alert a billing team for invoice disputes
- send sales leads to a CRM
- escalate low-confidence classifications for human review
- route multilingual messages to language-specific queues
- expose topic counts for a lightweight operations dashboard
The architecture stays the same.
The webhook receives the message. The actor classifies it, looks up the routing rule, sends the reply, and stores the result.
Production notes
Before using this with real customers, I would add:
- webhook signature verification
- SMS opt-out and consent handling
- idempotency for duplicate webhook delivery
- PII redaction in stored message history
- role-based controls for changing routes
- a dead-letter or alerting path when SMS send fails
- human review for low-confidence classifications
But as a starter architecture, this is a useful building block for AI-assisted support triage.
It shows how to combine messaging, AI inference, and durable state without spreading a small workflow across a pile of separate services.