Python SDK reference
from linkworld_sdk import ( App, Context, InboundEnvelope, MockSecrets, MockTools, ToolCallError, load_manifest, load_manifest_from_string,)app = App.from_manifest("linkworld.app.yaml")Decorators
Section titled “Decorators”@app.tool("classify", description="...", scopes_required=["mail.send"])async def classify(ctx: Context, text: str) -> dict: return {"category": "tax"}
@app.on_installasync def on_install(ctx: Context) -> None: ...
@app.on_uninstallasync def on_uninstall(ctx: Context) -> None: ...
@app.on_inboundasync def on_inbound(ctx: Context, env: InboundEnvelope) -> None: ...
@app.on_user_addedasync def on_user_added(ctx: Context, user: dict) -> None: ...
@app.on_schedule("daily-summary") # name from manifest.lifecycle.schedulesasync def daily(ctx: Context) -> None: ...Registration rules:
- Handler must be
async def. Sync handlers raiseTypeErrorat registration. scopes_requiredmust be a subset ofmanifest.required_scopes. Mismatch raisesValueError.@app.on_Xrequiresmanifest.lifecycle.X = true. Mismatch raisesValueError.- Schedule names must match
manifest.lifecycle.schedules[].name. - Double-registration of the same hook raises
ValueError.
app.run()
Section titled “app.run()”app.run() # production: HTTP server on manifest.runtime.portapp.run(local=True) # local: same shape + Mock backendsProduction mode reads LINKWORLD_MCP_URL + LINKWORLD_MCP_TOKEN from
container env (the platform injects them at provision time). Local
mode swaps in MockTools + MockSecrets and exposes /__mock/tool
and /__mock/secret endpoints for direct curl.
Context
Section titled “Context”Request-scoped helper passed to every handler.
ctx.tenant_id # strctx.app_id # str — your slugctx.event_type # 'tool' | 'on_inbound' | …ctx.tools # ToolsApictx.secrets # SecretsApictx.user_id # Optional[str]ctx.tools.call(name, **kwargs)
Section titled “ctx.tools.call(name, **kwargs)”result = await ctx.tools.call( "email_send", subject="Hello", body="…",)Returns the platform’s result dict. Raises ToolCallError on:
decision | Meaning |
|---|---|
scope_denied | Your app or the tenant didn’t grant the required scope. exc.needed_scopes lists which |
network_error | Couldn’t reach the platform (rare; container is in the same VM) |
platform_error | 5xx from the platform — transient |
bad_response | Platform returned non-JSON |
denied | Generic denial (rate-limit, gate decision, …) |
ctx.secrets.get(key)
Section titled “ctx.secrets.get(key)”api_key = await ctx.secrets.get("OPENAI_KEY")if api_key is None: raise RuntimeError("Tenant did not configure OPENAI_KEY")Returns str | None. Never raises on auth/network failure — fail-closed.
Key must match ^[A-Z][A-Z0-9_]{0,63}$ (validated SDK-side).
ToolCallError
Section titled “ToolCallError”class ToolCallError(Exception): decision: str needed_scopes: list[str] tool_name: strMockTools / MockSecrets
Section titled “MockTools / MockSecrets”For local dev:
tools = MockTools()tools.register("email_send", lambda **kwargs: {"sent": True})
secrets = MockSecrets({"OPENAI_KEY": "sk-test"})You don’t normally instantiate these directly — app.run(local=True)
attaches them automatically. The /__mock/tool and /__mock/secret
HTTP endpoints in local mode are sugar over the same classes.
InboundEnvelope
Section titled “InboundEnvelope”@dataclassclass InboundEnvelope: message_id: str channel: str # 'email' | 'whatsapp' | 'webhook' | … from_: str # alias for `from` (Python keyword) body: str subject: Optional[str] received_at: str # ISO-8601 UTC attachment_ids: list[str] metadata: dictManifest helpers
Section titled “Manifest helpers”load_manifest("linkworld.app.yaml") # path → Manifest, or ManifestErrorload_manifest_from_string("apiVersion: …") # string → Manifest, or ManifestErrorBoth raise ManifestError (a wrapper around Pydantic’s ValidationError)
on parse or schema failure.