I wanted to see if LangGraph could run inside a real edge compute actor — not a notebook, not a local REPL, but actual edge infrastructure with durable state, retry semantics, and a 30-second inbound budget.
And I wanted to do it without managing an API key inside the function.
The Telnyx code example is here:
https://github.com/team-telnyx/telnyx-code-examples/tree/main/langgraph-agent-on-edge
The result is an SMS support agent that runs a 3-node LangGraph graph (intent → action → response) inside a Telnyx Edge Compute actor, with LLM inference through a pre-authenticated binding. No API key in code. No API key in the bundle. No API key in the logs.
The Problem with Frameworks on Edge
Most agent framework examples assume you have a long-running process with environment variables and a stable network. You import the framework, pass it your API key, and it makes HTTP calls to your LLM provider.
Edge functions flip that. You have a short-lived runtime, a 30-second inbound budget, and — on Telnyx — a pre-authenticated API binding that eliminates the need for keys entirely.
The challenge is that frameworks like LangGraph call the LLM through their own HTTP client. The stock ChatOpenAI from LangChain takes an apiKey and a baseURL. If you use it inside a Telnyx Edge function, you are managing a key that the binding was designed to make unnecessary.
The Adapter
I wrote a small adapter called TelnyxBoundChatModel. It extends LangChain's SimpleChatModel and calls the Telnyx binding instead of making its own HTTP calls:
class TelnyxBoundChatModel extends SimpleChatModel {
async _call(messages: BaseMessage[]): Promise<string> {
const mapped = messages.map(m => ({
role: roleForMessage(m),
content: contentToString(m.content),
}));
const res = await this.env.TELNYX.ai.openai.chat.createCompletion({
model: this.model,
messages: mapped,
});
return res.choices[0].message.content;
}
}
That is about 70 lines of code. It maps LangChain messages to the format the binding expects, calls createCompletion, and returns the content. No key. No baseURL. No secret to rotate.
LangGraph does not know or care that the model is the binding. It just sees a SimpleChatModel that returns strings. The graph runs the same way it would with ChatOpenAI.
The Graph
Three nodes:
intent → LLM classifies the message as "order" or "smalltalk"
action → plain TypeScript looks up the order
response → LLM composes a reply
If the intent is order, the graph runs the action node before the response node. If the intent is smalltalk, it skips action and goes straight to response.
The whole thing is about 80 lines of graph code. It is not a ReAct agent with tool calling. It is an explicit, typed graph — the kind of thing you would build if you wanted to control the flow rather than let the model decide.
The State Problem
Here is where it gets interesting.
LangGraph has its own state — the channels that flow between nodes. The Agent SDK has durable state — setState and getState that survive restarts. And the Agent SDK has message history — this.messages, which is the conversation log.
These are three different things. The sample teaches the distinction deliberately:
-
Graph state (
intentLabel,actionResult,replyText) is ephemeral. It lives and dies inside oneprocess()run. -
Durable state (
turn,queuedTurn,lastSentTurn) survives restarts. It is for turn tracking and idempotency. -
Message history (
this.messages) is the memory. It is the conversation.
If you conflate them, you end up with bugs. For example, if you put the graph's intentLabel into durable state, it persists across turns and the next message gets the wrong intent. If you put the conversation into graph state, it resets on every process() run and the agent has no memory.
The Turn State Machine
Edge actors deliver messages at-least-once. A crash after a successful SMS send can retry the entire process() method. Without protection, that means duplicate replies.
The sample uses a per-turn state machine:
receive() → bump turn, set queuedTurn, queue("process")
process() → if queuedTurn <= lastSentTurn: return (stale)
→ run graph
→ stage pendingOutbound
→ send SMS
→ commit lastSentTurn
If two messages arrive before the first process() runs, the second bumps queuedTurn. The first process() handles the latest turn. The stale second process() sees queuedTurn <= lastSentTurn and returns immediately. One reply, not two.
The guard is on turn, not reply text. So identical replies across different turns are never suppressed.
The 30-Second Budget
The inbound method runs under a 30-second wall-clock budget. That is fine for acking a webhook, but not for an LLM round-trip plus tool calls.
So the inbound method does zero model I/O. It adds the user message to history, bumps the turn counter, and queues a background task. The webhook acks immediately.
The queued process() task runs in the actor's alarm handler, which has a budget on the order of minutes. That is where the graph runs, the LLM is called, and the SMS is sent. If it throws, the scheduler retries with backoff.
What I Learned
LangGraph runs fine inside an edge actor. You just need to give it a chat model that calls the binding instead of an HTTP endpoint.
The binding is the whole point. Once you have the adapter, the rest of the code has no keys, no secrets, and no authentication logic. The platform handles it.
State layers matter. Graph state, durable state, and message history are three different things. The sample makes that explicit because it is the most common mistake.
At-least-once delivery is real. If you do not guard outbound side effects, you will send duplicate SMS replies under retry. The turn state machine is the answer.
The 30-second budget is real. If you call the LLM inside the inbound method, you will time out. Defer to a queued task.
Run It
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/langgraph-agent-on-edge
npm install
Fetch the public key and store it as a secret:
PUBLIC_KEY=$(curl -s -H "Authorization: Bearer $TELNYX_API_KEY" \
https://api.telnyx.com/v2/public_key | jq -r '.data.public')
telnyx-edge secrets add TELNYX_PUBLIC_KEY "$PUBLIC_KEY"
Deploy:
npm run typecheck
npm run types
npm run ship
Send an SMS with "where is my order ORD-10042?" and get a reply. Visit the function URL for a demo UI that shows the conversation, the turn state counters, and the process log.
The code example is here:
https://github.com/team-telnyx/telnyx-code-examples/tree/main/langgraph-agent-on-edge
Top comments (0)