Experimental decorators reference
@ext.tray / @ext.widget / @ext.expose / @ext.oauth_provider — under-specified surfaces with prominent warnings
These decorators exist in the SDK but have one or more of: zero production deployment, incomplete manifest emission, undocumented host-side contract, or routing paths not yet committed to a public API. Each section below documents what is in the SDK source, what is known to work, and what is not yet reliable.
Experimental surface — contract subject to change
The four decorators on this page are not part of the stable v4.1 contract. No production extensions on Imperal Cloud currently use them at scale. The signatures may shift, manifest emission may change, and web-kernel-side routing is not documented as a public API. Build against them only with explicit awareness that you may need to migrate when the contract solidifies.
@ext.tray
System tray item: an icon in the platform's top-bar area with an optional badge count and dropdown panel. The handler returns a UINode tree (typically ui.TrayResponse) that the platform renders as a compact overlay.
Experimental — zero production deployments
@ext.tray is SDK-defined and manifest-emitting, but no production extension currently uses it. Frontend rendering behaviour of TrayResponse (badge positioning, dropdown lifecycle) is not formally documented. Do not ship tray items to users without verifying that your platform version renders them.
Signature
def tray(
self,
tray_id: str,
icon: str = "Circle",
tooltip: str = "",
) -> Callable:
...Kwargs reference
Prop
Type
Handler signature
async def handler(ctx, **kwargs) -> UINode:
...The decorator wraps your handler: if the return value has a .to_dict() method, the wrapper serializes it to {"ui": ..., "tray_id": tray_id, "icon": icon} before returning to the caller. If the return value does not have .to_dict(), it is passed through as-is.
How it registers
Internally, @ext.tray does two things:
- Adds a
TrayDeftoext._tray[tray_id]withtray_id,icon, andtooltip. - Adds a synthetic
ToolDefnamed__tray__{tray_id}toext._tools, which is what the web-kernel dispatches via the/callendpoint.
The generate_manifest() function emits a tray[] section in the manifest for every registered TrayDef:
{
"tray": [
{"tray_id": "unread", "icon": "Mail", "tooltip": "Unread messages"}
]
}Recommended return type
Use ui.TrayResponse for a badge + optional dropdown panel:
from imperal_sdk import Extension, ui
ext = Extension(
"my-mail",
version="1.0.0",
display_name="My Mail",
description="My Mail extension — read and send email with AI assistance.",
icon="icon.svg",
actions_explicit=True,
)
@ext.tray("unread", icon="📧", tooltip="Unread messages")
async def tray_unread(ctx, **kwargs):
count = await ctx.store.count("messages", where={"read": False})
badge = ui.Badge(str(count), color="red" if count > 0 else "gray")
return ui.TrayResponse(badge=badge)ui.TrayResponse accepts two optional kwargs:
| Kwarg | Type | Description |
|---|---|---|
badge | UINode | None | Overlay node — typically ui.Badge |
panel | UINode | None | Dropdown panel node — any UINode tree |
Known constraints
- There is no slot validation at decoration time — unlike
@ext.panel,@ext.traydoes not enforce known tray IDs. - The platform-side rendering contract for
TrayResponse.panel(dropdown positioning, dismiss behaviour, scroll containment) is not documented. - Zero production extensions use
@ext.tray. If you build one, verify rendering end-to-end on your platform version before shipping to users.
Production usage
None found in any reviewed Imperal extension at time of audit (SDK v4.2.0).
@ext.widget
Widget for platform injection points: a small UI fragment (stat tile, toolbar button, contextual action) rendered at a named slot in the platform host. The handler returns a UINode tree.
Manifest emission gap — web-kernel cannot discover widgets
@ext.widget stores only a synthetic ToolDef (__widget__{widget_id}) in ext._tools. There is no widgets[] section emitted by generate_manifest() — the manifest builder skips all __*-prefixed tools. This means the web-kernel cannot discover registered widgets from the manifest using the standard manifest-driven flow. Side-channel discovery would be required. Until this gap is closed, @ext.widget is not production-ready for manifest-driven deployments.
Signature
def widget(
self,
widget_id: str,
slot: str = "dashboard.stats",
label: str = "",
icon: str = "",
**kwargs,
) -> Callable:
...Kwargs reference
Prop
Type
Additional **kwargs are silently accepted and ignored — they are not stored anywhere.
What actually gets stored
The decorator wraps your handler (serializing .to_dict() results to {"ui": ..., "widget_id": widget_id}) and registers only a synthetic ToolDef:
ext._tools["__widget__my_stat"] = ToolDef(
name="__widget__my_stat",
func=<wrapper>,
description="Widget: My Stat",
)The widget metadata (slot, label, icon) is not stored in any dedicated registry. There is no ext._widgets attribute. The manifest emitter skips __widget__* entries along with all other __* entries. The platform has no way to read widget slot assignments from the manifest.
Illustrative usage
The following shows the decorator API as written. It will not produce a discoverable manifest entry:
from imperal_sdk import Extension, ui
ext = Extension(
"my-app",
version="1.0.0",
display_name="My App",
description="My App extension — manage resources with AI assistance.",
icon="icon.svg",
actions_explicit=True,
)
@ext.widget("active_count", slot="dashboard.stats", label="Active Items", icon="🌀")
async def widget_active_count(ctx, **kwargs):
count = await ctx.store.count("items", where={"status": "active"})
return ui.Badge(str(count), color="blue")No manifest entry emitted
The code above registers the handler callable and a synthetic tool for direct dispatch, but generate_manifest() will not include a widgets[] section. The web-kernel relies on the manifest for discovery. Without a widgets[] section, the slot assignment is invisible to the platform.
Production usage
None found in any reviewed Imperal extension at time of audit (SDK v4.2.0).
@ext.expose
Cross-extension IPC: expose a named method that other extensions can call via ctx.extensions.call(app_id, method_name, **kwargs). The Auth Gateway routes the call to the target extension's exposed handler.
Experimental — IPC contract not fully documented
@ext.expose stores the handler in ext._exposed and emits it to the manifest exposed[] section. ExtensionsClient.call() routes via the Auth Gateway to the target. However, the following are not documented as a public contract: the exact request shape the target handler receives, error semantics (what happens on timeout, auth failure, or handler exception), and cross-tenant IPC limitations. No production extension has deployed @ext.expose at time of audit. The imperal-drive extension design document uses this pattern as a planned interface — it is not yet deployed.
Signature
def expose(
self,
name: str,
action_type: str = "read",
) -> Callable:
...Kwargs reference
Prop
Type
Handler signature
async def handler(ctx, **kwargs) -> Any:
...The handler receives the caller's Context object (with the caller's identity, not the calling extension's identity — verify this with a live test) and the keyword arguments passed to ctx.extensions.call().
What gets stored and emitted
# Internal storage:
ext._exposed["my_method"] = ExposedMethod(
name="my_method",
func=<handler>,
action_type="read",
)generate_manifest() emits:
{
"exposed": [
{"name": "my_method", "action_type": "read"}
]
}Calling side
From the consuming extension:
result = await ctx.extensions.call("target-app", "method_name", param1="value")ctx.extensions is an ExtensionsClient. It raises CircularCallError if a circular dependency is detected.
Illustrative usage
from imperal_sdk import Extension
ext = Extension(
"my-storage",
version="1.0.0",
display_name="My Storage",
description="My Storage extension — manage files with AI assistance.",
icon="icon.svg",
actions_explicit=True,
)
@ext.expose("get_file_url", action_type="read")
async def expose_get_file_url(ctx, file_id: str = "", ttl_s: int = 3600):
doc = await ctx.store.get("files", file_id)
if doc is None:
return {"error": "not_found"}
return {"url": doc.data.get("url"), "expires_in_s": ttl_s}
@ext.expose("register_file", action_type="write")
async def expose_register_file(ctx, path: str = "", mime_type: str = ""):
doc = await ctx.store.create("files", {"path": path, "mime_type": mime_type})
return {"file_id": doc.id}Consuming extension:
# In another extension's @chat.function handler:
url_result = await ctx.extensions.call(
"my-storage", "get_file_url", file_id="abc123", ttl_s=7200
)Known gaps
- The Auth Gateway routing path for cross-extension calls is not documented as a stable public contract.
- Whether the target handler receives the caller's user context, the target's service context, or a merged context is not specified in public documentation.
- Error handling on the receiving side (what the caller sees on handler exception, timeout, or auth failure) is not specified.
- Cross-tenant calls (caller in tenant A, target in tenant B) may be blocked — the isolation contract is not documented.
Production usage
No deployed production extension uses @ext.expose at time of audit (SDK v4.2.0). The imperal-drive extension has @ext.expose in its design specification (handlers_cross_ext.py section) but this extension had not been deployed.
@ext.oauth_provider
@ext.oauth_provider does not exist in the SDK at v4.2.0. A search of the SDK source finds no oauth_provider method, class, or decorator anywhere in imperal_sdk.
This decorator does not exist in SDK v4.2.10
There is no @ext.oauth_provider in the current SDK. If you encountered this name in older documentation, a design document, or a community post, it refers to a planned future surface that has not shipped. Do not write code that references it.
OAuth flows in current SDK extensions are handled via @ext.webhook (OAuth callback endpoint) combined with secret storage via ctx.secrets. The imperal-drive design document references an ctx.oauth.complete_flow() call in its @ext.webhook("oauth_callback") handler — that method is not part of the public Context surface documented in v4.2.12.
If you need OAuth token management, use the pattern from deployed extensions:
- Register a webhook to receive the OAuth callback:
@ext.webhook("oauth_callback") - Inside the handler, extract the authorization code from
query_params - Exchange it for tokens via
ctx.http.post(...)to the provider's token endpoint - Persist tokens in
ctx.store
When to use experimental decorators
Short answer: only if you are prepared to refactor when the contract changes, and only after verifying the feature works in a staging environment on your exact platform version.
Specifically:
@ext.tray— use in staging only; verify badge rendering and dropdown lifecycle before shipping to users. The manifest does emit atray[]section, so the web-kernel can find tray items, but frontend rendering is unverified in production.@ext.widget— do not use in production until the manifest emission gap is resolved. The web-kernel cannot discover widget slots from the manifest.@ext.expose— use carefully in non-critical paths; the IPC error contract is not yet specified. Start withaction_type="read"methods where failure is recoverable.@ext.oauth_provider— does not exist; do not reference it.
Filing feedback
If you build against an experimental decorator and discover routing behaviour, error semantics, or rendering constraints that are not documented here, file a ticket with the Imperal Developer Portal. Experimental contracts solidify faster when concrete usage findings are reported.
Cross-references
@ext.panel reference
Stable full-screen panel decorator — use this for primary extension UI before reaching for @ext.widget.
@ext.webhook reference
Webhook endpoint decorator — the current stable pattern for OAuth callbacks and external integrations.
@ext.events reference
Event-driven cross-extension communication via @ext.emits and @ext.on_event — the stable alternative to @ext.expose for loose coupling.
Decorators reference
Quick index of all SDK decorators — stable and experimental.