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.
Server-side surfaces
Section titled “Server-side surfaces”| Capability | API | Example |
|---|---|---|
| Subscribe to inbound messages | app.onInbound((ctx, env) => …) | Triage emails into categories |
| Run on a cron schedule | app.onSchedule('name', (ctx) => …) | Weekday 07:30 prep digest |
| Run on tenant install / uninstall | app.onInstall, app.onUninstall | Provision per-tenant resources |
| Expose a custom tool the tenant agent can call | app.tool('name', opts, fn) | summarize_pdf, compose_quote |
| Wire the tenant’s chat agent to your tools | workflows: in manifest | Teach the orchestrator when to call your sequence |
| Provision a specialist agent | agents: in manifest | Your own LLM persona installed alongside the app |
| Call platform tools | ctx.tools.call('email_send', {...}) | Reply to inbound, query Odoo, render PDF |
| Read encrypted secrets | ctx.secrets.get('OPENAI_KEY') | LLM credentials, third-party API keys |
| Run any code in your container | Your Dockerfile + your code | LLM calls, scraping, ML inference, your own DB |
| Outbound HTTP to anywhere | Standard fetch / requests | Call your own backend, third-party APIs |
| Mount custom HTTP routes | app.httpRoute('/inbox', …) + http_routes: in manifest | OAuth callbacks, third-party webhooks, downloads |
| Talk to other installed apps | ctx.apps.invoke(...), ctx.events.emit(...), ctx.team.delegate(...) | Cross-app RPC, pub/sub wires, intra-app delegation |
| Post to a shared room | ctx.rooms.post(room_id, ...) | Surface progress, results, or asks alongside humans |
| Track follow-ups | ctx.commitments.add(...) | Due-dated promises that land in the inbox |
| Run recurring agent routines | heartbeats: in manifest | Cheap 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.
In-app UI
Section titled “In-app UI”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.
| Capability | API | Notes |
|---|---|---|
| Ship a static frontend | linkworld deploy --bundle web/ | Tarballs the dir, uploads to apps-cdn, version-frozen |
| Call platform tools from the iframe | bridge.tools.call('email_send', {...}) | Same scope checks as server-side calls |
| Call your own HTTP routes from the iframe | bridge.callRoute('GET', '/inbox') | Proxied through the platform with auth + audit |
| Inherit the tenant’s theme | Auto-applied via --lw-bg, --lw-text, etc. | Light/dark switches forwarded automatically |
| Resize the iframe to fit content | bridge.resize(height) | Clamped 80–8000px |
| Update the URL bar for deep-links | bridge.navigate('/settings') | Hash-based, doesn’t trigger a Next route |
| Per-(tenant, app) key-value store | bridge.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.
Storage
Section titled “Storage”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— ax_my_app_stateJSONB 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.
Calling third-party APIs
Section titled “Calling third-party APIs”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.
App identity
Section titled “App identity”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.
Office Assistant–shaped apps
Section titled “Office Assistant–shaped apps”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:
| Piece | Where it lives |
|---|---|
| Custom tools | tools: + app.tool('name', …) |
| Orchestrator wiring | workflows: (guide) |
| Specialist agent | agents: (manifest reference) |
| Dashboard UI | Frontend bundle at /apps/<slug> (guide) |
| Built-in chat / settings / files tabs | chrome: (guide) |
| HTTP routes | http_routes: + @app.http_route (SDK reference) |
| Install-time consent | install_settings: (guide) |
| Cross-app collaboration | ctx.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.