Skip to content

Capabilities

An SDK app can be a headless bot (an on_inbound handler, a schedule, a custom tool) or a thicker product with its own UI, specialist agent, workflow hooks, and HTTP routes. Both shapes ship today — the Office Assistant port in examples/office-assistant/ exercises the thicker end.

CapabilityAPIExample
Subscribe to inbound messagesapp.onInbound((ctx, env) => …)Triage emails into categories
Run on a cron scheduleapp.onSchedule('name', (ctx) => …)Weekday 07:30 prep digest
Run on tenant install / uninstallapp.onInstall, app.onUninstallProvision per-tenant resources
Expose a custom tool the tenant agent can callapp.tool('name', opts, fn)summarize_pdf, compose_quote
Wire the tenant’s chat agent to your toolsworkflows: in manifestTeach the orchestrator when to call your sequence
Provision a specialist agentagents: in manifestYour own LLM persona installed alongside the app
Call platform toolsctx.tools.call('email_send', {...})Reply to inbound, query Odoo, render PDF
Read encrypted secretsctx.secrets.get('OPENAI_KEY')LLM credentials, third-party API keys
Run any code in your containerYour Dockerfile + your codeLLM calls, scraping, ML inference, your own DB
Outbound HTTP to anywhereStandard fetch / requestsCall your own backend, third-party APIs
Mount custom HTTP routesapp.httpRoute('/inbox', …) + http_routes: in manifestOAuth callbacks, third-party webhooks, downloads
Talk to other installed appsctx.apps.invoke(...), ctx.events.emit(...), ctx.team.delegate(...)Cross-app RPC, pub/sub wires, intra-app delegation
Post to a shared roomctx.rooms.post(room_id, ...)Surface progress, results, or asks alongside humans
Track follow-upsctx.commitments.add(...)Due-dated promises that land in the inbox
Run recurring agent routinesheartbeats: in manifestCheap pre-filter then LLM only when there’s work

A reasonable mental model: your app is a small service the platform wakes via well-defined event types (inbound, schedule, tool call, lifecycle, HTTP route, cross-app event). Inside the container you have full Python or TypeScript freedom.

Your app can ship a sandboxed frontend bundle that mounts at /apps/<your-slug> inside the tenant shell. Static HTML/CSS/JS, served from apps-cdn.linkworld.ai, talking to the platform via a postMessage bridge with full scope + audit enforcement.

CapabilityAPINotes
Ship a static frontendlinkworld deploy --bundle web/Tarballs the dir, uploads to apps-cdn, version-frozen
Call platform tools from the iframebridge.tools.call('email_send', {...})Same scope checks as server-side calls
Call your own HTTP routes from the iframebridge.callRoute('GET', '/inbox')Proxied through the platform with auth + audit
Inherit the tenant’s themeAuto-applied via --lw-bg, --lw-text, etc.Light/dark switches forwarded automatically
Resize the iframe to fit contentbridge.resize(height)Clamped 80–8000px
Update the URL bar for deep-linksbridge.navigate('/settings')Hash-based, doesn’t trigger a Next route
Per-(tenant, app) key-value storebridge.kv.get/set(...)Same store as ctx.kv on the server

The platform also renders a right-side panel with built-in tabs (chat with your specialist agent, install settings, files) and slots for custom tabs — no bundle code needed for those.

All I/O from the bundle iframe goes through the bridge, which proxies platform-tool calls and your own HTTP routes with scope checks and audit logging. Direct fetch() from the iframe is disabled by CSP — that’s the trust boundary, and it’s what makes the audit trail complete.

See Building app UIs for the full bundle pipeline.

Your app’s state lives in your container, not in platform tables. Pick whatever fits:

  • SQLite in a mounted volume — fits most single-tenant apps
  • Postgres / Supabase / your own DB — for richer querying or multi-region setups, call from inside the container
  • S3 / Cloudflare R2 / object storage — large blobs, generated PDFs, audio
  • Upstash Redis or similar — caches, rate-limit counters, ephemeral state
  • ctx.kv + bridge.kv — built-in per-(tenant, app) key-value store for small structured state (settings, last-run timestamps, small lookup tables). Same store from server and bundle.
  • Odoo custom fields when the tenant has erp.write — a x_my_app_state JSONB column on a custom model lets you persist app state inside the tenant’s own data, which is often what they want for compliance.

The tenant’s platform data belongs to the tenant — they keep control of it through Workspace Control, not through your app.

For third-party services with a platform tool wrapper (Microsoft Graph, Odoo, WhatsApp, image generators…) call ctx.tools.call(...) — scope grants and audit logs cover you for free.

For everything else (Stripe, LinkedIn, Notion, the long tail), call the API directly from your container with your own credentials stored via ctx.secrets.get(...). Those calls don’t appear in the tenant’s platform audit trail, so log inside your handler what you sent and got back.

Your app acts under a synthetic identity, app-principal:<your-slug> — that’s what shows up in audit logs and what tools see as the caller. The app is its own principal, not a stand-in for any specific user.

When the app needs to act “as” a specific person (send mail “from” the tenant’s CEO, post a calendar invite from a sales rep), the mechanism is channel-binding: the tenant grants the relevant scope (mail.send, calendar.write), and the underlying connection delegates to whatever account the tenant configured for that channel — usually a shared service mailbox, sometimes a specific user. Your app names the what; the tenant controls the who.

A representative checklist if you’re porting something with the shape of Office Assistant — custom tools, orchestrator wiring, specialist agent, dashboard UI, HTTP routes, install-time consent:

PieceWhere it lives
Custom toolstools: + app.tool('name', …)
Orchestrator wiringworkflows: (guide)
Specialist agentagents: (manifest reference)
Dashboard UIFrontend bundle at /apps/<slug> (guide)
Built-in chat / settings / files tabschrome: (guide)
HTTP routeshttp_routes: + @app.http_route (SDK reference)
Install-time consentinstall_settings: (guide)
Cross-app collaborationctx.apps.invoke, ctx.events.emit, ctx.team.delegate (reference)

The full source for the Office Assistant SDK port is at examples/office-assistant/ — start there.