Voice AI

Run LangGraph Inside a Telnyx Edge Actor with Zero-Credential Inference

LangGraph is a powerful framework for building stateful, multi-step LLM agents. But running it on edge infrastructure usually means managing API keys inside your function, dealing with cold-start latency, and wiring up your own durable state.

This Telnyx code example solves all three. LangGraph runs inside a Telnyx Edge Compute actor, the Agent SDK provides durable message history and state, and LLM inference goes through the pre-authenticated Telnyx API binding — no API key in your code, bundle, or logs.

The code example is here:

https://github.com/team-telnyx/telnyx-code-examples/tree/main/langgraph-agent-on-edge

What This Example Builds

The sample is a Node.js and TypeScript app running on Telnyx Edge Compute. It uses the Agent SDK's Agent base class as the durable substrate and LangGraph's StateGraph as the reasoning harness.

The graph has three nodes:

intent   → LLM classifies the user message as "order" or "smalltalk"
action   → plain TypeScript looks up the order status (no LLM needed)
response → LLM composes a reply using the history and action result

The user experience is simple:

User: where is my order ORD-10042?
Agent: Your order ORD-10042 has shipped via Telnyx Logistics and is expected to arrive on Friday.

User: hi there
Agent: Hi! How can I help you with your Telnyx Logistics order today?

The graph routes conditionally: if the intent is order, it runs the action node before the response node. If the intent is smalltalk, it skips the action node and goes straight to the response.

The Zero-Credential Adapter

This is the key innovation in the sample.

LangGraph nodes that call the LLM do so through a BaseChatModel. The stock ChatOpenAI from LangChain takes an apiKey and a baseURL. That works, but it means you are managing an API key inside your edge function. The Telnyx API binding is designed to handle that for you — so this sample uses a small adapter that lets LangGraph call the binding directly.

This sample ships a minimal adapter called TelnyxBoundChatModel:

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;
  }
}

It extends SimpleChatModel from @langchain/core, maps LangChain messages to the {role, content} format the binding expects, and calls this.env.TELNYX.ai.openai.chat.createCompletion(). No API key. No baseURL. No secret to rotate or leak.

The binding is declared in telnyx.toml:

[telnyx]
binding = "TELNYX"

That is it. The Edge platform injects credentials at runtime. Your code, your bundle, and your logs never see them.

The Turn State Machine

Edge actors deliver messages at-least-once. A crash after a successful send can retry the entire process() method. Without protection, that means duplicate SMS replies.

This sample ships a per-turn state machine:

FieldPurpose
turnMonotonic counter, incremented on each inbound
queuedTurnThe turn process() should handle next
processingTurnThe turn currently being processed
lastSentTurnHighest turn for which SMS send resolved
pendingOutboundStaging record before send

The flow:

receive() → bump turn, set queuedTurn, queue("process")
process() → if queuedTurn <= lastSentTurn: return (stale no-op)
             → run graph for queuedTurn
             → stage pendingOutbound
             → send SMS
             → commit lastSentTurn, clear pendingOutbound
             → if newer turn arrived: re-queue

If two inbound 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. No duplicate reply.

The guard is on turn, not reply text. So identical legitimate replies across different turns are never suppressed.

Three State Layers

One of the most common mistakes when building agents with LangGraph and the Agent SDK is conflating state. This sample teaches the distinction deliberately:

  1. LangGraph graph stateintentLabel, actionResult, replyText. Ephemeral. Lives and dies inside one process() run.
  1. Agent SDK durable stateturn, queuedTurn, lastSentTurn, pendingOutbound. Survives restarts. Per actor. Merge-patch via setState().
  1. Agent SDK message historythis.messages. The actual conversation log. Per actor. Durable.

The graph state is for passing data between nodes. The durable state is for turn tracking and idempotency. The message history is the memory. They are three separate things, and the sample code and README make that explicit.

Architecture

SMS webhook (message.received)
  └─> src/index.ts fetch()
        • verify Ed25519 signature via telnyx SDK
        • route to Conversation actor by phone number
        • return 200 immediately (30s budget)

  Conversation.receive()              ← inbound, ~ms
        • add user message to history
        • bump turn counter
        • queue("process") → ack webhook

  Conversation.process()              ← queued task, minutes of budget
        • stale-task no-op guard (turn ≤ lastSentTurn → return)
        • this.messages.toLangChain() → history
        • LangGraph StateGraph: intent → action → response
        • stage pendingOutbound → send SMS → commit lastSentTurn
        • re-queue if newer turn arrived during processing
        • schedule 24h nudge

  TelnyxBoundChatModel                ← the zero-credential adapter
        • extends SimpleChatModel (LangChain)
        • _call() maps messages → {role, content}[]
        • calls this.env.TELNYX.ai.openai.chat.createCompletion()
        • no API key in code, bundle, or logs

The inbound method runs under a 30-second wall-clock budget. It does zero model I/O. All LLM calls happen in the queued process() task, which has a budget on the order of minutes and retries on failure.

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

Point your messaging profile webhook at the function URL. Send an SMS with "where is my order ORD-10042?" and get a reply.

Visit the function URL in your browser for a demo UI that shows the conversation, the turn state machine counters, and the process log.

Why This Matters

This sample is a good example of LangGraph on Edge using Telnyx's latest Edge Compute platform. LangGraph provides the reasoning loop, the Agent SDK provides the durable substrate, and the Telnyx API binding handles inference — each layer doing what it is best at, with zero credentials to manage in your code.

That is the hello world of LangGraph-on-Edge.

Ready to build with low-latency voice AI?

Join developers building the future of real-time conversations