Imperal Docs
SDK Reference

UI actions reference

UI actions reference — panel actions that trigger handlers and refresh surfaces: ui.Call, ui.Navigate, ui.Send, ui.Open, plus the auto_action prop format.

UI actions are produced by imperal_sdk.ui helper functions and serialized through the UIAction dataclass (imperal_sdk.ui.base.UIAction). They are the four ways a rendered panel dispatches behavior back to the web-kernel or the host application. Actions appear as prop values on interactive UINode components — typically on_click, on_submit, or on_change — and also as the value of the special auto_action prop on a left-panel root node.

All four helpers live at imperal_sdk.ui.actions and are re-exported from imperal_sdk.ui:

from imperal_sdk import ui

action = ui.Call("__panel__editor", note_id="abc")

ui.Call

The most-used action. Triggers a @chat.function or panel handler directly, bypassing the chat pipeline entirely. The web-kernel routes the call based on the function-name prefix.

Signature

from imperal_sdk import ui
from imperal_sdk.ui.base import UIAction


def Call(function: str, **params) -> UIAction:
    return UIAction(action="call", params={"function": function, "params": params})

ui.Call returns a UIAction with action="call". On the wire the dict is:

# ui.Call("__panel__editor", note_id="abc").to_dict() ==
{"action": "call", "function": "__panel__editor", "params": {"note_id": "abc"}}

Function-name conventions

PrefixDispatch pathTypical use
__panel__{panel_id}System-task path — params spread directly to the handler via **kwargsOpen / refresh a panel
__widget__{widget_id}System-task pathRefresh a widget
__webhook__{name}System-task pathInternal webhook bridge
Plain name (no __ prefix)Chat-function pipeline (the typed-args dispatcher)Call a @chat.function directly from a button

Dispatch matrix

Every kwarg is forwarded identically. All keyword arguments you pass to ui.Call are delivered to the handler the same way regardless of their name — there is no declared-vs-undeclared kwarg split. Name-based validation only happens if your own @chat.function declares a strict Pydantic model.

from imperal_sdk import ui

# Both kwargs are forwarded to the handler identically.
on_click_a = ui.Call("__panel__editor", note_id="abc")
on_click_b = ui.Call("__panel__editor", active_view="plan")

The only place where declared vs undeclared matters is inside your handler: if you annotate a @chat.function with a strict Pydantic model, unknown keys raise a ValidationError. That is extension-authored policy, not a host contract.

Examples

Panel open from a list item:

from imperal_sdk import Extension, ui

ext = Extension(app_id="my-ext")


@ext.panel("sidebar", slot="left", title="Items")
async def sidebar(ctx, **params):  # type: ignore[attr-defined]
    items = [{"id": "1", "title": "First item"}, {"id": "2", "title": "Second item"}]
    return ui.List(
        items=[
            ui.ListItem(
                id=item["id"],
                title=item["title"],
                on_click=ui.Call("__panel__detail", item_id=item["id"]),
            )
            for item in items
        ]
    )

Calling a @chat.function from a button:

from imperal_sdk import Extension, ui, ActionResult

ext = Extension(app_id="my-ext")


@ext.panel("editor", slot="center", title="Editor", center_overlay=True)
async def editor(ctx, **params):  # type: ignore[attr-defined]
    item_id: str = params.get("item_id", "")
    return ui.Stack(
        direction="v",
        children=[
            ui.Text(f"Editing: {item_id}"),
            ui.Button(
                label="Archive",
                on_click=ui.Call("archive_item", item_id=item_id),
            ),
        ],
    )

ui.Navigate

Client-side navigation. When the host receives a Navigate action it pushes path onto its router — equivalent to the user clicking a link.

Signature

from imperal_sdk import ui
from imperal_sdk.ui.base import UIAction


def Navigate(path: str) -> UIAction:
    return UIAction(action="navigate", params={"path": path})

Wire shape: {"action": "navigate", "path": "/settings"}.

When to use

Use for "after save, jump to a different page" or "take the user to another extension's URL". Navigate does not open a panel — it replaces the host's current route.

Example

from imperal_sdk import Extension, ui, ActionResult

ext = Extension(app_id="my-ext")


@ext.panel("onboarding", slot="center", title="Welcome", center_overlay=True)
async def onboarding(ctx, **params):  # type: ignore[attr-defined]
    return ui.Stack(
        direction="v",
        children=[
            ui.Header("Get started"),
            ui.Button(
                label="Go to settings",
                on_click=ui.Navigate("/settings"),
            ),
        ],
    )

ui.Send

Sends a pre-formed message into chat as if the user typed it. Useful for quick-action buttons that trigger a chat interaction without leaving the panel.

Signature

from imperal_sdk import ui
from imperal_sdk.ui.base import UIAction


def Send(message: str) -> UIAction:
    return UIAction(action="send", params={"message": message})

Wire shape: {"action": "send", "message": "Summarise this folder"}.

When to use

Use when a panel button should kick off a chat task — for example, a "Summarise" button on a folder view that sends a ready-made query. The message is injected into the chat input and submitted exactly as if the user had typed it.

Example

from imperal_sdk import Extension, ui

ext = Extension(app_id="my-ext")


@ext.panel("folder", slot="left", title="Folder")
async def folder(ctx, **params):  # type: ignore[attr-defined]
    folder_id: str = params.get("folder_id", "")
    return ui.Stack(
        direction="v",
        children=[
            ui.Header("Folder actions"),
            ui.Button(
                label="Summarise contents",
                on_click=ui.Send(f"Summarise folder {folder_id}"),
            ),
        ],
    )

ui.Open

Opens a URL in a new browser tab. Use sparingly — it takes the user away from the panel UI to an external page.

Signature

from imperal_sdk import ui
from imperal_sdk.ui.base import UIAction


def Open(url: str) -> UIAction:
    return UIAction(action="open", params={"url": url})

Wire shape: {"action": "open", "url": "https://example.com"}.

When to use

Use for "view source", "open in external app", or "download link" patterns where the content lives outside the Imperal Panel. Prefer ui.Navigate for in-app routes.

Example

from imperal_sdk import Extension, ui

ext = Extension(app_id="my-ext")


@ext.panel("detail", slot="right", title="Detail")
async def detail(ctx, **params):  # type: ignore[attr-defined]
    doc_url: str = params.get("doc_url", "")
    return ui.Stack(
        direction="v",
        children=[
            ui.Text("View the original document:"),
            ui.Button(
                label="Open in browser",
                on_click=ui.Open(doc_url),
            ),
        ],
    )

auto_action prop format

Constraint: auto_action is only honored when set on the root UINode of a left-panel handler. It is ignored on nested nodes and on right or center panels. Whether the triggered call opens a center overlay or refreshes a slot panel is decided by the host, not by the slot= metadata in your extension manifest. See auto_action does not re-fire on revisit and Recipe: panel-auto-action-on-load.

auto_action is a prop attached to the root UINode of the left panel only. It fires once per page visit — when you first navigate to the extension page. It does not re-fire after an in-place panel refresh, but it re-fires if you navigate away and return.

The format is a UIAction produced by any of the four helpers. In production it is always ui.Call.

Setup pattern

Production reference: notes/panels.py.

from imperal_sdk import Extension, ui

ext = Extension(app_id="my-ext")


@ext.panel("sidebar", slot="left", title="Notes")
async def sidebar(ctx, **params):  # type: ignore[attr-defined]
    notes = [{"id": "n1", "title": "First note"}, {"id": "n2", "title": "Second note"}]
    root = ui.List(
        items=[
            ui.ListItem(
                id=n["id"],
                title=n["title"],
                on_click=ui.Call("__panel__editor", note_id=n["id"]),
            )
            for n in notes
        ]
    )
    if notes:
        # Conditional: only set when there is content to open.
        root.props["auto_action"] = ui.Call(
            "__panel__editor", note_id=notes[0]["id"]
        )
    return root

Fire-once semantics

auto_action fires once the first time your left panel loads on a page visit, and is not re-dispatched by subsequent in-place refreshes. This means:

  • auto_action fires once per session (once per navigation to the extension page).
  • It does not re-fire when the left panel refreshes after user interaction.
  • It does re-fire if the user navigates away and returns.

Common mistakes

Root-only. auto_action must be set on the UINode that is the direct return value of your handler (the root), not on a nested child. Setting auto_action on a ui.ListItem, a nested ui.Stack, or any non-root node is a silent no-op — only the top-level node is read.

Right panel is ignored. If an extension sets auto_action on the root of a right-panel handler, it is silently ignored. There is no mechanism for right-panel or center-overlay auto_action.

return None blocks auto_action. If your left-panel handler returns None, the panel is never rendered and auto_action cannot fire. Use ui.Empty(message="...") when there is nothing to show.


Where next

On this page