---
title: Protocol lifecycle | Letta Docs
description: Understand App Server websocket channels, runtime_start, input, sync, abort, and event handling
---

App Server uses native v2 websocket frames. Every command and event is a JSON object with a `type` field. Commands are sent on the control channel. Events can arrive on either channel.

## Transport

Start App Server:

Terminal window

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

Open two websocket connections:

| Channel | URL                                      | Use                                                  |
| ------- | ---------------------------------------- | ---------------------------------------------------- |
| Control | `ws://127.0.0.1:4500/ws?channel=control` | Commands, request responses, external tool callbacks |
| Stream  | `ws://127.0.0.1:4500/ws?channel=stream`  | Streamed turn output and runtime updates             |

The server also exposes health probes:

| Endpoint       | Result                                         |
| -------------- | ---------------------------------------------- |
| `GET /readyz`  | `200 OK` when the listener accepts connections |
| `GET /healthz` | `200 OK` for no-origin local health checks     |

## Runtime startup

Send `runtime_start` before sending turns. It resolves the agent and conversation, applies runtime state, registers external tools, and replays state.

runtime\_start

```
{
  "type": "runtime_start",
  "request_id": "runtime-1",
  "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"
  },
  "recover_approvals": true,
  "force_device_status": true
}
```

The response contains the canonical runtime scope:

runtime\_start\_response

```
{
  "type": "runtime_start_response",
  "request_id": "runtime-1",
  "success": true,
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "created": {
    "agent": false,
    "conversation": false
  },
  "agent": {},
  "conversation": {}
}
```

Use the returned `runtime` for all scoped commands. Do not reconstruct it from assumptions if App Server created either object.

## Creating agents and conversations

`runtime_start` accepts exactly one of `agent_id` or `create_agent`.

Create an agent

```
{
  "type": "runtime_start",
  "request_id": "runtime-1",
  "create_agent": {
    "body": {
      "name": "Build Agent",
      "memory_blocks": []
    },
    "pin_global": true
  },
  "create_conversation": {
    "body": {}
  }
}
```

It accepts either an existing `conversation_id` or `create_conversation`. If you pass `create_agent`, you should also pass `create_conversation` for the first runtime.

## Runtime state fields

| Field                 | Direction                              | Meaning                                                                          |
| --------------------- | -------------------------------------- | -------------------------------------------------------------------------------- |
| `cwd`                 | `runtime_start`, `change_device_state` | Working directory for local tools. `null` resets to the listener boot directory. |
| `mode`                | `runtime_start`, `change_device_state` | Permission mode for local tool execution.                                        |
| `recover_approvals`   | `runtime_start`, `sync`                | Probe backend state for stale pending approvals.                                 |
| `force_device_status` | `runtime_start`, `sync`                | Force a device status replay even if the cached status did not change.           |
| `client_info`         | `runtime_start`                        | Client metadata for diagnostics and future negotiation.                          |

Valid permission modes match Letta Code device permission modes, including `standard`, `acceptEdits`, `memory`, and `unrestricted`.

## Sending turns

Use `input` with `payload.kind: "create_message"` to send a user turn.

input create\_message

```
{
  "type": "input",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "payload": {
    "kind": "create_message",
    "messages": [
      {
        "role": "user",
        "content": "Inspect the auth flow and report risks.",
        "client_message_id": "client-msg-1"
      }
    ],
    "client_tool_allowlist": ["Read", "Grep", "Glob"]
  }
}
```

`client_message_id` is optional, but useful for UI deduplication and local optimistic rows.

`client_tool_allowlist` narrows the locally executed client tools for this turn. Omit it to use the runtime’s normal toolset. Pass an empty array to expose no client tools for the turn.

## Approval responses

Use `input` with `payload.kind: "approval_response"` to answer a pending approval request.

Allow a tool call

```
{
  "type": "input",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "payload": {
    "kind": "approval_response",
    "request_id": "approval-123",
    "decision": {
      "behavior": "allow",
      "message": "Approved by controller"
    }
  }
}
```

Deny a tool call

```
{
  "type": "input",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "payload": {
    "kind": "approval_response",
    "request_id": "approval-123",
    "decision": {
      "behavior": "deny",
      "message": "Do not modify production files"
    }
  }
}
```

## Streaming and completion

The primary turn event is `stream_delta`:

stream\_delta

```
{
  "type": "stream_delta",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "delta": {
    "id": "msg-1",
    "date": "2026-06-17T00:00:00.000Z",
    "message_type": "status",
    "message": "Reading files",
    "level": "info"
  }
}
```

A turn is normally complete when the stream emits a `stream_delta` whose `delta.message_type` is `stop_reason`. Treat `loop_error` and `error_message` deltas as failures.

Also listen for runtime updates:

| Event                   | Use                                                   |
| ----------------------- | ----------------------------------------------------- |
| `update_loop_status`    | Active run IDs and waiting/running state              |
| `update_device_status`  | Runtime availability, cwd, mode, and status snapshots |
| `update_queue`          | Full turn queue snapshot                              |
| `update_subagent_state` | Subagent state snapshots                              |
| `stream_delta`          | Agent output and lifecycle deltas                     |

Do not assume every event for a turn arrives on the stream channel. Subscribe to both channels.

## Sync

Use `sync` to replay current state after reconnects or UI reloads.

sync

```
{
  "type": "sync",
  "request_id": "sync-1",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "recover_approvals": false,
  "force_device_status": true
}
```

The response only acknowledges the sync request. The replayed state arrives as normal events.

sync\_response

```
{
  "type": "sync_response",
  "request_id": "sync-1",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "success": true
}
```

## Abort

Use `abort_message` to stop active work or interrupt a pending approval.

abort\_message

```
{
  "type": "abort_message",
  "request_id": "abort-1",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  }
}
```

abort\_message\_response

```
{
  "type": "abort_message_response",
  "request_id": "abort-1",
  "runtime": {
    "agent_id": "agent-123",
    "conversation_id": "conv-123"
  },
  "aborted": true,
  "success": true
}
```

## Device capability commands

App Server also forwards device capability commands through the same v2 transport. Use these when building richer UIs around the runtime:

- Filesystem: `list_in_directory`, `get_tree`, `read_file`, `write_file`, `edit_file`, `file_ops`, `watch_file`, `unwatch_file`
- Search: `search_files`, `grep_in_files`
- Memory: `list_memory`, `read_memory_file`, `write_memory_file`, `delete_memory_file`, `memory_history`, `memory_commit_diff`, `memory_file_at_ref`
- Models and providers: `list_models`, `update_model`, `update_toolset`, `list_connect_providers`, `connect_provider`, `disconnect_provider`
- Terminals: `terminal_spawn`, `terminal_input`, `terminal_resize`, `terminal_kill`
- Schedules: `cron_list`, `cron_add`, `cron_get`, `cron_update`, `cron_delete`, `cron_delete_all`, `cron_runs`, `cron_trigger`
- Channels: `channels_list`, `channel_accounts_list`, `channel_account_create`, and related channel management commands

Use `request_id` on commands that return a response. Keep command handlers tolerant of additional fields.

## Legacy command names to avoid

Do not use v1 listener command names in new App Server clients:

| Do not send                 | Use instead                               |
| --------------------------- | ----------------------------------------- |
| `request_state`             | `sync`                                    |
| `change_cwd`                | `change_device_state` with `payload.cwd`  |
| `change_mode`               | `change_device_state` with `payload.mode` |
| `cancel_run`                | `abort_message`                           |
| `recover_pending_approvals` | `sync` with `recover_approvals: true`     |
