AI Assistant Needs a Back End. Put It at the Edge
Building a voice AI assistant has never been easier...
Building a voice AI assistant has never been easier. You write a prompt, connect a phone number, pick a model, and within minutes your assistant is answering calls. The first successful conversation feels like magic. Then someone asks:
"Can you tell me where my order is?"
Or:
"Can you schedule someone to come tomorrow?"
At that point, your assistant needs information it doesn't have. It needs to talk to your CRM, your scheduling system, or your own APIs. The LLM isn't the application anymore, it's one component in a much larger system. That's the point where every production AI assistant grows a backend. In this article, we'll build that backend using a single Telnyx Edge Compute function. Instead of deploying another webhook service, we'll use one Go function to handle both dynamic variables and webhook tool calls, verify incoming requests, and connect the assistant to business logic running behind the scenes.
The Architecture
Let's look at what the sample application actually does. The example assistant is named Jordan and works for a fictional home services company. From the caller's perspective, the interaction feels simple. The assistant answers the phone. It greets the caller. It collects information. It schedules an on-site estimate. Behind the scenes, though, two very different backend interactions happen.
Before Jordan says the first word, the assistant requests runtime information that shouldn't live inside the prompt. Later, after gathering enough information from the caller, Jordan needs to schedule the appointment. Both requests are handled by exactly the same Edge Compute function. Conceptually, the architecture looks like this:
Instead of deploying separate webhook services for different responsibilities, one function owns all assistant callbacks. That may seem like a small implementation detail, but it has a few advantages. There's only one deployment, one endpoint, one place to manage secrets, one place to verify requests, and one place to connect your business logic to the assistant. As your application grows, that simplicity becomes surprisingly valuable.
If you would like to follow along, the complete source code for the backend is available in the edge-ai-assistant-backend-go example repository, and the deployment guide walks through configuring the AI Assistant and Edge Compute function step by step.
GitHub repository: https://github.com/team-telnyx/telnyx-code-examples/tree/main/edge-ai-assistant-backend-go
Deployment guide: https://developers.telnyx.com/docs/edge-compute/guides/ai-assistant-backend
Understanding Dynamic Variables
One of the first callbacks happens before the assistant even starts talking. This is where dynamic variables come in. Think about what happens when someone calls a business. The greeting usually isn't static. Maybe you want to say: "Thanks for calling Pinecrest Home Services." Maybe premium customers hear a different greeting or maybe the transfer number changes depending on the office that's currently open. Maybe the estimated wait time depends on today's schedule.
None of that belongs inside a prompt. Prompts describe behavior. Runtime data belongs somewhere else. Dynamic variables solve that problem. Instead of hardcoding values into the assistant configuration, Telnyx asks your backend for them every time a call begins. The function responds with JSON like this:
{ "dynamic_variables": { "company_name": "Pinecrest Home Services", "timeframe": "two business days", "placeholder_transfer_destination": "+15551234567" } }
One detail that's easy to overlook is the wrapper object. The response must be returned under the dynamic_variables key. Returning a flat JSON object won't populate the assistant variables because the assistant expects a very specific response format. Once those values arrive, they're immediately available inside your prompt. From that point on, the assistant behaves as though those values had always been there. The difference is that they're resolved at runtime instead of being baked into your configuration.
When the Assistant Needs Your Application
Dynamic variables solve the first half of the problem. The second half happens during the conversation itself. Imagine the caller says: "I'd like someone to come out this Friday." At this point the assistant needs to create something that doesn't exist yet, an appointment. This isn't something an LLM should invent. It needs to ask your application.
That's where webhook tools come in. The assistant recognizes that it has enough information to perform an action and invokes a tool named schedule_estimate. The request is sent to the exact same Edge Compute function that handled the dynamic variables callback earlier. Your function performs whatever business logic you need like calling an internal scheduling API, checking technician availability, or creating a CRM record. The sample keeps things intentionally simple and returns:
{ "scheduled_date": "2025-04-10", "scheduled_time": "10:00", "confirmation_number": "CONF-1715234567", "estimate_id": "EST-1715234567" }
The important part isn't the fake confirmation number. It's the pattern. The assistant doesn't generate business data. Your application does. The assistant simply knows when it needs information. Your backend decides what that information should be. That's a much healthier separation than asking an LLM to hallucinate identifiers, appointment slots, or customer records.
One Function, Two Very Different Requests
One thing I particularly like about this sample is how little infrastructure it requires. Many webhook-based systems end up looking something like this:
/dynamic-variables
/schedule
/orders
/customers
/transfers
/authentication
...
Before long you're deploying an API solely to support your assistant. This example takes a different approach:
Both callback types point to exactly the same URL.
The function examines the incoming request.
If it contains data.event_type, it knows it's handling dynamic variables.
If it receives a flat JSON payload, it treats the request as a webhook tool invocation.
The implementation is small enough to understand in a few minutes while still demonstrating a production-friendly architecture. As your assistant becomes more capable, you can continue routing different callback types through the same function or split them into separate services later if your application grows beyond that model.
Starting simple is usually the right decision.
Why Put the Backend at the Edge?
At this point you might be wondering: Why not just deploy a normal webhook server? That's a perfectly valid option. Many teams do exactly that. The question isn't whether a traditional backend works. It's whether it's the simplest architecture for this particular workload. Voice AI applications have a very different latency profile than most web applications.
Dynamic variables are resolved before the assistant even says hello. Tool invocations happen while someone is actively waiting on the phone. Unlike a background API request, every additional network hop directly affects the conversational experience. That doesn't mean every millisecond determines success or failure, but it does mean callback latency is part of the user experience.
Running your assistant's backend on Edge Compute keeps the callback logic close to the communications infrastructure that's already handling the call. It also removes an entire category of operational work. Instead of provisioning another web service, exposing it publicly, managing deployments, and maintaining separate infrastructure, you deploy a single Edge Compute function and point your assistant at its invoke URL.
For many voice applications, that's enough. Your assistant still talks to your own databases, CRMs, scheduling systems, and internal APIs. The difference is that you don't need another standalone backend just to receive assistant callbacks. And perhaps more importantly, the architecture stays easy to reason about. There's a clear mental model:
The AI Assistant owns the conversation.
Edge Compute owns the business logic.
That's a separation that continues to hold even as your application becomes significantly more sophisticated.
Building a Production Backend
The scheduling example in this repository is intentionally simple, but the architecture scales far beyond scheduling appointments. Once you have a backend sitting behind your AI Assistant, it becomes the bridge between the conversation and the rest of your application.
Think about the kinds of actions your assistant might need to perform during a call. It might retrieve customer information from a CRM, check the status of an order, create a support ticket, schedule an appointment, trigger an internal workflow, or decide whether a call should be transferred to a human. From the assistant's perspective, these are all the same operation. It recognizes that it needs information it doesn't have, invokes a tool, waits for the response, and continues the conversation.
The backend is where all of those differences live. One tool might call Salesforce. Another might query PostgreSQL. Another might invoke an internal REST API, while a fourth simply looks up data in Redis. The assistant doesn't need to know how any of that works. Its job is to recognize when business logic is required. Your backend decides how to execute that logic and what data should come back.
Keeping conversational logic separate from application logic turns out to be a useful architectural boundary. Your prompts remain focused on how the assistant behaves, while your backend evolves independently as your business systems grow.
Security Shouldn't Be a Production TODO
One thing I appreciated about this sample is that it doesn't leave security as an exercise for the reader. If you've spent enough time browsing GitHub repositories, you've probably seen comments like:
// TODO: Verify webhook signature
That works for a demo, but not for an application that's participating in a live phone call. Every webhook request sent by Telnyx is signed using Ed25519. Before the function processes a request, it reads the telnyx-signature-ed25519 and telnyx-timestamp headers, reconstructs the signed payload by combining the timestamp with the raw request body, validates that the timestamp falls within an acceptable window, and finally verifies the signature using your Telnyx public key.
Only after those checks succeed does the function continue with your business logic. That matters because this endpoint isn't simply returning JSON. It's creating appointments, resolving customer-specific information, and potentially interacting with internal systems. The function should have confidence that every request genuinely originated from Telnyx before it performs any of those operations.
Managing Secrets
Signature verification requires your Telnyx public key, and the example stores it as an Edge Compute secret rather than hardcoding it into the application.
Fetching the key is straightforward:
PUBLIC_KEY=$(curl -s \ -H "Authorization: Bearer $TELNYX_API_KEY" \ https://api.telnyx.com/v2/public_key \ | jq -r '.data.public')
Once retrieved, it's stored securely using the CLI:
telnyx-edge secrets add TELNYX_PUBLIC_KEY "$PUBLIC_KEY"
The same mechanism works for anything else your backend depends on, including API keys, OAuth credentials, database passwords, or authentication tokens for downstream services. Keeping configuration outside the application code makes deployments easier to manage and avoids the temptation to bake secrets into source control.
Why Everything Goes Through One Function
Perhaps the most interesting design decision in this example is that everything flows through a single Edge Compute function.
At first glance, this might seem unusual. Most web applications expose multiple routes for different resources. You might expect separate endpoints for customers, appointm
[truncated for AI cost control]