Imperal Docs
SDK Reference

Decorators reference

Every SDK decorator and what it accepts

A reference for every decorator the SDK ships. Each one registers a different surface to the web-kernel. All signatures track the latest imperal-sdk source — for the version-pinned release history see the changelog.

@chat.function

The most-used decorator. Registered on a ChatExtension instance (not directly on Extension).

from imperal_sdk import Extension, ChatExtension, ActionResult
from pydantic import BaseModel, Field

ext = Extension("my-app", display_name="My App", description="...", icon="icon.svg", actions_explicit=True)
chat = ChatExtension(ext, tool_name="my-app", description="My App assistant")

class CreateTaskParams(BaseModel):
    title: str = Field(description="Task title — what needs to be done")
    due_at: str | None = Field(None, description="Optional ISO datetime when the task is due")

@chat.function(
    description="Create a new task for the user.",
    action_type="write",
    chain_callable=True,
    effects=["task.create"],
    event="task.created",
    id_projection=None,
)
async def create_task(ctx, params: CreateTaskParams):
    doc = await ctx.store.create("tasks", {"title": params.title})
    return ActionResult.success({"task_id": doc.id}, summary=f"Created task: {params.title}")

Prop

Type

Pydantic params: define the model at module scope (not inside the function) — V17 enforces this. The SDK auto-detects a BaseModel typed parameter and emits its JSON schema in the manifest.

Return type: always return ActionResult.success(data, summary) or ActionResult.error(msg, retryable). Never return bare dicts from destructive/write tools.

Full chat functions concept →


@ext.skeleton

Registers a live data probe. The web-kernel calls it on a TTL schedule; the result is 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: {"response": {...}} where the dict contains flat scalar values. The intent classifier reads them directly.

Access guard: ctx.skeleton.get() raises SkeletonAccessForbidden outside @ext.skeleton context (invariant I-SKELETON-LLM-ONLY). Regular tools and panels must use ctx.store or ctx.cache.

Concept page →


@ext.panel

Registers a UI panel handler. The web-kernel exposes your handler as a callable endpoint (__panel__{panel_id}) that the Imperal Panel frontend fetches on load and on explicit re-fetch. Returns a UINode tree built with ui.* primitives.

from imperal_sdk import Extension, ui

ext = Extension("my-app", ...)

@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

Slot allowlist

SlotDescriptionFrontend status
"center"Default. Middle content area.Active
"left"Left sidebar. Resizable.Active
"right"Right sidebar. Resizable.Active
"overlay"Accepted by SDK — no frontend render path.Frontend-dead
"bottom"Accepted by SDK — no frontend render path.Frontend-dead
"chat-sidebar"Accepted by SDK — no frontend render path.Frontend-dead

"overlay", "bottom", and "chat-sidebar" will not raise at build time but the current Imperal Panel frontend has no code path to render them — panels with these slots will silently never be fetched. The "main" slot was removed in SDK 3.4.0 — use "center".

For slot="center" to render, you must also pass center_overlay=True (federal v4.1.8+) and trigger the panel via auto_action from your sidebar's root UINode. See concepts/panels.mdx § How slot="center" actually activates for the full wire-up and the I-PANEL-RENDERING-CONTRACT federal contract.

Refresh modes

ValueBehavior
"manual" (default)No automatic re-fetch. Panel fetched once on load, then only on ActionResult.refresh_panels.
"on_event:scope.action[,...]"SSE subscriber re-fetches when any listed event arrives.
"interval:Ns"Frontend interval polling (must be injected manually via admin API — web-kernel publisher ignores it).

refresh_seconds=N does not exist as a named parameter on @ext.panel. Passing it lands in **kwargs and is silently ignored by the web-kernel publisher. Use refresh="on_event:..." or refresh="interval:Ns".

Handler signature

@ext.panel("detail", slot="right", title="Detail", default_width=320)
async def detail(ctx, **params):
    item_id: str = params.get("item_id", "")
    if not item_id:
        return ui.Empty(message="Select an item.")
    doc = await ctx.store.get("items", item_id)
    return ui.Stack(direction="v", children=[
        ui.Text(doc.data["title"]),
    ])

Returns: any ui.* primitive. Returning None is valid Python but use ui.Empty(message="...") so the user sees a clear message.

See also


@ext.schedule

Registers a cron-driven background job. Handler runs in system context.

@ext.schedule("nightly_archive", cron="0 3 * * *")
async def nightly_archive(ctx):
    async for user_id in ctx.store.list_users():
        user_ctx = ctx.as_user(user_id)
        await process_user(user_ctx)

Prop

Type

System context: the handler receives a system context (ctx.user.imperal_id == "__system__"). ctx.store.list_users() and ctx.as_user() are only available in system context.


@ext.webhook

Registers a webhook receiver. Auth Gateway routes incoming POST to your handler.

import hmac, hashlib

@ext.webhook("/incoming", method="POST", secret_header="X-Hub-Signature-256")
async def receive_webhook(ctx, headers: dict, body: str, query_params: dict):
    # Verify HMAC manually — no SDK helper
    sig = headers.get("X-Hub-Signature-256", "")
    secret = ctx.config.require("webhook_secret")
    expected = "sha256=" + hmac.new(
        secret.encode(), body.encode(), hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(sig, expected):
        return {"error": "Invalid signature"}
    payload = json.loads(body)
    # process...
    return {"ok": True}

Prop

Type

Handler receives: ctx (minimal, ctx.user.imperal_id == "__webhook__"), headers: dict, body: str (raw request body), query_params: dict.

There is no ctx.verify_webhook_secret() helper. secret_header only names the header. Your handler must call hmac.compare_digest() manually.


@ext.on_event

Subscribes to a platform event. Handler called when the event fires.

@ext.on_event("email.received")
async def handle_email_received(ctx, event):
    subject = event.data.get("subject", "")
    await ctx.notify(f"New email: {subject}")

Prop

Type

Handler receives: ctx (minimal context), event (EventModel with .type, .data).

Event handlers receive a minimal context. ctx.http / ctx.ai / ctx.db may raise AttributeError. Only ctx.store, ctx.notify, and basic identity attributes are reliably available.


@ext.emits

Declares an event this extension publishes. Powers the manifest events.emits[] section.

@ext.emits("my-app.item.created", schema_ref="ItemCreatedEvent")
async def _declare_item_created(): ...

@ext.emits("my-app.item.deleted")
async def _declare_item_deleted(): ...

Prop

Type

The decorated function is a dummy placeholder — the decorator only registers the declaration, it does not call the function.

Actually emit events at runtime via ctx.extensions.emit("my-app.item.created", {"id": "..."})


@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

Prop

Type

Constraint: cls must be a pydantic.BaseModel subclass (raises TypeError). Duplicate names raise ValueError. The model is scoped to the extension instance.


@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": "..."}

Prop

Type

@ext.expose is experimental. Zero production usage. Auth GW routing mechanism and cross-tenant IPC limitations are not fully documented.


@ext.tray

Declares a system tray item. Handler returns a UINode tree.

@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.


@ext.secret

Declarative per-user encrypted credential declaration (SDK v4.2.2+). Federal EXT-SECRETS-V1 API — extension declares a secret it needs (API key, OAuth refresh token, webhook signing secret) and reads the plaintext via ctx.secrets.get(). Storage is AES-256-GCM ciphertext via the platform KMS (non-exportable key); plaintext never lands in the audit ledger, logs, or backups.

ext.secret(
    name="spotify_api_key",
    description="Your Spotify API key (from developer.spotify.com).",
    required=True,
    write_mode="user",
    max_bytes=200,
)(lambda: None)

@chat.function
async def search(ctx, query: str):
    api_key = await ctx.secrets.get("spotify_api_key")
    # Use api_key for the duration of this handler call only.
    ...

Prop

Type

Returns an identity decorator — the call itself registers the SecretSpec. See full @ext.secret reference for usage patterns, dev-mode, pytest fixtures, and the seven federal invariants enforced by this surface.


@ext.tool

Low-level tool registration. Prefer @chat.function for LLM-callable tools. Use @ext.tool for platform-internal tools (e.g., the skeleton alert companion).

@ext.tool("skeleton_alert_tasks", scopes=[], description="Alert when tasks change.")
async def skeleton_alert_tasks(ctx):
    return {"changed": True}

Prop

Type


Lifecycle hooks

Applied directly as decorators on the Extension instance.

@ext.on_install
async def on_install(ctx):
    await ctx.store.create("settings", {"theme": "dark"})

@ext.on_upgrade("2.0.0")
async def upgrade_to_2_0(ctx):
    await ctx.store.update("settings", "main", {"schema_v": 2})

@ext.on_uninstall
async def on_uninstall(ctx): ...

@ext.on_enable
async def on_enable(ctx): ...

@ext.on_disable
async def on_disable(ctx): ...

@ext.health_check
async def health_check(ctx):
    return HealthStatus(healthy=True)
DecoratorSignatureWhen
@ext.on_install(ctx)First install
@ext.on_upgrade(version)(version)User upgrades to that semver
@ext.on_uninstall(ctx)User uninstalls
@ext.on_enable(ctx)Admin enables
@ext.on_disable(ctx)Admin disables
@ext.health_check(ctx)Every 60s by web-kernel

Decorators that DON'T exist

The following are not in the SDK and were erroneously listed in earlier documentation:

DecoratorReality
@ext.scheduleDoes not exist. Use @ext.schedule (no 'd').
@ext.secretDoes not exist yet. Planned as EXT-SECRETS-V1 (not shipped).
@ext.oauth_providerDoes not exist.
@panels.sidebarDoes not exist. Use @ext.panel(..., slot="left").
@panels.editorDoes not exist. Use @ext.panel(..., slot="center", center_overlay=True).
@panels.settingsDoes not exist.
@panels.actionDoes not exist.
@cacheDoes not exist. Use @ext.cache_model + ctx.cache.
@auth_requiredDoes not exist. Auth always runs before dispatch.
@require_scopeDoes not exist. Scopes declared in manifest, enforced before dispatch.

Where to next

On this page