Complete API surface
Every SDK element on a single scrollable page — decorators, ctx, manifest, validators, action types, errors, invariants
This is the single-page exhaustive reference of every public element in the current imperal-sdk line. Use Cmd-F to find anything fast. Each element links to its dedicated deep-dive page. Version-pinned history lives in the changelog.
Quick index
Extension class
The top-level declaration.
@chat.function
LLM-callable handler.
@ext.skeleton
Live data feed.
@ext.panel
UI panel surfaces.
@ext.schedule
Cron job.
@ext.webhook
Webhook receiver.
@ext.on_event / @ext.emits
Event bus.
@ext.cache_model
Short-lived typed cache.
@ext.expose
Inter-extension IPC.
@ext.tray
System tray item.
Lifecycle hooks
on_install / on_upgrade / …
ctx attributes
user, store, ai, http, notify, cache, …
Manifest fields
imperal.json complete schema.
Federal validators
V14-V22 + V24 + V31 (11 ERROR-severity).
@ext.secret
Per-user encrypted credentials (v4.2.2+).
ctx.secrets
Read/write user secrets in handlers.
ui.Password
Browser-blind credential-entry input (v4.2.6+).
ctx.webhook_url
Build the canonical public OAuth/webhook URL for this extension (v4.2.7+).
Action types
read / write / destructive.
Allowed panel slots
Federal allowlist of UI slots.
[ActionResult](/en/reference/glossary/)
Return type for all chat functions.
Error codes
Stable codes returned to clients.
Federal invariants (selected)
The most-cited I-* contracts.
Extension class
The top-level declaration in every extension. First positional argument is app_id.
from imperal_sdk import Extension
ext = Extension(
"my-app", # app_id — positional
version="1.0.0",
display_name="My App",
description="A longer description — at least 40 characters (V14).",
icon="icon.svg",
actions_explicit=True,
capabilities=["my-app:read", "my-app:write"],
)Prop
Type
Concept page → · Federal contract →
@chat.function
The most-used decorator. Registers an LLM-callable tool on a ChatExtension instance.
from imperal_sdk import ChatExtension
from pydantic import BaseModel, Field
chat = ChatExtension(ext, tool_name="my-app", description="My App assistant")
class SendEmailParams(BaseModel):
to: str = Field(description="Recipient email address")
subject: str = Field(description="Email subject line")
body: str = Field(description="Email body text")
@chat.function(
description="Send an email message via the user's connected mail account.",
action_type="write",
chain_callable=True,
effects=["email.send"],
event="email.sent",
id_projection=None,
)
async def send_email(ctx, params: SendEmailParams):
# params is auto-validated Pydantic model
return ActionResult.success({"sent": True}, summary="Email sent")Prop
Type
Pydantic params auto-detection: if your handler has a typed parameter whose type is a BaseModel subclass, the SDK auto-detects it and emits its JSON schema in the manifest. Define the model at module scope (V17 — not inside the function).
Concept page → · Decorators reference →
@ext.skeleton
Registers a live data probe. Called by the web-kernel on a TTL schedule; result stored as LLM context for the intent classifier.
@ext.skeleton("tasks_summary", alert=True, ttl=300)
async def refresh_tasks_skeleton(ctx) -> dict:
total = await ctx.store.count("tasks")
pending = await ctx.store.count("tasks", where={"status": "pending"})
return {"response": {
"total": total,
"pending": pending,
}}Prop
Type
Return contract: must return {"response": {...}} where the dict contains scalar fields (counts, flags, short strings). The web-kernel's classifier reads the values directly.
Access guard: ctx.skeleton.get() raises SkeletonAccessForbidden outside @ext.skeleton context. Regular tools and panels must use ctx.store or ctx.cache instead.
@ext.panel
Registers a UI panel handler. Returns a UINode tree built with ui.* primitives.
@ext.panel(
"sidebar",
slot="left",
title="My Sidebar",
icon="📜",
refresh="on_event:my-ext.item.created,my-ext.item.updated",
default_width=280,
min_width=200,
max_width=500,
)
async def sidebar(ctx, **params):
items = await ctx.store.query("items")
if not items.data:
return ui.Empty(message="No items yet.")
return ui.List(items=[ui.Text(i.data["label"]) for i in items.data])Prop
Type
Allowed panel slots
Prop
Type
"overlay", "bottom", and "chat-sidebar" will not raise at build time but the current Imperal Panel frontend has no code path to render them. Use slot="center" for center-overlay behavior. The "main" slot was removed in SDK 3.4.0 — use "center".
@ext.schedule
Registers a cron-driven background job. The web-kernel runs it per the cron expression; the handler receives a system context (ctx.user.imperal_id == "__system__").
@ext.schedule("nightly_archive", cron="0 3 * * *")
async def nightly_archive(ctx):
# Fan-out pattern: iterate all installed users
async for user_id in ctx.store.list_users():
user_ctx = ctx.as_user(user_id)
# ... do per-user workProp
Type
System context only. ctx.store.list_users() and ctx.as_user() are only available when ctx.user.imperal_id == "__system__". Regular tool contexts will raise RuntimeError.
@ext.webhook
Registers a webhook receiver endpoint.
@ext.webhook("/incoming", method="POST", secret_header="X-Webhook-Secret")
async def receive_webhook(ctx, headers: dict, body: str, query_params: dict):
# HMAC verification must be done manually — no SDK helper
sig = headers.get("X-Webhook-Secret", "")
# ... verify and process
return {"ok": True}Prop
Type
There is no ctx.verify_webhook_secret() helper. The secret_header param only declares the header name. Your handler must implement HMAC verification manually using hmac.compare_digest().
@ext.on_event / @ext.emits
Subscribe to or declare platform events.
# Subscribe
@ext.on_event("email.received")
async def handle_email(ctx, event):
# ctx.http.* is NOT available here — minimal context only
# ctx.store and ctx.notify are available
await ctx.notify(f"New email from {event.data.get('from')}")
# Declare emission (must prefix with own app_id)
@ext.emits("my-app.item.created")
async def _declare_item_created(): ...Prop
Type
@ext.on_event handlers receive a minimal context. Calling ctx.* methods (http, store, etc.) inside an event handler raises AttributeError. Only ctx.store, ctx.notify, and ctx.ai are reliably available.
@ext.cache_model
Registers a Pydantic model as a valid value shape for ctx.cache.
from pydantic import BaseModel
class InboxSummary(BaseModel):
unread: int
latest_subject: str
@ext.cache_model("inbox_summary")
class _InboxSummary(InboxSummary):
pass
# Then in a handler:
cached = await ctx.cache.get("inbox", InboxSummary)
if cached is None:
fresh = await fetch_inbox_summary(ctx)
await ctx.cache.set("inbox", fresh, ttl_seconds=60)The model class must be a BaseModel subclass. Duplicate names within the same extension raise ValueError.
@ext.expose
Exposes a method for inter-extension calls via ctx.extensions.call().
@ext.expose("get_summary", action_type="read")
async def get_summary(ctx, **kwargs):
return {"summary": "..."}@ext.expose is in the SDK and emits to the manifest but has zero production usage. The Auth GW routing mechanism and cross-tenant IPC limitations are undocumented. Mark as experimental.
@ext.tray
Declares a system tray item. Handler returns a UINode tree with badge and optional dropdown.
@ext.tray("unread", icon="📧", tooltip="Unread messages")
async def tray_unread(ctx, **kwargs):
count = await ctx.store.count("messages", where={"read": False})
return ui.Stack([
ui.Badge(str(count), color="red" if count > 0 else "gray"),
])Prop
Type
@ext.tray is experimental with no production usage. Behavior in the Imperal Panel frontend is not verified.
@ext.secret
Declares a per-user encrypted credential the extension needs. Federal EXT-SECRETS-V1 (SDK v4.2.2+). Plaintext is encrypted by the platform KMS (AES-256-GCM, non-exportable key), stored as ciphertext in the encrypted-secrets store, and is never present in logs, journals, error responses, chat history, workflow event history, or backups.
ext.secret(
name="openai_api_key",
description="Your OpenAI API key (sk-proj-...).",
required=True,
write_mode="user", # 'user' | 'extension' | 'both'
max_bytes=200,
rotation_hint_days=90,
)(lambda: None)Prop
Type
Auto-registered Secrets panel (v4.2.4+): every Extension instance unconditionally registers a synthetic secrets panel in __init__ (slot right, title Secrets, icon KeyRound). Users land there to manage credentials without the author writing any panel code. The synthetic tool name is __panel__secrets and is excluded from validator tool_count (v4.2.5+).
Lifecycle hooks
Lifecycle hooks are registered as decorators directly on the Extension instance (no arguments for most).
@ext.on_install
async def on_install(ctx):
# ctx.user is the installing user
await ctx.store.create("settings", {"theme": "dark"})
@ext.on_upgrade("2.0.0")
async def on_upgrade_2_0_0(ctx):
# Runs when user upgrades from any version to 2.0.0
await ctx.store.update("settings", "main", {"migrated": True})
@ext.on_uninstall
async def on_uninstall(ctx):
pass # cleanup
@ext.on_enable
async def on_enable(ctx):
pass
@ext.on_disable
async def on_disable(ctx):
pass
@ext.health_check
async def health_check(ctx):
# Called every 60s by web-kernel
return HealthStatus(healthy=True)| Hook | Signature | When called |
|---|---|---|
@ext.on_install | (ctx) | First install by a user |
@ext.on_upgrade(version) | (ctx) | User upgrades to that semver |
@ext.on_uninstall | (ctx) | User uninstalls |
@ext.on_enable | (ctx) | Admin enables for user/tenant |
@ext.on_disable | (ctx) | Admin disables |
@ext.health_check | (ctx) | Every 60s by web-kernel |
ctx (Context)
The single object every handler receives. It is a dataclass built by the web-kernel after auth + tenant + scope checks pass.
async def my_handler(ctx, params):
ctx.user.imperal_id # str — canonical user ID (W1 canonical)
ctx.user.email # str | None
ctx.user.tenant_id # str
ctx.user.role # str
ctx.user.scopes # list[str]
ctx.user.attributes # dict — arbitrary admin-set attributes
ctx.tenant # TenantContext | None
ctx.store # StoreProtocol | None — per-user document store
ctx.db # DBProtocol | None — raw SQL access
ctx.ai # AIProtocol | None — LLM completion
ctx.skeleton # _SkeletonAccessGuard — only in @ext.skeleton context
ctx.billing # BillingProtocol | None
ctx.notify # NotifyProtocol | None — push/in-app notifications
ctx.storage # StorageProtocol | None — file storage
ctx.http # HTTPProtocol | None — outbound HTTP
ctx.tools # ToolsProtocol | None — cross-tool discovery/call
ctx.config # ConfigProtocol | None — extension config
ctx.extensions # ExtensionsProtocol | None — IPC + event emit
ctx.cache # CacheClient (property) — short-lived Pydantic cache
ctx.time # TimeContext — timezone, utc_offset, now_utc, now_local
ctx.agency_id # str | None
ctx.agency_theme # dict | NoneProp
Type
ctx.lang does not exist
There is no ctx.lang attribute. Language detection is handled at the web-kernel classifier layer. If your extension needs the user's language, read it from ctx.user.attributes or from skeleton data.
Always key state by ctx.user.imperal_id
Never key by display_name, email, or any other field. imperal_id is the federal canonical identifier. Multi-tenant safety depends on this.
ctx.store methods
doc = await ctx.store.create("collection", {"key": "value"})
doc = await ctx.store.get("collection", doc_id)
page = await ctx.store.query("collection", where={"status": "active"}, limit=100)
doc = await ctx.store.update("collection", doc_id, {"key": "new_value"})
ok = await ctx.store.delete("collection", doc_id)
count = await ctx.store.count("collection", where={"status": "active"})
# System context only:
async for user_id in ctx.store.list_users(): ...
page = await ctx.store.query_all("collection", where={...})ctx.http methods
resp = await ctx.http.get("https://api.example.com/data")
resp = await ctx.http.post("https://api.example.com/items", json={...})
resp = await ctx.http.put("https://api.example.com/items/1", json={...})
resp = await ctx.http.patch("https://api.example.com/items/1", json={...})
resp = await ctx.http.delete("https://api.example.com/items/1")
# resp.status_code, resp.json(), resp.text, resp.headers
# Per-call timeout override (v4.2.12+). Default 30s; federal cap 180s.
resp = await ctx.http.post("https://api.example.com/v1/...",
json={...}, timeout=120)Federal cap 180s — I-LONGRUN-HTTP-CAP-180S
The timeout= kwarg is capped at 180 seconds. Anything larger raises ValueError("ctx.http timeout {N}s exceeds federal cap (180s)..."). For ops that legitimately exceed 3 minutes, use ctx.background_task() instead.
The HTTP client on ctx is ctx.http, not ctx.api. There is no ctx.api attribute. Earlier docs incorrectly used ctx.api — this was a fabrication.
ctx.background_task() — LONGRUN-V1 (SDK v4.2.12+)
# Spawn a coroutine in the background; result auto-delivered to chat
# when the coroutine completes.
task_id: str = await ctx.background_task(
coro=_my_long_op(),
long_running=False, # False → 180s cap, True → 1800s cap
name="AI refinement", # human-readable for UI/audit
)
return ActionResult.success(
summary="Got it — running in background. I'll send the result here.",
data={"task_id": task_id},
)The coroutine MUST return ActionResult — federal I-LONGRUN-BG-CORO-RETURNS-ACTIONRESULT. Returning anything else triggers a critical audit row + delivers a fallback error to chat. The user sees two chat turns:
- Immediate: the caller's
ActionResult.success(summary="Got it — running…")ack. - Later (when coro finishes): the coro's
ActionResultrendered as a fresh bot message.
Use this when a single chat handler would otherwise take longer than the 180-second ctx.http cap, when you want chat to stay unblocked while work runs, or when the result delivery time is variable.
ctx.deliver_chat_message() — LONGRUN-V1 (SDK v4.2.12+)
# Inject a bot message into the user's chat at any time.
# Mirrors the web-kernel's auto-promote chat-injection path, but you call it.
await ctx.deliver_chat_message(
"Spotify connected! 🎵 You can now ask what's playing or search your library.",
msg_type="response", # "response" | "system" | "tool_result"
refresh_panels=["library"], # optional — panel_ids to re-render after
)Common use cases:
- OAuth callback acknowledgement — webhook handler tells the user the link succeeded.
- Multi-stage background announcements — background work emits intermediate progress as separate bot messages before the final result.
- One-off proactive notifications that don't fit the periodic skeleton-alert pattern.
Text truncated to 64KB with …(truncated) marker if larger. Federal: I-LONGRUN-CHAT-INJECT-USER-SCOPED (cross-user inject returns 403) + I-LONGRUN-CHAT-INJECT-AUDIT-EVERY (every inject writes an audit row).
ctx.cache methods
value = await ctx.cache.get("key", MyModel) # returns MyModel | None
await ctx.cache.set("key", value, ttl_seconds=60) # TTL: 5-300s
await ctx.cache.delete("key")
value = await ctx.cache.get_or_fetch("key", MyModel, fetcher, ttl_seconds=60)Key format: ^[A-Za-z0-9_\-:]+$, max 128 chars.
ctx.as_user(user_id) — system context only
# Only works when ctx.user.imperal_id == "__system__"
user_ctx = ctx.as_user("imp_u_tE-J9c_NxX")Returns a new Context scoped to the given user. Used in @ext.schedule handlers for per-user fan-out.
ctx.progress(percent, message="")
await ctx.progress(50.0, "Halfway done...")
# May raise TaskCancelled if user cancelledctx.secrets methods — EXT-SECRETS-V1 (SDK v4.2.2+)
# Read plaintext for one declared secret. Returns None if declared but unset.
api_key: str | None = await ctx.secrets.get("spotify_api_key")
# Write plaintext. Allowed only if manifest write_mode is 'extension' or 'both'.
await ctx.secrets.set("spotify_refresh_token", "rt-...")
# Delete (same write_mode rules apply).
was_set: bool = await ctx.secrets.delete("spotify_refresh_token")
# Cheap metadata read — does NOT decrypt, does NOT write an audit row.
is_set: bool = await ctx.secrets.is_set("spotify_api_key")
# List all declared secrets for this extension. Returns SecretStatus dataclasses
# with name, description, is_set, last_accessed_at — NEVER the value.
statuses = await ctx.secrets.list()Federal contract (full spec in @ext.secret reference):
ctx.secrets.get/set/delete/is_seton a name not in your manifest'ssecrets[]raisesSecretNotDeclaredError— manifest is single source of truth (I-SECRETS-CONTRACT-DECLARED).- Plaintext exists only inside the handler call stack. SDK has no module-level cache, no class-level cache, no
@lru_cache. Storing plaintext in instance/module state violatesI-SECRETS-HANDLER-SCOPE-MEMORY. - Platform KMS endpoint unavailable raises
SecretVaultUnavailable— fail-closed (I-SECRETS-VAULT-DEPENDENCY). - Setting a secret whose
write_mode='user'raisesSecretWriteForbidden. Only Panel UI (user-attributable session) can writeuser-mode secrets.
Dev mode (IMPERAL_DEV_MODE=true):
get(name)readsIMPERAL_SECRET_<UPPER_NAME>env var (or returns None).set/deleteare no-ops with WARN log.- Manifest contract still enforced — undeclared names still raise.
pytest fixture pattern:
import pytest
from imperal_sdk.testing import MockSecretStore
@pytest.fixture
def secrets():
return MockSecretStore({"spotify_api_key": "sk-test"})
async def test_search(ctx_factory, secrets):
ctx = ctx_factory(secrets=secrets)
...Panel UI write paths must use ui.Password (v4.2.6+) — <input type="password" autocomplete="new-password" spellcheck="false"> masks the value while the user types and suppresses browser autofill. The plaintext still travels in the POST body to the platform secrets endpoint, which is the only correctness boundary. See ui.Password.
ctx.config is NOT for credentials. Use ctx.config for non-sensitive settings (default timezone, feature flags, display preferences). Credentials — API keys, OAuth tokens, webhook signing secrets — MUST flow through ctx.secrets. ctx.config values are stored plaintext in the user store and visible to the Panel; only ctx.secrets is Vault-encrypted, audit-chokepoint-routed, and federal-grade.
ctx.webhook_url — SDK v4.2.7+
Build the canonical public OAuth/webhook callback URL for this extension.
url = ctx.webhook_url("/callback")
# → "https://panel.imperal.io/v1/ext/spotify/webhook/callback"Leading slash optional. Multi-segment paths supported.
Returns https://{IMPERAL_PUBLIC_HOST}/v1/ext/{web-kernel-authoritative-app_id}/webhook/{path}. Default host panel.imperal.io. The {app_id} component is the folder/manifest name (the value auth-gw and Vault know about), not the Python Extension("X", ...) value — if those drift, this helper still returns the correct deployed URL.
Use it instead of hardcoding SP_REDIRECT_URI = "https://..." in *_config.py — hardcoded URLs are the #1 cause of OAuth-callback drift bugs (auth-gw 401 / provider redirect URI mismatch). See @ext.webhook reference.
ActionResult
The return type for all @chat.function handlers.
from imperal_sdk import ActionResult
# Success
return ActionResult.success({"note_id": "123"}, summary="Note created")
return ActionResult.success(data, ui=ui.Stack([...]))
return ActionResult.success(data, refresh_panels=["sidebar", "editor"])
# Error
return ActionResult.error("Something went wrong", retryable=True)
return ActionResult.error("Permanent failure", retryable=False)Prop
Type
Never expose raw exception strings in ActionResult.error(). The web-kernel's Magic UX layer (I-MAGIC-UX-1/2) formats errors via i18n templates — your error message should be user-facing prose, not str(e) or Pydantic class names.
Manifest fields (imperal.json)
Auto-generated by imperal build ./extension. Do NOT hand-edit.
{
"manifest_schema_version": 3,
"app_id": "my-app",
"version": "1.0.0",
"name": "My App",
"description": "A longer description — at least 40 characters.",
"icon": "icon.svg",
"actions_explicit": true,
"capabilities": ["my-app:read", "my-app:write"],
"tools": [...],
"signals": [...],
"schedules": [...],
"required_scopes": [...],
"webhooks": [...],
"events": {"subscribes": [...], "emits": [...]},
"exposed": [...],
"lifecycle": {...},
"lifecycle_hooks": {...},
"tray": [...]
}Prop
Type
tools[] (FunctionDef) — typed chat.function entries
Prop
Type
Federal validators
All ERROR severity. Failing any blocks publishing via Developer Portal. Run with imperal validate ./extension.
Prop
Type
V23 was dropped in v4.0.1 as redundant.
Validators reference → · Federal contract →
Action types
Prop
Type
Error codes
Prop
Type
Federal invariants (selected)
Prop
Type
Federal invariants concept page →
CLI commands
imperal validate ./extension # run V14-V22+V24+V31
imperal build ./extension # generate imperal.json
python3 -m py_compile app.py # syntax check before deployAlso available via Python:
from imperal_sdk import generate_manifest, save_manifest, validate_extension
manifest = generate_manifest(ext)
save_manifest(ext, ".")
report = validate_extension(ext)Compatibility matrix
Prop
Type
Where to dig deeper
Federal contract
The complete contract every published extension satisfies.
Manifest reference
Every imperal.json field.
Decorators reference
Every decorator with full API.
Validators reference
V14-V22+V24+V31 with fixes.
[Pydantic feedback loop](/en/reference/glossary/)
The runtime retry layer.
Changelog
Release notes for the v4.x line.