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
| Prefix | Dispatch path | Typical use |
|---|---|---|
__panel__{panel_id} | System-task path — params spread directly to the handler via **kwargs | Open / refresh a panel |
__widget__{widget_id} | System-task path | Refresh a widget |
__webhook__{name} | System-task path | Internal 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 rootFire-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_actionfires 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
Panels concept
Slot ownership, lifecycle, batch discovery, and the dispatch model.
Decorators reference
@ext.panel signature, slot allowlist, refresh modes, and kwargs.
Recipe: auto-action on load
End-to-end walkthrough of the auto_action pattern.
Panel troubleshooting
Cases 3 (auto_action not re-firing) and 4 (dispatch-split myth).
SDL — Structured Data Layer
SDL — Structured Data Layer: type the meaning of fields so Webbee reads your entities as data, with typed facets and roles instead of field-name guessing.
UI primitives reference
UI primitives reference — the building-block components for Imperal panels: layout, data display, inputs, feedback, and graph nodes with props and examples.