Telnyx AI: Agent SDK (Beta) — Full Documentation
Complete page content for Agent SDK (Beta) (AI section) of the Telnyx developer docs (https://developers.telnyx.com). This file: https://developers.telnyx.com/development/llms/ai-agent-sdk-beta-llms-full-txt.md · Root index: https://developers.telnyx.com/llms.txt
Get Started
Overview
Source: https://developers.telnyx.com/docs/agent-sdk.mdThe Agent SDK is a TypeScript base class,
Agent, that extends
StatefulActor with the primitives AI agents need
most: a persistent conversation history, durable scheduled tasks, and merge-patch
state — with no extra infrastructure to manage.
Agent ships as an export of @telnyx/edge-runtime, alongside StatefulActor. There
is no separate package to install.
Agent base class is in Beta. The API surface may change.
What Agent adds to StatefulActor
State is durable and survives restarts — the same persistence guarantee that
StatefulActors provide. Everything
StatefulActor provides is inherited too,
including the embedded SQL database and
WebSocket termination.
Bring your own harness
The loop that makes an agent more than a single model call — build the prompt from history, call the model, run the tools it asks for, decide whether to continue or stop — is called a harness. The Agent SDK deliberately doesn’t ship one. It is the substrate a harness runs on: history, state, and timers stay durable underneath whatever loop you run, andthis.messages converts history to the format your stack
expects.
Two ways to wire one in:
- Roll your own.
process()is the harness: it calls inference through the pre-authenticated Telnyx API binding — no API key to manage — andtoOpenAI()produces exactly the payload it takes. → Roll Your Own Agent - Bring a framework as the harness. LangGraph, LangChain — anything that runs on Node — executes inside the actor, with the SDK as its durable memory and scheduler. → LangGraph Agent
baseURL
away. See Calling LLMs. And because the
substrate is harness-neutral, an opinionated first-party harness can slot in later
without changing anything you build now.
Where to go next
- Quickstart — a working agent in one file
- How Agents Run — the actor execution model in four properties
- Calling LLMs — both wiring patterns, side by side
- API reference — every method and override hook
Quickstart
Source: https://developers.telnyx.com/docs/agent-sdk/quickstart.mdThe simplest agent receives a message, queues a background task to process it, then schedules a follow-up.
Env is a global type generated by telnyx-edge types from your telnyx.toml — it
includes SUPPORT: ActorNamespace<SupportAgent> and any other bindings you declare.
Run telnyx-edge types before tsc or ship to keep it in sync.
telnyx.toml:
Deploy
Generate binding types, then ship:https://<name>-<id>.telnyxcompute.com — see
edge compute configuration for the full reference.
Next steps
- Calling LLMs — wire in a real model, with or without a framework
- Message History — the
this.messagesAPI and framework adapters - Scheduled Tasks —
queue,schedule,every, and retries - Durable State —
setState,getState, and typed state - Roll Your Own Agent / LangGraph Agent — two complete examples
Concepts
How Agents Run
Source: https://developers.telnyx.com/docs/agent-sdk/concepts/how-agents-run.mdAn
Agent is a StatefulActor — understanding four
properties of that execution model explains why the SDK looks the way it does.
One actor per conversation
You address an agent by name:env.CONVOS.idFromName("+15550001111") always routes to
the same actor instance, anywhere in the fleet. That makes the phone number (or user id,
or session id) the unit of isolation — each conversation gets its own history, state,
and timers, with no cross-talk and no shared database to partition. See
Addressing.
Turns are serialized
An actor runs one method call at a time. Whileprocess() is thinking, a second
inbound message waits — it never interleaves. That is what makes the read-modify-write
patterns in agent code safe without locks: this.messages.add() then
this.queue("process") can’t race another webhook for the same customer. See
Execution model.
Think in the background
Inbound RPC methods run under a 30-second wall-clock budget — fine forreceive(), tight for an LLM round-trip plus tool calls. That’s why the quickstart acks
the webhook and defers the thinking:
alarm() handler, which has a
larger budget — on the order of minutes — and retries on
failure. Put LLM calls, tool use, and outbound API
work there.
Everything durable survives restarts
History, state, and pending tasks live in the actor’s durable storage, not in memory. If the actor is evicted, crashes, or the pod restarts, a pending task’s timer re-fires after recovery, and the scheduler re-arms to the earliest pending task whenever the actor wakes. A 24-hour follow-up scheduled today fires tomorrow no matter what happens in between. See Lifecycle.Build
Message History
Source: https://developers.telnyx.com/docs/agent-sdk/message-history.md
this.messages is a persistent, ordered log of AgentMessage objects stored in the
actor’s KV. It is per-actor — each idFromName("user-123") gets its own independent
history.
Framework adapters
this.messages can convert history to the format expected by popular LLM SDKs:
toAnthropic() omits system messages: the Anthropic Messages API takes the system
prompt as a top-level system parameter, not as a conversation message. Pass it on the
request yourself.
The toOpenAI() payload feeds Telnyx Inference directly —
env.TELNYX.ai.openai.chat.createCompletion({ model, messages }) on the
pre-authenticated Telnyx API binding, or any
OpenAI-compatible endpoint over HTTP. See
Calling LLMs for both wiring patterns.
Message shape
Scheduled Tasks
Source: https://developers.telnyx.com/docs/agent-sdk/scheduled-tasks.md
this.queue(), this.schedule(), and this.every() let you defer work durably. Tasks
survive pod restarts — the scheduler uses the actor’s built-in alarm mechanism.
A task’s name must match a method on your class.
Task dispatch
When a task fires, the platform calls the method by name on your actor instance:Retries
Tasks retry up to 5 times by default on failure. Configure withmaxRetries:
Durable State
Source: https://developers.telnyx.com/docs/agent-sdk/state.md
this.setState() merges a patch into the actor’s current state. Use it for small
structured values you want to read quickly — the current conversation state, user
preferences, last-seen timestamp.
setState is a recursive merge (RFC 7396) — top-level keys you pass are merged in,
nested objects are deep-merged, and null deletes a key. replaceState replaces the
whole state object.
State is one value, read whole. The moment data turns relational or queryable —
orders, events, anything you’d filter or aggregate — put it in the actor’s embedded
SQL database instead.
SQL Storage
Source: https://developers.telnyx.com/docs/agent-sdk/sql.md
Agent extends StatefulActor, and every actor
carries a private, durable SQLite database at this.ctx.storage.sql. There is
nothing to declare in telnyx.toml — the database is created on first use, and it works
inside an agent exactly as it does on a plain actor.
Which tier holds the data?
An agent has three durable tiers, all in the same actor:
State is read and written as one value; history is read back in order. The moment you
want “the last five orders over $10” — a lookup neither tier answers without a scan —
put rows in SQL.
Using it from an agent
exec() is synchronous — the database is a local file in the actor’s own process —
with positional ? binds and a cursor you drain with toArray():
this.ctx.storage.transactionSync(() => { ... }) to
commit them atomically.
Patterns that come up in agents:
- Webhook dedup.
INSERTthe event id into a table with aUNIQUEconstraint beforequeue()-ing work; a thrown constraint violation means you already handled that event. - Tool-call ledger. Record each tool invocation and result as a row; answer “what did you do?” or audit questions with a query instead of replaying history.
- Searchable history.
this.messagesis an ordered log, not an index. If the agent needs keyword lookup over past conversation, append each message to an SQL table too — in the same method that callsmessages.add()— and query it withLIKEplus an index.
Semantics and limits
The full contract — cursor rules, multi-statement batches,transactionSync, binding
types, and the limits (1 GB per actor database, 2 MiB per bound value, integer range) —
is on the actor SQL guide. It
applies verbatim inside an Agent: the SDK reserves
alarm() for its scheduler, but all of
ctx.storage stays yours.
For data that more than one function or caller must query — shared across agents, or
read from the CLI and REST API — use a standalone
SQL Database instead; the embedded database is strictly
per-instance.
WebSockets
Source: https://developers.telnyx.com/docs/agent-sdk/websockets.mdAn
Agent can terminate WebSockets today. The capability comes from
StatefulActor: define one method,
webSocket(), and the agent owns the live socket — frames dispatch one at a time,
serialized with RPCs and tasks like every other call, with history, state, and timers
right there in the handler.
Actor WebSocket support is in beta — the platform guide carries the full contract
and current caveats.
The shape
A function terminates the handshake — authenticate once, pick the agent, hand off — and the agent handles every frame after that:queue() the LLM turn,
return. A message handler runs under the 30-second method budget, and while it runs
every other frame, RPC, and task on this agent waits — so respond() does the slow
thinking in a task, then pushes the reply to every open socket with
this.ctx.broadcast().
Pushing without an inbound frame
Tasks run inside the agent, so a timer can push. Use the scheduler — not raw alarms, which the SDK reserves for the task queue:What to know before shipping
- Connections are capped at about five minutes today, measured from the handshake,
even on a socket actively exchanging frames — surfacing as an abnormal close
(code
1006). The cap will be raised in a future update. Reconnecting is the client’s job: back off and reopen the same name.idFromNameroutes the new socket to the same agent, where history, state, and pending tasks all survived — only the socket is new. - Sends aren’t held for durability.
ws.send()andbroadcast()are immediate; a handler that sends and then throws leaves the client holding a frame about state that never committed. Frames that carry the message log’sseqlet a client detect gaps and re-derive on reconnect. - Bound per-frame work. Frames that arrive while a handler runs queue up to 256
events or 1 MiB; overflow closes the socket with
1013. Batch chatty clients into fewer, larger frames.
What’s in development
This page is the platform surface, and it stays. In development on top of it:AgentClient,
a browser SDK — browser clients connecting straight to an agent, automatic reconnect, and
state sync driven by onStateChanged — with
onConnect() on the Agent class as the seam it lands on. Until then, webSocket() is
the supported path.
Examples
Roll Your Own Agent
Source: https://developers.telnyx.com/docs/agent-sdk/examples/roll-your-own.mdNo framework — your
process() method is the agent loop. The Agent SDK supplies the
durable parts (history, retries, the follow-up timer); the LLM is one call on the
pre-authenticated Telnyx API binding, which speaks the
this.messages.toOpenAI() payload directly — no API key to manage. Prefer another
provider? Any OpenAI-compatible endpoint works over fetch, with its key in a
secret.
Each customer gets their own Conversation actor, keyed by phone number.
src/conversation.ts — the actor:
src/index.ts — the function that routes inbound webhooks to the right actor:
telnyx-signature-ed25519 header.
telnyx.toml:
[telnyx] binding carries auth, so there is no API
key to provision.
Prefer a framework running the loop instead? See the
LangGraph version of this same agent.
Task delivery is at-least-once: a crash
after messages.add() or messages.send() succeeds retries the whole process()
method. For production, guard outbound side effects — e.g. check state before sending,
or use a stable message ID to deduplicate.
LangGraph Agent
Source: https://developers.telnyx.com/docs/agent-sdk/examples/langgraph.mdHere an agent framework does the thinking. LangGraph’s ReAct agent runs inside the actor’s
process() method — with tool calling, multi-step reasoning, the works — while
the Agent SDK supplies what the framework doesn’t have: durable per-customer history,
retries, and the follow-up timer.
this.messages.toLangChain() returns plain { role, content } messages, which LangGraph
accepts as-is. The LLM is ChatOpenAI pointed at Telnyx Inference — swap baseURL,
key, and model to bring any OpenAI-compatible provider. The same pattern fits any agent
framework that runs on Node.
Install the framework alongside the runtime:
src/conversation.ts — the actor:
this.messages — intermediate tool calls and
tool results stay inside the LangGraph run. If you want them in the durable history too,
append() them from out.messages.
src/index.ts — the function that routes inbound webhooks to the right actor:
telnyx-signature-ed25519 header.
telnyx.toml:
process.env.TELNYX_API_KEY:
messages.add() or messages.send() succeeds retries the whole process()
method. For production, guard outbound side effects — e.g. check state before sending,
or use a stable message ID to deduplicate.
Reference
Overview
Source: https://developers.telnyx.com/docs/agent-sdk/api-reference.mdEverything the Agent SDK adds lives on the
Agent base class, imported from
@telnyx/edge-runtime:
Two rules apply across the whole surface:
- Every member is
protected— this is an inside-the-class API, called from your own methods. It is not part of the RPC surface your subclass exposes to stubs. alarm()is claimed by the SDK. The task scheduler runs on the actor’s single alarm slot. Do not overridealarm()in anAgentsubclass — schedule a task instead. (On a plainStatefulActor,alarm()remains yours — see Alarms.)
StatefulActor is still there — this.ctx, this.env, RPC dispatch,
fetch() — see the
StatefulActor Runtime API.
Agent Class
Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/agent.md
Agent<E, State> extends StatefulActor<E>. Subclass it, declare async methods for
your inbound surface, and use the protected members inside them.
Type parameters
Construction
You don’t construct agents yourself — the runtime does. On every activation the base constructor also re-arms the task scheduler to the earliest pending task (insidectx.blockConcurrencyWhile), which is what makes timers survive crashes and restarts.
If you need your own one-shot init, override the constructor and call
super(ctx, env) first, exactly as with a
StatefulActor.
Properties
Plus everything inherited:
this.ctx, this.env.
Methods
State and scheduling methods are listed with full semantics on their own pages:Override hooks
Reserved: alarm()
The SDK claims the actor’s alarm slot to drive the task scheduler: when the alarm
fires, Agent.alarm() drains every due task, dispatches each to the method it names,
and re-arms to the next deadline. Overriding alarm() in an Agent subclass breaks
queue/schedule/every. If you need timed work, schedule a task.
Message Log
Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/message-log.md
this.messages is the actor’s durable conversation log. Messages are ordered by an
assigned monotonic seq (insertion order, not wall-clock), and the log is append-only —
there is no delete.
Types
Writing
Reading
all() reads the entire history every time — on a long-lived conversation prefer
last(n) for a bounded context window.
Adapters
Each adapter reads the full history and converts it. They differ in how they treatsystem and tool messages:
append() with plain roles, or rebuild framework state
from toOpenAI() output instead.
Scheduling
Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/scheduling.mdTasks are durable named timers. Each task names a method on your class; when the timer fires, the SDK calls that method with the task’s payload. All of it rides the actor’s single alarm slot, re-armed to the earliest pending deadline.
Creating tasks
All three return
Promise<string> — the task id.
ScheduleOptions
id, scheduling is an upsert — the prior task with that id is replaced
and the timer re-armed. Without one, every call creates a new task under a random id.
Dispatch
- A due task calls
this[task.name](task.payload)— one argument, the payload. - If no such method exists, the fallback fires instead:
onTask(name, payload, { attempt }). - Task methods are ordinary methods. A method that should be schedulable but not
RPC-callable from a stub can be named with a leading
_— the runtime excludes_-names from RPC, but the scheduler still dispatches to them.
Failure and retries
- A task that throws is retried with exponential backoff (starting around a second,
capped at 5 minutes), up to
maxRetriestimes after the first delivery — 6 runs total by default. - A task that exhausts its retries is parked: deleted without firing again, and there is no callback when that happens.
- A recurring (
every) task resets its attempt count after each successful run, and schedules its next fire from the start of the drain turn (the timestamp captured before dispatch), not from when the method returns.
Delivery contract
Delivery is at-least-once: a crash after your method runs but before the task is marked done re-runs it on the next activation. Write task handlers to be idempotent — the same rule as alarm handlers. Payloads are stored in the actor’s durable storage and must be codec-safe — JSON-native values plusDate, Map, Set, ArrayBuffer/TypedArray, BigInt, RegExp.
Functions, class instances, or circular references throw a CodecError — see
Errors.
Inspecting and cancelling
State
Source: https://developers.telnyx.com/docs/agent-sdk/api-reference/state.mdAgent state is one durable value of your
State type. setState merges, which is
what you want for the common case — update two fields without re-writing the rest.
Methods
- The merge is recursive (RFC 7396) — top-level keys are merged in, nested objects are
deep-merged, and
nulldeletes a key. - Values must be codec-safe, like all actor storage — see
Errors for
CodecError.
Hooks
initialState(): State
The default for a fresh actor. Called whenever state is read and nothing has been
stored yet; the default implementation returns {}. Reading alone doesn’t persist it —
the first write does.
onStateChanged(next, prev)
Fires after every setState() resolves, with the merged state and the state before the
patch. Override it to react to changes — mirror to an external system, log transitions,
invalidate caches.
replaceState() does fire this hook — the next argument is the new state and prev
is the state before replacement.
Limits
Source: https://developers.telnyx.com/docs/agent-sdk/limits.md
Execution budgets
Do LLM and outbound API work in tasks, not inbound methods — see
How Agents Run. Full platform numbers live in
the StatefulActor API reference.
Task retries
A task that throws retries up to itsmaxRetries (default 5) with exponential
backoff, then is parked — it stops retrying and no longer fires. every() tasks reset
their attempt count after each successful run.
Storage
Message history and state live in the actor’s durable storage and are subject to its key/value size caps — see the storage reference. Message history is append-only — there is no delete or trim API.last(n) reads a
bounded window, but writes accumulate forever. For relational or queryable data, use
the actor’s embedded SQL database instead — up to 1 GB per agent.
What’s shipped vs. in development
Shipped today:Agentbase class with message history, scheduled tasks, and merge-patch state- WebSocket termination via
webSocket()— see WebSockets BlobStorefor blob access within actors
AgentClientbrowser SDK — browser clients connecting straight to an agent, with automatic reconnect and state sync on top ofonStateChanged.onConnect()is the seam it lands on.
Agent API surface is Beta and may change as pieces land.