---
title: Quickstart | Letta Docs
description: Start Letta Code App Server, connect websocket channels, and run a turn
---

Start App Server when your application needs protocol-level access to a local Letta Code runtime.

## Install Letta Code

Terminal window

```
npm install -g @letta-ai/letta-code
```

## Start the server

Run App Server on a fixed local port during development:

Terminal window

```
letta app-server --listen ws://127.0.0.1:4500
```

The process prints the base URL and the two channel URLs:

```
Listening on ws://127.0.0.1:4500
Control: ws://127.0.0.1:4500/ws?channel=control
Stream:  ws://127.0.0.1:4500/ws?channel=stream
```

You can also omit `--listen`. App Server will bind to an available loopback port and print the selected URL.

## Connect with the client helper

Use the exported App Server client for TypeScript integrations.

Terminal window

```
npm install @letta-ai/letta-code ws
```

app-server-client.ts

```
import WebSocket from "ws";
import { createAppServerClient } from "@letta-ai/letta-code/app-server-client";


const client = await createAppServerClient({
  url: "ws://127.0.0.1:4500",
  WebSocket,
}).connect();


client.onMessage((message, channel) => {
  console.log(channel, message.type);
});
```

The client opens both `/ws?channel=control` and `/ws?channel=stream`. Treat messages from both channels as the runtime event stream.

## Start a runtime

Start a runtime for an existing agent and conversation:

```
const started = await client.runtimeStart({
  agent_id: "agent-123",
  conversation_id: "conv-123",
  cwd: "/Users/me/project",
  mode: "standard",
  client_info: {
    name: "my_app",
    title: "My App",
    version: "0.1.0",
  },
});


if (!started.success || !started.runtime) {
  throw new Error(started.error ?? "Failed to start runtime");
}


const runtime = started.runtime;
```

Create a new conversation for an existing agent by omitting `conversation_id` and passing `create_conversation`:

```
const started = await client.runtimeStart({
  agent_id: "agent-123",
  create_conversation: {
    body: {},
  },
  cwd: "/Users/me/project",
});
```

Create a new agent and conversation in one call:

```
const started = await client.runtimeStart({
  create_agent: {
    body: {
      name: "App Server Agent",
      memory_blocks: [],
    },
  },
  create_conversation: {
    body: {},
  },
  cwd: "/Users/me/project",
  mode: "acceptEdits",
});
```

`runtime_start_response.runtime` is the canonical scope for future commands. Store and reuse it.

## Send a turn

Use `runTurn` when you want a promise that resolves when the turn completes:

```
const result = await client.runTurn({
  runtime,
  payload: {
    kind: "create_message",
    messages: [
      {
        role: "user",
        content: "Inspect this repository and summarize the test setup.",
      },
    ],
  },
});


console.log(result.stopReason);
```

Use `input` when you want to fire the turn and manage completion from events yourself:

```
client.input({
  runtime,
  payload: {
    kind: "create_message",
    messages: [
      {
        role: "user",
        content: "Run the relevant tests.",
      },
    ],
  },
});
```

## Read stream events

The main streamed output is `stream_delta`:

```
client.onMessage((message) => {
  if (message.type !== "stream_delta") return;


  const delta = message.delta;
  if ("message_type" in delta && delta.message_type === "loop_error") {
    console.error(delta.message);
    return;
  }


  console.log(delta);
});
```

`stream_delta.delta` can be either a standard Letta streaming message delta or a Letta Code lifecycle event such as tool starts/ends, command output, retries, status messages, and terminal stop reasons. Handle unknown delta shapes defensively and preserve them in logs while the protocol evolves.

## Sync state

Call `sync` after reconnecting or when a UI needs a fresh snapshot:

```
await client.sync({
  runtime,
  recover_approvals: false,
  force_device_status: true,
});
```

`recover_approvals: false` is useful for lightweight UI refreshes. Leave it unset when you want App Server to probe backend state for stale pending approvals.

## Abort active work

Cancel an active turn with `abort_message`:

```
const aborted = await client.abort({ runtime });
console.log(aborted.aborted);
```

## Minimal raw websocket flow

If you do not use the helper, send native JSON frames over the control socket.

runtime\_start

```
{
  "type": "runtime_start",
  "request_id": "runtime-1",
  "agent_id": "agent-123",
  "conversation_id": "conv-123",
  "cwd": "/Users/me/project",
  "mode": "standard"
}
```

input

```
{
  "type": "input",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "payload": {
    "kind": "create_message",
    "messages": [
      {
        "role": "user",
        "content": "Summarize this project."
      }
    ]
  }
}
```

## Next steps

- [Protocol lifecycle](/letta-code/app-server/protocol-lifecycle/index.md) - Learn the command and event flow.
- [External tools](/letta-code/app-server/external-tools/index.md) - Register tools that execute in your controller.
- [Integration patterns](/letta-code/app-server/integration-patterns/index.md) - Design robust controllers and team orchestrators.
