Imperal Docs
Core Concepts

Skeletons

Skeletons — live ambient state and entity feeds that keep Webbee aware of user state, enabling instant routing without exploratory tool calls.

The essence

A skeleton is an async data probe decorated with @ext.skeleton that the web-kernel queries on a per-user TTL schedule. The web-kernel stores each section snapshot in the user's isolated environment and injects ambient state and entity awareness into the routing context on every chat turn.

Because of skeletons, Webbee immediately knows what accounts, projects, unread counts, alerts, and active entities belong to the user — without making exploratory tool calls first.

When the user asks "what's happening with Acme Corp?" or "check my unread emails", Webbee already knows that customer Acme Corp (#cus_102) exists or that there are 8 unread messages across 2 connected accounts.

                    +--------------------------------------+
                    |     @ext.skeleton (every TTL sec)    |
                    +------------------+-------------------+
                                       |
                         Returns entity & state snapshot
                         (up to 50–200 KB per section)
                                       |
                                       v
                    +--------------------------------------+
                    |       Isolated User Environment      |
                    |   (Structured State & Entity Maps)   |
                    +------------------+-------------------+
                                       |
                         Ambient Injection / Routing
                                       |
                                       v
                    +--------------------------------------+
                    |       Webbee Agent & LLM Turn       |
                    |   (Grounds user intent instantly)    |
                    +--------------------------------------+

Skeletons are NOT for building UI

A skeleton is a data probe consumed by the AI Agent — it has no rendering side, no React component, no panel slot. Never use @ext.skeleton to build a sidebar, dashboard, editor, or visible surface. For UI use @ext.panel instead. Inside a panel handler, fetch user state via ctx.cache (short-lived) or ctx.store (persistent) — NOT via ctx.skeleton.get(), which the platform restricts strictly to @ext.skeleton handlers.


Ambient Entity Awareness vs Heavy Payloads

The primary architectural purpose of a skeleton is Ambient Entity Awareness. A skeleton provides the agent with an ambient map of the user's domain so it can recognize entity references, resolve pronouns, and route user commands with zero-shot precision.

1. Good Skeleton Data (Structured Entities & State)

  • Operational metrics and counters: unread_count, active_tasks, critical_alerts_count, total_balance.
  • Connected accounts and targets: [{email, provider, is_active}], [{server_name, ip, status}].
  • Active entity summaries (Entity Cards): Structured lists of key domain objects containing their identifiers and core descriptors:
    • Projects / Workspaces: [{"project_id": "proj_10", "name": "Marketing Portal", "status": "active", "tier": "pro"}]
    • Customers / Contacts: [{"customer_id": "cus_402", "name": "Acme Corp", "status": "active", "mrr": 1200}]
    • Services / Monitors: [{"monitor_id": "mon_88", "domain": "api.example.com", "status": "up", "latency_ms": 45}]
    • Invoices / Orders: [{"order_id": "ord_911", "customer": "Beta LLC", "amount": 350.0, "status": "pending"}]
  • Configuration flags & active contexts: current_workspace_id, sync_enabled, default_currency.

2. Bad Skeleton Data (Heavy Payloads to Avoid)

  • Full message / email bodies: Placing full HTML or plain-text email bodies in a skeleton wastes memory. Keep counts and subject headers in the skeleton; read full bodies on demand via @chat.function (e.g. mail.read_email()).
  • Raw binary data & Base64 files: Blobs, images, and document contents belong in storage or file readers.
  • Unstructured text dumps: Long free-form logs or raw stack traces should be queried via diagnostic tools rather than polled in skeletons.

Canonical Example: Projects & State Skeleton

Every skeleton is one Python async function. The decorator registers it as a synthetic tool named skeleton_refresh_{section_name}; the web-kernel schedules and invokes it periodically.

canonical skeleton example
from imperal_sdk import Extension

ext = Extension(
    "projects",
    display_name="Projects",
    description="Projects extension — manage active development projects with AI.",
    icon="icon.svg",
    actions_explicit=True,
)


@ext.skeleton(
    "projects_overview",
    alert=True,
    ttl=300,
    description="Project counts, active workspaces, and recent project entity cards.",
)
async def skeleton_refresh_projects_overview(ctx) -> dict:
    """Refresh project statistics and top active entities. Pure read — idempotent."""
    uid = ctx.user.imperal_id
    if not uid:
        return {"response": {"note": "no user on context"}}

    # Fetch counts
    total_projects = await ctx.store.count("projects")
    active_count   = await ctx.store.count("projects", where={"status": "active"})
    archived_count = await ctx.store.count("projects", where={"status": "archived"})

    # Fetch active project entities for ambient context
    recent = await ctx.store.query(
        "projects",
        where={"status": "active"},
        order_by="-updated_at",
        limit=20,
    )

    return {"response": {
        "total_projects": total_projects,
        "active_projects": active_count,
        "archived_projects": archived_count,
        "recent_projects": [
            {
                "project_id": d.id,
                "name": d.data.get("name", ""),
                "status": d.data.get("status", "active"),
                "tier": d.data.get("tier", "standard"),
            }
            for d in recent.data
        ],
    }}

The decorator's signature:

@ext.skeleton(
    section_name: str,      # required — positional, snake_case identifier
    *,
    alert: bool = False,    # if True, paired skeleton_alert_{section_name} fires on change
    ttl: int = 300,         # refresh interval in seconds
    description: str = "",  # manifest description for tools and catalog
)

Architecture & Scale Guidelines

Imperal Cloud's architecture is engineered to support hundreds of thousands of concurrent users with zero lock contention.

1. Payload Capacity & Density

Each skeleton section can return up to 50–200 KB of structured JSON, allowing an extension to comfortably surface hundreds of active entities (e.g. up to 100–500 compact entity cards).

The platform handles indexing and entity routing behind the scenes, ensuring that the AI agent receives the exact relevant entity slice without bloating the immediate prompt budget.

2. Choosing the Right TTL

Set your TTL based on data velocity:

FrequencyTypical TTLBest Suited For
High Velocity30–60sActive inbox counters, unread notification counts, live system incidents.
Standard Operational120–300sProject summaries, monitor status gauges, active task boards, shopping carts.
Slow-Moving / Static300–600sConnected service lists, database schemas, subscription plans, user settings.

3. Clear Entity Descriptors

When returning entity cards in arrays, include standard identifying fields:

  • id or {entity}_id (e.g. project_id, task_id, invoice_id).
  • name or title / label.
  • status or state (e.g. active, pending, failed, paid).

This allows the agent to immediately resolve pronouns and natural names (e.g., "deploy the marketing portal" matches name="Marketing Portal", project_id="proj_10").


Common Patterns

1. Counter & Metrics Skeleton

Surfaces instant counts for quick triage without executing search tools:

counter pattern
@ext.skeleton(
    "tasks_summary",
    alert=True,
    ttl=60,
    description="Overdue, today, and pending task counts.",
)
async def skeleton_refresh_tasks_summary(ctx) -> dict:
    overdue  = await ctx.store.count("tasks", where={"overdue": True})
    today    = await ctx.store.count("tasks", where={"due_today": True})
    pending  = await ctx.store.count("tasks", where={"status": "pending"})

    return {"response": {
        "overdue_count": overdue,
        "today_count": today,
        "pending_count": pending,
    }}

2. Multi-Account / Service Inventory

Surfaces which accounts and external platforms are currently active:

accounts inventory pattern
@ext.skeleton(
    "connected_accounts",
    ttl=300,
    description="List of connected external accounts and operational status.",
)
async def skeleton_refresh_connected_accounts(ctx) -> dict:
    accounts = await ctx.store.query("oauth_accounts", limit=20)
    return {"response": {
        "accounts_count": len(accounts.data),
        "accounts": [
            {
                "account_id": a.id,
                "email": a.data.get("email", ""),
                "provider": a.data.get("provider", ""),
                "is_active": a.data.get("is_active", True),
            }
            for a in accounts.data
        ],
    }}

3. Alert-on-Change Skeleton

When alert=True, the web-kernel monitors snapshot diffs and invokes the paired tool skeleton_alert_{section_name} when data changes:

alert-on-change pattern
@ext.skeleton("monitors", alert=True, ttl=60)
async def skeleton_refresh_monitors(ctx) -> dict:
    critical = await ctx.store.count("monitors", where={"status": "down"})
    total    = await ctx.store.count("monitors")
    return {"response": {"critical_down": critical, "total_monitors": total}}


@ext.tool(
    "skeleton_alert_monitors",
    description="Emits ambient notification when monitors change state.",
)
async def skeleton_alert_monitors(
    ctx,
    old: dict | None = None,
    new: dict | None = None,
) -> dict:
    if not new:
        return {"response": ""}
    new_down = new.get("critical_down", 0)
    old_down = (old or {}).get("critical_down", 0)
    if new_down > old_down:
        diff = new_down - old_down
        return {"response": f"{diff} service(s) went down — {new_down} critical total."}
    return {"response": ""}

Skeletons vs Other Context Channels

LayerTriggerConsumerTypical LifetimeMain Purpose
Skeleton@ext.skeleton (TTL)AI Agent Routing ContextContinuous (Refreshed)Ambient entity map, live counters, status flags
Cachectx.cache.set()Tools & Panels5–300s TTLPre-computed pages, API responses
Fact-Ledger(Automatic)AI AgentLast 5 chat turnsVerbatim tool output recall across turns
Panel@ext.panelUser (Browser / UI)Active UI sessionInteractive dashboards, visual forms, tables
Storectx.storeExtension BackendPermanentDurable database records and documents

Federal guarantees

🪜

Auto-Discovery

Sections are discovered automatically by the skeleton_refresh_{X} contract — no manual manifest wiring needed.

🔄

Resilient Watchdog

The platform supervises background refresh loops, restarting any stalled worker gracefully.

🧹

Instant Invalidation

When an extension is uninstalled, all its ambient skeleton state is purged immediately.

🔒

Per-User Isolation

One isolated skeleton store runs per user. Identity is kernel-authoritative and leak-proof.

🚫

Access-Guarded

ctx.skeleton.get() is restricted exclusively to @ext.skeleton probes, preventing cross-layer coupling.


What's next

On this page