Pi for Excel supports runtime extensions that can register commands/tools, render UI in the sidebar, and use mediated host capabilities (LLM, HTTP, storage, clipboard, agent steering/context, skills, downloads).
Status: shipped with feature flags for advanced controls. Inline-code and remote-URL extensions run in sandbox runtime by default; built-in/local-module extensions stay on host runtime. Roll back untrusted sources to host runtime only via
/experimental on extension-sandbox-rollback. Additive Widget API v2 is feature-flagged via/experimental on extension-widget-v2.
- Open the manager with:
/extensions
- Install one of:
- Pasted code (recommended for quick prototypes)
- URL module (requires explicit unsafe opt-in)
- Enable/disable/reload/uninstall from the same manager.
- Review and edit capability permissions per extension (changes auto-reload enabled extensions).
You can ask Pi to build and install an extension without leaving the conversation.
Example prompt:
Create an extension named "Quick KPI" that adds /kpi-summary.
The command should read the active sheet, find numeric columns, and show a small widget with totals.
Then install it.Pi can use the extensions_manager tool to:
- list installed extensions
- install an extension from generated code
- enable/disable, reload, and uninstall extensions
| Source | How to use | Default policy |
|---|---|---|
| Local module specifier | Built-ins/programmatic installs (not currently exposed in /extensions UI) |
✅ allowed |
| Blob URL (pasted code) | /extensions → install code (stored in settings, loaded via blob URL + dynamic import) |
✅ allowed |
| Remote HTTP(S) URL | /extensions → install URL |
❌ blocked by default |
Enable remote URLs only if you trust the code source:
/experimental on remote-extension-urlsAn extension module must export activate(api) (named export or default export).
export function activate(api) {
// register commands/tools/UI
}Optional cleanup hooks:
activate(api)may return:void- a cleanup function
- an array of cleanup functions
- Module may also export
deactivate()
On disable/reload/uninstall, Pi runs cleanup functions (reverse order), then deactivate().
Registers a slash command.
busyAllowedcontrols whether the command can run while Pi is actively streaming/busy.- Default:
truefor extension commands. - Set
busyAllowed: falsewhen the command should wait until Pi is idle.
Registers or removes an agent-callable tool.
Notes:
parametersshould be a JSON-schema/TypeBox-compatible object.- In sandbox runtime, plain JSON schema objects are accepted and wrapped safely by the host bridge.
- Tool names must not conflict with core built-in tools.
- Tool names must be unique across enabled extensions.
toolDef.requiresConnectioncan be a string or string[] of connection ids.- IDs are owner-qualified automatically (for extension
ext.foo,"apollo"becomes"ext.foo.apollo"). - Required connections are preflight-checked before tool execution.
- IDs are owner-qualified automatically (for extension
Connection registration + credential lifecycle APIs:
connections.register(definition)connections.unregister(connectionId)connections.list()/connections.get(connectionId)connections.setSecrets(connectionId, secrets)/connections.clearSecrets(connectionId)connections.markValidated(connectionId)/connections.markInvalid(connectionId, reason)connections.markStatus(connectionId, status, reason?)connections.getSecrets(connectionId)(escape hatch; gated byconnections.secrets.read)
Use this to declare extension-specific connection requirements (capability + secret fields), store credentials securely in host-managed local settings, and surface deterministic setup/auth states to the assistant.
Default recommendation:
- Prefer host-managed auth injection (
http.fetch(..., { connection: "..." })) so extension code does not handle raw secrets. - Use
connections.getSecrets(...)only for advanced/non-standard auth flows (for example SDK bootstrap or custom request signing).
Registered extension connections appear automatically in the /tools overlay under Extension connections (between Web search and MCP servers). Each connection renders as a card with:
- Status badge: Connected / Not configured / Invalid / Error
- Secret field inputs (empty by default;
✓ Savedindicator when a value exists) - Save (merge-patch — only entered fields are updated) and Clear actions
- Error callout when the last tool call triggered an auth failure
The section is hidden when no extensions are installed and shows an empty state when extensions are installed but none have registered connections.
Optional connection definition block for http.fetch(..., { connection }):
httpAuth: {
placement: "header",
headerName: "Authorization",
valueTemplate: "Bearer {apiKey}",
allowedHosts: ["api.example.com"],
}Rules:
placementcurrently supports"header"only.valueTemplateplaceholders ({...}) must reference declaredsecretFieldsids.allowedHostsis required and uses exact host matching for safe auth injection.
Agent API surface:
agent.raw(host runtime only; capability-gated)agent.injectContext(content)agent.steer(content)agent.followUp(content)
Host-mediated LLM completion. Supports optional model override (provider/modelId or model id), optional systemPrompt, and messages (user/assistant).
Cache/prompt-shape guidance for extension authors:
- Treat
llm.completeas an independent side completion by default (separate from the main chat loop/tool prefix). - Host runtime uses an extension-scoped side session key for
llm.complete, so side-call churn is isolated from the primary runtime session telemetry. - Keep
systemPromptshort and stable across repeated extension calls when possible. - Put volatile data in
messagesrather than rewritingsystemPromptevery call. - Use
agent.injectContext/agent.steerwhen you need to influence the primary runtime conversation instead of emulating it throughllm.complete.
Host-mediated outbound HTTP fetch with security policy enforcement.
Options include:
method,headers,body,timeoutMs- optional
connectionid for host-managed auth injection
When connection is provided:
- host qualifies and validates ownership (
ext.<id>.<connection>) - connection status must be
connected - request host must match
definition.httpAuth.allowedHosts - auth headers are injected from stored connection secrets
- 401/403 responses mark the connection as runtime auth-failed and surface structured connection errors
Persistent extension-scoped key/value storage.
Writes plain text to clipboard via host bridge.
Read bundled+external skills, and install/uninstall external skills.
Triggers a browser download.
Subscribe to runtime events (returns unsubscribe function).
Show or dismiss a full-screen overlay.
Primary widget lifecycle API (feature-flagged):
upsertcreates/updates by stablespec.idremoveunmounts one widget by idclearunmounts all widgets owned by the extension
Enable with:
/experimental on extension-widget-v2upsert(spec) supports optional metadata: title, placement (above-input | below-input), order, collapsible, collapsed, minHeightPx, maxHeightPx.
Widget API v2 host behavior:
collapsible: truerenders a built-in header toggle (expand/collapse) for predictable UX.- Omitted optional fields preserve prior widget metadata on upsert (title/placement/order/collapse/size).
minHeightPx/maxHeightPxare clamped to safe host bounds (72..640px).- If both bounds are set and
maxHeightPx < minHeightPx, host coercesmaxHeightPxup tominHeightPx. - Pass
nullforminHeightPx/maxHeightPxto clear a previously set bound while keeping other widget metadata unchanged. - Use stable, extension-local ids (
"main","summary","warnings", etc.) and callapi.widget.clear()for explicit in-session teardown when needed.
- Keep widget ids stable and semantic; avoid random ids each render.
- Use content-only refreshes (
upsert({ id, el })) when layout metadata is unchanged. - Put long content in bounded cards (
maxHeightPx) so chat/input layout stays predictable. - Prefer host collapse controls (
collapsible) over custom hide/show chrome where possible. - Use
placement: "below-input"sparingly (for low-priority/persistent helper widgets).
export function activate(api) {
const renderSummary = () => {
const el = document.createElement("div");
el.textContent = "Summary: 4 checks passed";
api.widget.upsert({
id: "summary",
el,
title: "Sheet summary",
order: 0,
collapsible: true,
collapsed: false,
minHeightPx: 96,
maxHeightPx: 220,
});
};
const renderWarnings = () => {
const el = document.createElement("div");
el.textContent = "Warnings: 2 outliers detected";
api.widget.upsert({
id: "warnings",
el,
title: "Warnings",
placement: "below-input",
order: 10,
collapsible: true,
collapsed: true,
});
};
renderSummary();
renderWarnings();
return () => {
api.widget.clear();
};
}Show a short toast notification.
export function activate(api) {
api.registerCommand("hello_ext", {
description: "Say hello from extension",
handler: () => {
api.toast("Hello from extension 👋");
},
});
const schema = {
type: "object",
properties: {
text: { type: "string", description: "Text to echo" },
},
required: ["text"],
additionalProperties: false,
};
api.registerTool("echo_text", {
description: "Echo text back",
parameters: schema,
async execute(params) {
const text = typeof params.text === "string" ? params.text : "";
return {
content: [{ type: "text", text: `Echo: ${text}` }],
details: { length: text.length },
};
},
});
const onTurnEnd = api.onAgentEvent((ev) => {
if (ev.type === "turn_end") {
// optional event handling
}
});
return () => {
onTurnEnd();
api.widget.clear();
api.overlay.dismiss();
};
}The /extensions manager shows capability toggles per installed extension.
- Install from URL/code asks for confirmation and shows the default granted permissions.
- Enabling an extension with higher-risk grants prompts for confirmation.
- Toggling a permission updates stored grants in
extensions.registry.v2. - If the extension is enabled, Pi reloads it immediately so revokes/grants take effect right away.
- If
/experimental on extension-permissionsis off, configured grants are still saved but not enforced until you enable the flag.
High-risk capabilities include:
tools.registeragent.readagent.events.readllm.completehttp.fetchagent.context.writeagent.steeragent.followupskills.writeconnections.readwriteconnections.secrets.read
Default behavior:
- inline-code and remote-URL extensions run in an iframe sandbox runtime
- built-in/local-module extensions stay on host runtime
/extensionsshows runtime mode per extension
If maintainers need an emergency rollback path, enable host-runtime fallback for untrusted sources:
/experimental on extension-sandbox-rollbackDisable rollback and return to default sandbox routing:
/experimental off extension-sandbox-rollbackYou can also toggle this in /extensions via the Sandbox runtime (default for untrusted sources) card.
Current sandbox bridge limitations (intentional for this slice):
api.agent.rawis not available in sandbox runtime (use bridgedinjectContext/steer/followUp)- widget/overlay rendering uses a structured, sanitized UI tree (no raw HTML / no
innerHTML) - interactive callbacks are limited to explicit action markers (
data-pi-action), which dispatch click events back inside sandbox runtime - Widget API v2 (
widget.upsert/remove/clear) is available only whenextension-widget-v2is enabled
Local module specifiers are used for built-ins (for example the seeded Snake extension).
For built-in/repo extensions:
- Add a file under
src/extensions/*.ts - Export
activate(api) - Register/load it through app/runtime wiring (the
/extensionsUI currently exposes URL + pasted-code installs)
Production builds only bundle local extension modules matched by src/extensions/*.{ts,js}.
If a local specifier is not bundled, loading fails with a clear error.
- "Extension module "..." must export an activate(api) function"
- Missing/invalid export.
- "Remote extension URL imports are disabled by default"
- Enable with
/experimental on remote-extension-urls.
- Enable with
- "Local extension module "..." was not bundled"
- Local module path is outside bundled extension files.
- Command/tool already registered
- Name conflicts with built-in or another extension.
- Cleanup failure during disable/reload
- Check extension cleanup functions and optional
deactivate().
- Check extension cleanup functions and optional
- Extensions can read/write workbook data through registered tools and host APIs.
- Remote URL loading is intentionally off by default.
- Untrusted extension sources (inline/remote) run in sandbox runtime by default.
- Built-in/local-module extensions remain on host runtime.
- Capability gates can be enabled with
/experimental on extension-permissions. - Rollback kill switch for untrusted host-runtime fallback:
/experimental on extension-sandbox-rollback.