> ## 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.

# create

```bash theme={null}
telnyx-acp create
telnyx-acp create --name support-agent \
  --model zai-org/GLM-5.3-Flash
```

No flags opens the wizard, which [Deploy the First Agent](/docs/agents/get-started) walks through. Any flag is the non-interactive form; `--name` is the one required flag, and leaving it out fails with one line and a hint. The flag form writes no `memory` block, where the wizard, for a configuration project, adds one by default. `create` writes `agent.json`, `instructions.md`, `README.md`, `AGENTS.md` and `.gitignore`, and [Project Layout](/docs/agents/project-layout) shows the files. Run inside a project, `create` refuses and points at `add` and at editing `agent.json`; `--force` nests a new project there anyway. It ends by printing the next commands, `dev`, `chat --local`, `deploy`, `status` and `logs`.

| Flag | Meaning |
| - | - |
| `--name <name>` | The project and agent name. |
| `--kind <kind>` | `configuration`, the default, or `code`. |
| `--model <model id>` | A Telnyx inference model id. `telnyx-acp models` lists them, and the default is `meta-llama/Llama-3.3-70B-Instruct`. An `openai/*`, `anthropic/*`, `google/*` or `groq/*` model needs the user's own key as a secret. |
| `--phone <number>` | A number in the account the agent answers on, E.164, repeatable. |
| `--email [address]` | An email inbox. Alone, an inbox named after the agent on the account's shared inbound subdomain. With an address, that inbox on a domain of the account's. |
| `--web` | The browser page. |
| `--channel <artifact>:<id>` | An existing artifact, repeatable. `call_control_application`, `texml_application`, `messaging_profile` or `email_domain`. |
| `--force` | Write into a non-empty directory, or nest a project inside another. |

## A Code-based Project

`--kind code` scaffolds a class on the Agent Runtime SDK in place of `instructions.md`. The other route to a code-based project is [export](/docs/agents/cli/export), once a configuration project needs code.

### Scaffold

```bash theme={null}
telnyx-acp create --name code-agent --kind code --model zai-org/GLM-5.3-Flash
```

```text theme={null}
code-agent/
├── agent.json        kind "code", with "entry": "src/agent.ts" in place of instructions
├── src/agent.ts      the class, the one file the user owns
├── package.json      depends on @telnyx/edge-runtime
├── .gitignore        excludes .telnyx/ and node_modules/
├── README.md
└── AGENTS.md
```

`agent.json` keeps everything a configuration project has, numbers, channels, tools, memory, limits and secret names, and `telnyx-acp add`, `remove`, `set` and `unset` keep editing it. Only the agent's behavior moves into the class.

### The Class

The scaffolded `src/agent.ts` is the SDK's `Agent` with the contract the CLI and the channels rely on, and one method to fill in:

```ts theme={null}
import { Agent, rpc, type Claim, type Env } from "@telnyx/edge-runtime";
import { AgentHttpServer } from "@telnyx/edge-runtime/agent-http";

// The one file you own. `telnyx-acp deploy` writes the manifest and the
// entry that mount this class; you never edit those.
//
// Each conversation is one durable instance of this class: its messages,
// state and scheduled work survive restarts and are kept until destroyed.
//
// `receive`, `receiveVia`, `run`, `fetch` and `authorize` are the
// contract `telnyx-acp invoke`, `chat`, the browser page and the phone and
// email handlers rely on; keep them. `process` is the turn: yours.

/** What a run leaves behind; `telnyx-acp invoke` reads `lastRunId` and `lastReply`. */
type State = {
  from?: string;
  lastRunId?: string;
  lastReply?: string;
  at?: number;
  /** Each run's status by id, what `run` answers: "accepted" | "completed". */
  runs?: Record<string, string>;
};

/** Where a reply goes when the message came in on a channel: the handler passes this to `receiveVia`. */
type ReplyVia =
  | { kind: "messaging"; profileId: string; from: string; to: string }
  | { kind: "email"; inboxId: string; messageId: string; from: string };

/** The platform's Telnyx client, bound as `TELNYX` by the manifest `deploy` writes. */
type Bindings = Env & {
  TELNYX?: { messages: { send(params: Record<string, unknown>): Promise<unknown> }; post(path: string, options: { body: Record<string, unknown> }): Promise<unknown> };
};

export class CodeAgent extends Agent<Bindings, State> {
  private door: AgentHttpServer<State> | undefined;

  /** The streams and RPC over HTTP, what `invoke`, `chat` and the page talk to. */
  override fetch(req: Request): Promise<Response> {
    return (this.door ??= new AgentHttpServer<State>(this.connectionEngine)).fetch(req);
  }

  /** Who may read the streams and call the @rpc methods: everyone, in the beta phase. */
  protected override authorize(): readonly Claim[] {
    return ["read", "rpc"];
  }

  /** One inbound message from `invoke`, `chat` or the page. Record it, answer off the request path, say which run will. */
  @rpc({ description: "Deliver one inbound message; the reply is produced off the request path" })
  async receive(text: string, from?: string, key?: string): Promise<{ runId: string; duplicate: boolean }> {
    return this.admit(text, from, key);
  }

  /** The same from a text or an email; the reply goes back out on that channel. */
  async receiveVia(text: string, from: string, key: string, via: ReplyVia): Promise<{ runId: string; duplicate: boolean }> {
    return this.admit(text, from, key, via);
  }

  /** One run's status, which `invoke` polls until the state names the run. */
  @rpc({ description: "One run's status" })
  async run(runId: string): Promise<{ id: string; status: string } | undefined> {
    const status = (await this.getState()).runs?.[runId];
    return status === undefined ? undefined : { id: runId, status };
  }

  private async admit(text: string, from: string | undefined, key: string | undefined, via?: ReplyVia): Promise<{ runId: string; duplicate: boolean }> {
    const runId = key === undefined ? `run-${Date.now()}-${crypto.randomUUID()}` : `run-${key}`;
    const state = await this.getState();
    if (state.runs?.[runId] !== undefined) return { runId, duplicate: true }; // a redelivered message runs once
    await this.setState({ ...(from === undefined ? {} : { from }), runs: { [runId]: "accepted" } });
    await this.messages.add("user", text);
    await this.queue("process", { runId, via });
    return { runId, duplicate: false };
  }

  /** The turn: read the history, produce a reply, remember it, plan a follow-up. */
  async process({ runId, via }: { runId: string; via?: ReplyVia }): Promise<void> {
    const history = await this.messages.toLangChain();
    void history; // hand this to your model
    const reply = "Hello! How can I help?";
    await this.messages.add("assistant", reply);
    await this.setState({ lastRunId: runId, lastReply: reply, at: Date.now(), runs: { [runId]: "completed" } });
    if (via) await this.deliver(reply, via);
    await this.schedule(86_400, "nudge", null, { id: "nudge" });
  }

  /** A reply back out on the channel the message came in on. */
  protected async deliver(text: string, via: ReplyVia): Promise<void> {
    const telnyx = this.env.TELNYX;
    if (!telnyx) throw new Error("replying on a channel needs the TELNYX binding");
    if (via.kind === "messaging") await telnyx.messages.send({ from: via.to, to: via.from, text, messaging_profile_id: via.profileId });
    else await telnyx.post(`/email_inboxes/${encodeURIComponent(via.inboxId)}/messages/${encodeURIComponent(via.messageId)}/actions/reply`, { body: { text } });
  }

  /** Runs a day later unless the conversation moved on. */
  async nudge(): Promise<void> {
    const last = await this.messages.last();
    if (last?.role !== "assistant") return;
  }
}
```

`receive` is what `invoke`, `chat` and the browser page call, and `receiveVia` is what a text or an email arrives through; both record the message once per key and queue the turn, so the caller is acknowledged within the 30-second budget. `process` runs as a task with minutes to think: it is the one method to change, where the history is handed to a model or a framework; a reply to a text or an email goes back out through `deliver`. `run` is the status `invoke` polls, `fetch` serves the streams and RPC over HTTP, and `authorize` admits callers (everyone, in the beta phase). The message log, the state and the schedule are the instance's own and survive restarts. [Bring Your Own Framework](/docs/agent-sdk/examples/bring-your-own-framework) is where `process` hands the history to a model or a framework.

### Run It Locally

```bash theme={null}
npm install
telnyx-acp dev
```

On a code-based project `dev` writes `.telnyx/`, what `deploy` would ship, and hands over to `telnyx-edge dev`, the local actor stack in Docker, with the class reloaded on save; the stack keeps running after Ctrl-C, and `docker compose -f .telnyx/dev/compose.yaml down` stops it.

`chat --local` and `invoke … --local` reach the class through its `receive`, and the reply is the one `process` produces. Texts, email and memory need a deploy.

### Deploy

```bash theme={null}
telnyx-acp deploy
```

There is no container to build. `deploy` writes `.telnyx/` inside the project, the manifest, the entry that mounts the class, the types and the dependencies, bundles the entry's module graph locally and ships one file; the platform builds it and rolls it out, typically about ten minutes the first time (`deploy` waits up to 25). From there the project is operated like any other: `status`, `logs`, `versions`, `delete`, and `add phone` to give it a number; a text then arrives through `receiveVia` and the reply goes back through `deliver`. A call to that number does not reach this class: the scaffold has no voice layer, so the call is answered and the media stream finds nothing to talk to. A class that takes calls wraps the base in `voice()` from [`@telnyx/edge-runtime/voice`](/docs/agent-sdk/runtime-sdk) and implements the turn engine and `warmCall`, as [The Voice Loop](/docs/agent-sdk/inside/turn-taking/voice-loop) and [The Call Webhook](/docs/agent-sdk/inside/request-handling/call-webhook) show; until it does, the number is for texts.
