Imperal Docs
SDK Reference

Manifest reference

Every field in the Imperal SDK imperal.json manifest (schema v3): what each does, required vs optional, accepted values — auto-generated by imperal build.

imperal.json is the manifest the marketplace and web-kernel both read. You don't author it by hand — imperal build ./extension generates it from your app.py. This page documents every field so you know what's in there and why.

All field names track generate_manifest() in the current imperal-sdk source — see the changelog for version-pinned history.

Top-level shape

{
  "manifest_schema_version": 3,
  "sdk_version": "5.4.2",
  "app_id": "tasks",
  "version": "1.2.3",
  "name": "Tasks (Mini)",
  "description": "A task manager Webbee can drive in plain language. ≥40 chars.",
  "icon": "icon.svg",
  "icon_size_bytes": 4096,
  "actions_explicit": true,
  "system": false,
  "capabilities": ["tasks:read", "tasks:write"],
  "tools": [...],
  "signals": [...],
  "schedules": [...],
  "required_scopes": [...],
  "webhooks": [...],
  "oauth": [...],
  "events": {
    "subscribes": [...],
    "emits": [...]
  },
  "exposed": [...],
  "lifecycle": {...},
  "lifecycle_hooks": {...},
  "tray": [...]
}

Top-level fields

Prop

Type

How icons are served

The icon field is a path in your extension repo (icon.svg next to app.py). Commit the file, then publish your extension once — the platform persists the SVG bytes alongside the rest of your manifest and streams them back to every surface (marketplace, sidebar, chat) at request time. There is no filesystem sync to configure; one publish propagates the icon everywhere.

The marketplace exposes a public, cached endpoint per app that returns image/svg+xml. Browsers cache aggressively because icon bytes change only when you re-publish.

Marketplace listing fields

These describe how your app is presented in the marketplace: name, description, author, license, homepage, icon, category, tags.

They are not produced by imperal build the way tools[] or schedules[] are — the SDK never reads them off your Extension(...) constructor. They are merged in from your Dev Portal entry at publish time. Editing them in a built imperal.json changes nothing.

Category

Pick from the catalog — it is not free text. The list is served by the platform (GET /v1/marketplace/categories/catalog, grouped and localised), and that catalog is the only source of truth. The SDK deliberately keeps no copy: category is a plain optional string in the schema with no enum, so a new category ships without an SDK release.

Inventing a value is how a category list rots — free text is what once made system and System two separate browsable categories.

You do not have to match an id character-for-character. On write the platform canonicalises what you send, and all three of these land on the same category:

You writeResolves toWhy
messagingmessagingthe catalog id itself
Messaging & Chatmessagingthe display label shown in the picker
tools, general, otherdeveloper-tools, productivity, productivityhistorical spellings kept working

Ids are ASCII and permanent; display labels live in the platform's locale packs, so renaming a label never changes an app's category. Text that genuinely maps to nothing is rejected, with suggestions. Omit the field and the app lands in productivity.

tools[] (ToolDef)

One entry per @chat.function and per @ext.tool (non-synthetic). The platform reads tools[] directly.

{
  "name": "create_task",
  "description": "Add a task to the user's pending list.",
  "action_type": "write",
  "chain_callable": true,
  "effects": ["task.create"],
  "id_projection": null,
  "params_schema": {
    "$defs": {...},
    "properties": {
      "title": { "description": "The task — what needs to be done", "type": "string" }
    },
    "required": ["title"],
    "title": "CreateTaskParams",
    "type": "object"
  },
  "return_schema": {},
  "event": "task.created",
  "scopes": [],
  "owner_chat_tool": "my-app"
}

Prop

Type

schedules[] (ScheduleDef)

One entry per @ext.schedule.

{
  "name": "nightly_archive",
  "cron": "0 3 * * *"
}

Prop

Type

webhooks[] (WebhookDef)

One entry per @ext.webhook.

{
  "path": "/incoming",
  "method": "POST",
  "secret_header": "X-Hub-Signature-256"
}

Prop

Type

oauth[] (OAuthDecl) — unified OAuth-connect (v5.9.0+)

One entry per ext.oauth(...). Declares a provider the extension connects to through the platform's unified OAuth-connect flow. The platform hosts the callback at /v1/ext/{app_id}/oauth/{provider}/callback, validates a signed state, exchanges the code, and persists the connected account + tokens into collection.

{
  "provider": "google",
  "collection": "mail_accounts",
  "scopes": ["https://www.googleapis.com/auth/gmail.readonly"]
}

Prop

Type

The OAuth client credentials ({provider}_client_id / {provider}_client_secret) are declared as app-scope secrets (scope: "app" in secrets[]) — set once by the developer in the Dev Portal, shared across all users. See the decorator ext.oauth reference.

events.subscribes[]

One entry per @ext.on_event.

{"type": "email.received", "handler": "handle_email_received"}

events.emits[]

One entry per @ext.emits.

{"type": "my-app.item.created", "schema_ref": "ItemCreatedEvent"}

lifecycle

{
  "on_install": true,
  "on_uninstall": true,
  "on_upgrade": ["2.0.0"],
  "health_check": {"interval_sec": 60}
}

Prop

Type

lifecycle_hooks

Added in v4.0.0 (V22). Records Python signature strings so the web-kernel knows which kwargs to pass to each lifecycle hook.

{
  "on_install": {"signature": "(ctx)"},
  "on_upgrade:2.0.0": {"signature": "(ctx)"}
}

tray[] (TrayDef)

One entry per @ext.tray.

{
  "tray_id": "unread",
  "icon": "Mail",
  "tooltip": "Unread messages",
  "zone": "status",
  "order": 100,
  "badge_style": "corner",
  "icon_color": "default"
}

zone and order are emitted since 5.10.0, badge_style since 5.12.0, icon_color since 5.13.0. All four carry their defaults when the decorator does not set them, so an older manifest and a freshly emitted one render identically — the defaults are the pre-existing appearance. Full meanings are in the @ext.tray reference.

exposed[] (ExposedMethod)

One entry per @ext.expose.

{"name": "get_summary", "action_type": "read"}

secrets[] (SecretDecl) — EXT-SECRETS-V1 (v4.2.2+)

One entry per @ext.secret. Optional field; manifest schema v3 stays — secrets[] is additive and back-compatible. Extensions without @ext.secret declarations omit the field entirely.

The Pydantic schema for this entry is SecretDecl in imperal_sdk.manifest_schema (added in v4.2.8 to close a manifest emitter/schema drift gap, so the field round-trips through the schema validator). It mirrors the runtime SecretSpec dataclass after to_manifest_dict(). Since v4.2.8 validate_manifest_dict() rejects malformed secret entries at publish time — uppercase name, invalid write_mode, max_bytes outside [1, 65536], empty description. Earlier versions silently accepted these.

{
  "secrets": [
    {
      "name": "spotify_client_secret",
      "description": "Your Spotify app's client secret (shared by all users).",
      "required": true,
      "write_mode": "user",
      "max_bytes": 200,
      "scope": "app",
      "env_fallback": "IMPERAL_APPSECRET_SPOTIFY_SPOTIFY_CLIENT_SECRET"
    },
    {
      "name": "spotify_refresh_token",
      "description": "OAuth refresh token written by extension after this user authorizes.",
      "required": false,
      "write_mode": "extension",
      "max_bytes": 4096,
      "rotation_hint_days": 30,
      "scope": "user"
    }
  ]
}

Prop

Type

The Panel UI at /ext/[extId]/secrets lists every entry, lets the user set/rotate write_mode='user' and write_mode='both' secrets, and shows read-only helper text for write_mode='extension' secrets. See @ext.secret reference for the full federal contract.

Generated, not authored

You don't write imperal.json by hand:

imperal build ./extension
# or
python3 -c "
from imperal_sdk import save_manifest
import app
save_manifest(app.ext, '.')
"

This walks your app.py, introspects every decorated function, and produces the JSON. Editing it manually after build is counterproductive — the web-kernel and your code will drift and validators stop catching things.

Size: the 1 MB wire cap

The platform stores your manifest through a gateway that caps the request body at 1 MB. The number that decides the outcome is the compact serialization (no indentation) that the deploy path actually sends -- not the pretty-printed file sitting on disk.

For a very large app the two differ enough to change the answer. WordPress Hub, 260 chat functions:

FormSizeUnder the cap
imperal.json on disk (indent=2)1212 KBno
Compact, as sent609 KByes

603 KB of that file is indentation whitespace. A manifest can look oversized and still deploy perfectly well -- only the compact size tells you the truth.

If the compact form genuinely exceeds 1 MB, the deploy reports manifest_synced: false and logs a warning naming the measured size. The manifest is the only artifact skipped -- tools, panels and the icon still sync. Shrink it by trimming long description= strings and oversized return_schema declarations; on a large manifest those dominate the bytes.

Common mistakes

Wrong field name: The top-level version field is manifest_schema_version, not schema_version. Several tools in older docs and LLM completions use the wrong name — the web-kernel will reject a manifest with schema_version.

Wrong array name: Chat function entries are in tools[], not functions[]. functions is not a valid manifest field.

Where to next

On this page