Imperal Docs
SDK Reference

@ext.skeleton reference

@ext.skeleton reference — declare ambient live state and entity feeds for Webbee, covering section naming, return contracts, and TTL intervals.

@ext.skeleton registers a live data probe that the web-kernel queries on a per-user schedule. The structured snapshot the probe returns is stored in the user's isolated environment and injected into the routing context on every chat turn as ambient awareness.

The LLM does not call skeleton tools explicitly — it sees their output as ambient background facts.

Use skeletons for "what does the user have right now" state: unread counters, active projects, connected accounts, monitor alerts, and key entity summaries.

Not a UI primitive

@ext.skeleton is strictly a data probe consumed by the AI Agent. It has no rendering, no panel slot, and no React component. If you are building anything visible to the user — sidebars, dashboards, editors, settings forms — use @ext.panel instead. Inside a panel, fetch state via ctx.cache (short-lived) or ctx.store (persistent). ctx.skeleton.get() is restricted exclusively to @ext.skeleton handlers and raises SkeletonAccessForbidden from any other context.


Where it lives

@ext.skeleton is a method on the Extension instance (not on ChatExtension). Register it from the same file that holds your ext = Extension(...), or import ext from app.py into a dedicated skeleton.py module — the pattern used across production extensions.

app.py or skeleton.py
from imperal_sdk import Extension

ext = Extension(
    "projects",
    display_name="Projects",
    description="Manage active projects and entities.",
    icon="icon.svg",
    actions_explicit=True,
)


@ext.skeleton("projects", alert=True, ttl=300)
async def skeleton_refresh_projects(ctx) -> dict:
    ...

Signature

def skeleton(
    self,
    section_name: str,
    *,
    alert: bool = False,
    ttl: int = 300,
    description: str = "",
) -> Callable:

Parameters

section_name

str · positional

Required. Positional. The name for this skeleton section. The decorator registers your handler under the tool name skeleton_refresh_{section_name}.

  • Must be a valid identifier: ASCII letters, numbers, underscores (e.g. "status", "inbox", "projects_overview").
  • Keep it concise — the section name is used in logs, audit records, and context framing.
  • Each call to @ext.skeleton must use a unique section_name within an extension.

alert

bool · default: False

When alert=True, the platform monitors the returned snapshot between refresh cycles:

  1. After every skeleton refresh, the web-kernel diffs the previous snapshot against the new one.
  2. If the snapshots differ, the web-kernel calls skeleton_alert_{section_name}(ctx, old=<prev>, new=<next>).
  3. If that alert tool returns a non-empty string, Webbee proactively notifies the user about the change.
alert-enabled skeleton
@ext.skeleton("tasks", alert=True, ttl=60)
async def skeleton_refresh_tasks(ctx) -> dict:
    overdue = await ctx.store.count("tasks", where={"overdue": True})
    return {"response": {"overdue_count": overdue}}


@ext.tool(
    "skeleton_alert_tasks",
    description="Alerts user when overdue count increases.",
)
async def skeleton_alert_tasks(
    ctx,
    old: dict | None = None,
    new: dict | None = None,
) -> dict:
    if not new:
        return {"response": ""}
    old_c = (old or {}).get("overdue_count", 0)
    new_c = new.get("overdue_count", 0)
    if new_c > old_c:
        return {"response": f"You have {new_c - old_c} new overdue task(s)."}
    return {"response": ""}

ttl

int · default: 300

Refresh interval in seconds. Defines how frequently the platform re-queries this probe for each active user.

  • 30–60s: High-velocity feeds (unread email counts, active outages, live sensor alerts).
  • 120–300s: Standard operational state (active projects, task boards, carts).
  • 300–600s: Stable configuration & catalogs (connected services, database schemas, plan limits).

description

str · default: ""

Human-readable description for Developer Portal manifests and operational logs. Defaults to "Skeleton refresh: {section_name}".


Return Contract

Every @ext.skeleton handler must return a dictionary with a single "response" key containing scalar fields and entity cards:

return {
    "response": {
        "scalar_metric": 42,
        "is_synced": True,
        "key_entities": [
            {"id": "ent_1", "name": "Primary", "status": "active"},
            {"id": "ent_2", "name": "Secondary", "status": "pending"},
        ],
    }
}

1. What to put into a Skeleton (Entity Cards & Ambient State)

  • Top-level scalar metrics & counters: unread_count, active_servers, overdue_tasks.
  • Connected accounts & targets: [{email, provider, is_active}], [{host, ip, status}].
  • Key Entity Cards (up to 50–200 KB per section):
    • Identity: id or {type}_id (e.g. customer_id, project_id, invoice_id).
    • Label: name, title, or label.
    • State: status, state, or stage (e.g. active, paid, down).
    • Key attributes: tier, amount, role, tags.

2. What NOT to put into a Skeleton (Heavy Payloads)

Content TypeWhy it does not belong in SkeletonWhere it belongs
Full email / message bodiesConsumes unnecessary context; unneeded for routingDedicated @chat.function (mail.read_email)
Unbounded raw database tablesSkeletons are ambient summaries, not database replicasPaginated list tools (tasks.list_tasks(limit=...))
Raw binary data / Base64 filesSkeletons only carry structured JSON metadatactx.store or dedicated download endpoints
Full historical log streamsHistorical analysis is done on-demandDiagnostic query tools

Reading Previous Snapshots: ctx.skeleton.get()

ctx.skeleton.get(section) reads the previously stored snapshot for a section. It is available only inside @ext.skeleton handlers (or alert companions):

@ext.skeleton("inbox", ttl=60)
async def skeleton_refresh_inbox(ctx) -> dict:
    prev_snapshot = await ctx.skeleton.get("inbox")
    # compare or compute diffs if needed
    ...

Access Forbidden outside skeletons

Calling ctx.skeleton.get() from a standard @chat.function or @ext.panel handler raises SkeletonAccessForbidden at runtime (Validator V24-AST). Panels and tools must read user state via ctx.cache or ctx.store.


On this page