Use this skill to create or update trusted global Letta Code extensions in:
~/.letta/extensions/
Extensions are trusted local apps for Letta Code. They add small composable capabilities through extension APIs, not by importing app internals. Prefer scoped handles (ctx.conversation, ctx.cwd, ctx.agent, letta.getContext()) and guard optional UI with letta.capabilities.
Choose the right capability
| User wants | Build |
|---|---|
| Agent/model should autonomously call a local capability | Extension tool |
User wants /foo to send a prompt or run local UI logic |
Extension command |
| Slash command represents a reusable agent workflow | Skill + thin extension command |
| Command should work while the main agent is busy | Command with runWhenBusy: true, handled, panel/status, and usually ctx.conversation.fork() |
| Show transient output above input | Panel, usually from a command |
| Show small persistent state | Status value |
| React to app/session lifecycle or transform outbound turns | Event |
| Change the bottom statusline appearance | Use customizing-statusline, not this skill |
Default to a tool when the model should decide when to use the capability. Default to a command when the human explicitly invokes it. Compose capabilities when the UX needs it, e.g. command + panel + scoped conversation fork.
Workflow
- Inspect
~/.letta/extensions/for related files. - Preserve unrelated extension code. Prefer a focused new file if merging would be messy.
- Choose the extension shape:
- simple tool/command/event: read the specific recipe below
- multi-capability or stateful extension: also read
references/architecture.md
- Load only the needed recipe:
- tools:
references/tools.md - commands:
references/commands.md - events:
references/events.md - panels/status/capabilities:
references/ui.md - busy side-question pattern:
references/btw-command.md
- tools:
- Write a single-file extension unless the user asks for something larger.
- Return disposers for registered commands/tools/events, timers, subscriptions, and panels that should close on reload.
- Do a basic review: valid names, descriptions present, schemas are object schemas, optional capabilities guarded, scoped APIs used, cleanup returned.
- Tell the user the absolute file path changed and to run
/reload. If an extension breaks startup or command handling, recover withletta --no-extensionsorLETTA_DISABLE_EXTENSIONS=1 letta.
Core extension shape
export default function activate(letta) {
const disposers = [];
if (letta.capabilities.tools) {
disposers.push(letta.tools.register(/* ... */));
}
if (letta.capabilities.commands) {
disposers.push(letta.commands.register(/* ... */));
}
return () => {
for (const dispose of disposers.reverse()) dispose();
};
}
Use letta.capabilities for optional behavior:
letta.capabilities.tools
letta.capabilities.commands
letta.capabilities.events.lifecycle
letta.capabilities.events.tools
letta.capabilities.events.turns
letta.capabilities.ui.panels
letta.capabilities.ui.statusValues
letta.capabilities.ui.customStatuslineRenderer
Scoped API model
- In commands and events, use
ctx.conversationfor conversation operations:ctx.conversation.getHistory()for recent messagesctx.conversation.fork()for independent/background model workforked.sendMessageStream([...])to stream from a fork
- In tools, use
ctx.conversation.getHistory()when the tool needs recent context. - Use
letta.clientonly for server-specific Letta API calls; do not use it as a substitute for scoped conversation helpers. - Do not import
@/backend,@/cli, or other Letta Code internals from extension files.
Rules
- Global trusted code only for now. Do not create project extensions.
- Do not assume extra npm packages are available.
- Do not do surprising side effects on startup; extensions activate on app start and
/reload. - Keep user-facing output short and intentional.
- Prefer Node/Bun standard APIs (
node:child_process,node:fs, etc.) for local work. - For shell execution, prefer
execFile/spawnover shell strings. - Do not use emojis for loading states; use text or spinner-like characters if the user asks for loading UI.
- For
runWhenBusy: true, do not returnprompt; returnhandledand own the UI/background work. - Treat
turn_startas powerful trusted code: keep transforms narrow and unsurprising.
Pre-flight checklist for complex extensions
Before finishing, verify:
- The extension has one clear owner/file and does not mix unrelated features.
- Command/tool IDs are valid; command overrides of built-ins are intentional, and tool IDs do not collide with built-ins.
- Tool descriptions explain when the model should call them.
- JSON schemas are object schemas with useful descriptions.
- Optional UI/event/statusline APIs are capability-guarded.
- Timers, intervals, event registrations, and panels are cleaned up in a disposer.
- Busy commands return
{ type: "handled" }quickly and avoid main-conversation sends. - Conversation work uses
ctx.conversationor forked handles, not app internals. - Local shell/file work is scoped to
ctx.cwd/ctx.workingDirectoryunless intentionally global. - Errors shown to the user are short and actionable.
References
references/architecture.md- composition, state, cleanup, scoped conversation, and review checklist for complex extensionsreferences/tools.md- extension tools the model can callreferences/commands.md- slash commands, command results, and skill-backed commandsreferences/events.md- lifecycle and turn event handlersreferences/ui.md- panels, status values, capability guardsreferences/btw-command.md- advanced busy-safe side-question command using scoped conversation helpers