Skip to content
Letta Code Letta Code Letta Docs
Sign up

Integration patterns

Build robust App Server controllers for custom UIs, teams, dashboards, and background orchestration

App Server is an execution substrate. Your controller should own product state and use App Server to run Letta Code agents.

This page is written for agents and developers implementing controllers. Use it as a checklist before adding new protocol features.

NeedUse
High-level TypeScript API for Letta Code sessionsLetta Code SDK
Protocol-level control of a local Letta Code runtimeApp Server
Direct server-side agent API without local computer-use toolsLetta API client SDKs
Human terminal workflowLetta Code CLI

Use App Server when you need to control runtime lifecycle, stream detailed events, or expose controller-owned tools.

Keep these responsibilities in your application:

  • Product objects, such as teams, tasks, projects, dashboards, and users
  • Durable job state and results
  • UI routing and optimistic state
  • Reconnect logic and replay checkpoints
  • Domain-specific authorization
  • External API clients and secrets

Let App Server own these responsibilities:

  • Resolving and creating Letta agents and conversations
  • Running turns
  • Preparing and executing local tools
  • Managing runtime CWD and permission mode
  • Streaming runtime events
  • Routing external tool callbacks
  • Replaying runtime state through sync

Store runtime metadata in your controller database.

interface RuntimeRecord {
id: string;
agentId: string;
conversationId: string;
cwd: string | null;
displayName: string;
role: string;
lastStartedAt: string;
}

On startup:

  1. Load records from your database.
  2. Start App Server.
  3. For each active record, call runtime_start when you need that runtime.
  4. Store the returned runtime exactly as App Server returns it.
  5. Call sync before rendering stale UI state.

Do not rely on in-memory runtime state alone. App Server can restart, and a controller should be able to recover from its own database.

For teams or agent pools, model each teammate as a persistent agent plus one or more conversations.

interface Teammate {
name: string;
role: string;
agentId: string;
defaultConversationId: string;
}
interface TaskRun {
id: string;
teammateName: string;
conversationId: string;
status: "queued" | "running" | "done" | "error";
result?: string;
}

Use App Server for execution:

  • runtime_start to start each teammate runtime
  • input or runTurn to dispatch work
  • stream_delta to capture progress and results
  • External tools like update_task, complete_task, or dispatch_task for structured coordination

Keep task state in the controller. Do not encode your full task database into agent memory or App Server runtime state.

Prefer external tools for app-specific commands.

Good candidates:

  • dispatch_task
  • update_progress
  • complete_task
  • lookup_ticket
  • read_dashboard_state
  • send_user_notification

Avoid adding protocol commands for these unless multiple independent clients need the same primitive and the behavior belongs in Letta Code itself.

Websocket clients should be able to reconnect without losing their model of the world.

On reconnect:

  1. Reopen control and stream sockets.
  2. Call runtime_start for the selected runtime if the App Server process restarted.
  3. Call sync with force_device_status: true.
  4. Rebuild UI state from replayed events plus your durable controller state.
  5. Re-register external tools through runtime_start.

runtime_start.external_tools is startup-bound. If the App Server process or control session restarts, register tools again.

Decide early how your controller handles permission requests.

Controller typeRecommendation
Human-facing UIRender approval cards and answer with input.kind: "approval_response"
Bot or background workerUse a restrictive permission mode or tool allowlist
Trusted local automationUse acceptEdits or unrestricted only when the environment is safe
Multi-tenant serviceKeep App Server isolated per tenant or machine, and enforce authorization in your controller

Do not leave background teammates waiting indefinitely on human approvals unless that is part of the product design.

Use the smallest visibility surface that fits the task:

  1. Omit client_tool_allowlist for normal Letta Code behavior.
  2. Use client_tool_allowlist to narrow built-in client tools for one turn.
  3. Register unscoped external tools for safe controller actions that are always available.
  4. Register scoped external tools and pass external_tool_scope_ids for context-specific abilities.

Do not use external tools as a security boundary by themselves. Treat them as model-facing visibility controls and enforce real authorization in the controller.

Build event handlers as reducers. Store raw events when practical.

client.onMessage((message, channel) => {
appendEventLog({ channel, message, receivedAt: Date.now() });
switch (message.type) {
case "stream_delta":
updateTranscript(message.runtime, message.delta);
break;
case "update_loop_status":
updateRuntimeStatus(message.runtime, message.loop_status);
break;
case "update_queue":
replaceQueueSnapshot(message.runtime, message.queue);
break;
default:
preserveUnknownEvent(message);
}
});

Use tolerant parsing. New event fields and event types may appear as App Server evolves.

App Server can coordinate multiple runtimes, but each {agent_id, conversation_id} should have at most one active turn from your controller at a time.

Recommended controller behavior:

  • Keep a per-runtime turn lock.
  • Queue user messages while a runtime is active.
  • Use update_queue and update_loop_status to reflect waiting/running state.
  • Dispatch parallel work across different teammate runtimes, not the same conversation.
  • Set clear timeouts around controller-owned external tools.

Do not use App Server when:

  • You only need to create agents and send normal server-side messages through the Letta API.
  • You need a public remote API exposed to browsers over the internet.
  • You cannot run a trusted local Letta Code process.
  • You want a stable high-level SDK abstraction and do not need protocol control.

Use Letta Code SDK or the Letta API client SDKs instead.

Before shipping an App Server integration:

  • Start App Server on a loopback URL.
  • Connect both control and stream channels.
  • Treat both channels as event sources.
  • Store canonical runtime_start_response.runtime values.
  • Register external tools during every runtime_start.
  • Keep product state outside App Server.
  • Implement reconnect with runtime_start and sync.
  • Add per-runtime turn locks.
  • Decide approval behavior for background runs.
  • Log unknown protocol events without crashing.