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.
Choose the right surface
Section titled “Choose the right surface”| Need | Use |
|---|---|
| High-level TypeScript API for Letta Code sessions | Letta Code SDK |
| Protocol-level control of a local Letta Code runtime | App Server |
| Direct server-side agent API without local computer-use tools | Letta API client SDKs |
| Human terminal workflow | Letta Code CLI |
Use App Server when you need to control runtime lifecycle, stream detailed events, or expose controller-owned tools.
Controller responsibilities
Section titled “Controller responsibilities”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
Runtime registry pattern
Section titled “Runtime registry pattern”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:
- Load records from your database.
- Start App Server.
- For each active record, call
runtime_startwhen you need that runtime. - Store the returned
runtimeexactly as App Server returns it. - Call
syncbefore 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.
Multi-agent orchestration pattern
Section titled “Multi-agent orchestration pattern”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_startto start each teammate runtimeinputorrunTurnto dispatch workstream_deltato capture progress and results- External tools like
update_task,complete_task, ordispatch_taskfor structured coordination
Keep task state in the controller. Do not encode your full task database into agent memory or App Server runtime state.
External tools as app commands
Section titled “External tools as app commands”Prefer external tools for app-specific commands.
Good candidates:
dispatch_taskupdate_progresscomplete_tasklookup_ticketread_dashboard_statesend_user_notification
Avoid adding protocol commands for these unless multiple independent clients need the same primitive and the behavior belongs in Letta Code itself.
Reconnect and replay pattern
Section titled “Reconnect and replay pattern”Websocket clients should be able to reconnect without losing their model of the world.
On reconnect:
- Reopen control and stream sockets.
- Call
runtime_startfor the selected runtime if the App Server process restarted. - Call
syncwithforce_device_status: true. - Rebuild UI state from replayed events plus your durable controller state.
- 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.
Approval handling pattern
Section titled “Approval handling pattern”Decide early how your controller handles permission requests.
| Controller type | Recommendation |
|---|---|
| Human-facing UI | Render approval cards and answer with input.kind: "approval_response" |
| Bot or background worker | Use a restrictive permission mode or tool allowlist |
| Trusted local automation | Use acceptEdits or unrestricted only when the environment is safe |
| Multi-tenant service | Keep 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.
Tool visibility pattern
Section titled “Tool visibility pattern”Use the smallest visibility surface that fits the task:
- Omit
client_tool_allowlistfor normal Letta Code behavior. - Use
client_tool_allowlistto narrow built-in client tools for one turn. - Register unscoped external tools for safe controller actions that are always available.
- Register scoped external tools and pass
external_tool_scope_idsfor 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.
Event handling pattern
Section titled “Event handling pattern”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.
Concurrency pattern
Section titled “Concurrency pattern”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_queueandupdate_loop_statusto reflect waiting/running state. - Dispatch parallel work across different teammate runtimes, not the same conversation.
- Set clear timeouts around controller-owned external tools.
When NOT to use App Server
Section titled “When NOT to use App Server”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.
Implementation checklist for agents
Section titled “Implementation checklist for agents”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.runtimevalues. - Register external tools during every
runtime_start. - Keep product state outside App Server.
- Implement reconnect with
runtime_startandsync. - Add per-runtime turn locks.
- Decide approval behavior for background runs.
- Log unknown protocol events without crashing.