> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Roll Your Own Agent

> An SMS support agent with a hand-written loop: toOpenAI() history in, one call on the Telnyx binding, reply out.

No 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](/docs/edge-compute/telnyx-api), 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](/docs/edge-compute/configuration/secrets).

Each customer gets their own `Conversation` actor, keyed by phone number.

**`src/conversation.ts`** — the actor:

```ts theme={null}
import { Agent } from "@telnyx/edge-runtime";

const SYSTEM_PROMPT =
  "You are a terse, helpful SMS support agent. Answer in one or two sentences.";

// Minimal hand-typed slice of the binding — `telnyx-edge types` generates the full types
interface ConvEnv {
  TELNYX: {
    messages: {
      send(m: { from: string; to: string; text: string }): Promise<unknown>;
    };
    ai: {
      openai: {
        chat: {
          createCompletion(req: {
            model: string;
            messages: Array<{ role: string; content: string }>;
          }): Promise<{ choices: Array<{ message: { content: string } }> }>;
        };
      };
    };
  };
}

interface ConvState extends Record<string, unknown> {
  from: string;
  to: string;
  at: number;
}

export class Conversation extends Agent<ConvEnv, ConvState> {
  protected override initialState(): ConvState {
    return { from: "", to: "", at: 0 };
  }

  async receive({ text, from, to }: { text: string; from: string; to: string }): Promise<void> {
    await this.setState({ from, to });
    await this.messages.add("user", text);
    await this.queue("process"); // ack the webhook now, think in the background
  }

  async process(): Promise<void> {
    // toOpenAI() emits Chat Completions format — the binding takes it directly
    const history = await this.messages.toOpenAI();

    // Pre-authenticated Telnyx client: no base URL, no key. An API error
    // rejects, which marks the task failed — the scheduler retries with backoff.
    const res = await this.env.TELNYX.ai.openai.chat.createCompletion({
      model: "zai-org/GLM-5.2",
      messages: [{ role: "system", content: SYSTEM_PROMPT }, ...history],
    });
    const reply = res.choices[0].message.content;

    await this.messages.add("assistant", reply);
    const { from, to } = await this.getState();
    await this.env.TELNYX.messages.send({ from: to, to: from, text: reply });
    await this.setState({ at: Date.now() });

    // Follow up in 24 hours if the customer goes quiet
    await this.schedule(86_400, "nudge", null, { id: "nudge" });
  }

  async nudge(): Promise<void> {
    const last = await this.messages.last();
    if (last?.role !== "assistant") return; // customer replied — skip
    const { from, to } = await this.getState();
    await this.env.TELNYX.messages.send({
      from: to,
      to: from,
      text: "Just checking in — did that sort things out?",
    });
  }
}
```

**`src/index.ts`** — the function that routes inbound webhooks to the right actor:

```ts theme={null}
import type { ActorNamespace } from "@telnyx/edge-runtime";
import { Conversation } from "./conversation.js";

export { Conversation };

interface Env {
  CONVOS: ActorNamespace<Conversation>;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (new URL(req.url).pathname.startsWith("/health")) return new Response("ok");
    const hook = await req.json() as {
      data: { event_type: string; payload: { from: { phone_number: string }; to: Array<{ phone_number: string }>; text: string } };
    };
    if (hook.data.event_type !== "message.received") return new Response("ignored");
    const { from, to, text } = {
      from: hook.data.payload.from.phone_number,
      to: hook.data.payload.to[0]?.phone_number ?? "",
      text: hook.data.payload.text,
    };
    await env.CONVOS.idFromName(from).receive({ text, from, to });
    return new Response("ok");
  },
};
```

<Note>
  Verify Telnyx webhook signatures before processing — see
  [receiving webhooks](/development/api-fundamentals/webhooks/receiving-webhooks).
  The examples above omit verification for brevity; production code must check the
  `telnyx-signature-ed25519` header.
</Note>

**`telnyx.toml`:**

```toml theme={null}
name = "support-agent"
main = "src/index.ts"
compatibility_date = "2026-05-01"

[[actors]]
binding = "CONVOS"
type    = "Conversation"

[telnyx]
binding = "TELNYX"
```

That's the whole deployment — the `[telnyx]` binding carries auth, so there is no API
key to provision.

Prefer a framework running the loop instead? See the
[LangGraph version](/docs/agent-sdk/examples/langgraph) of this same agent.

<Note>
  Task delivery is [at-least-once](/docs/agent-sdk/api-reference/scheduling): 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.
</Note>
