Imperal Docs
Guides

Local dev setup

From zero to a running extension in 10 minutes — Python install, file layout, validate, run, iterate

This guide takes you from a blank terminal to a validated, locally testable extension. No running web-kernel required for any step on this page.

What you'll have at the end

  • A working extension directory with the production-standard file layout
  • A minimal @chat.function that compiles and passes the validator
  • A quick-test script that exercises the handler without a running web-kernel
  • A manifest generated from your code via imperal build
  • Confidence that imperal validate, py_compile, and pyright all exit 0

Prerequisites

ToolMinimumRecommended
Python3.113.12 or 3.13
pip24.0latest
pyrightanylatest (pip install pyright)
OSmacOS / Linux / Windows (WSL)macOS or Linux

Why 3.11+?

The SDK uses X | None union syntax, asyncio.TaskGroup, and typing.get_type_hints() behaviour that requires Python 3.11. 3.10 will fail at import time.

If you are not sure which Python version is active:

python3 --version

To install a specific version with pyenv:

pyenv install 3.12
pyenv local 3.12

Install the SDK

Create a virtual environment

Always develop inside a virtual environment. This isolates your extension from system packages and avoids version conflicts between extensions on the same machine.

python3 -m venv .venv
source .venv/bin/activate      # macOS / Linux
# .venv\Scripts\activate       # Windows PowerShell

Install imperal-sdk

pip install "imperal-sdk"

In requirements.txt, pin to whatever version you tested against. The production worker uses last-install-wins semantics, so a >= pin can pull a future major version and break a shared environment.

The [dev] extras add the full test stack:

pip install "imperal-sdk[dev]"

This installs: pytest, pytest-asyncio, respx, openapi-spec-validator, jsonschema.

Verify

python -c "import imperal_sdk; print(imperal_sdk.__version__)"
imperal --version

Both commands should print a version string. The exact numbers track PyPI — see Changelog for what's in each release, and use pip install -U imperal-sdk to upgrade later.


Production extensions follow a consistent structure that separates concerns by file. The notes extension is the canonical example:

main.py
app.py
handlers_crud.py
panels.py
skeleton.py
icon.svg
imperal.json
requirements.txt
__init__.py
conftest.py
test_handlers_crud.py
FilePurpose
main.pyEntrypoint — inserts directory into sys.path, purges stale sys.modules entries, then imports ext and all handler modules so decorators register against the same Extension instance
app.pyExtension(...) and ChatExtension(...) construction; @ext.cache_model, @ext.emits, lifecycle hooks; HTTP helper functions
handlers_*.py@chat.function handlers grouped by domain — one file per concern (crud, search, schedule, etc.)
panels.py@ext.panel handlers
skeleton.py@ext.skeleton handlers — only code that refreshes LLM context snapshots
icon.svgExtension icon — must be valid SVG with viewBox, ≤100 KB
imperal.jsonGenerated manifest — never hand-edit; regenerate with imperal build .
requirements.txtList your deps; pin to the version you tested against

main.py is required by the CLI

imperal validate and imperal build both import from main, not app.py. main.py must expose the ext object at module scope. The canonical pattern (from notes/main.py) is: insert the extension directory into sys.path, purge any stale module cache entries, then from app import ext, chat.


Hello world extension

Create the directory and files:

mkdir my-extension && cd my-extension
python3 -m venv .venv && source .venv/bin/activate
pip install "imperal-sdk[dev]"
mkdir tests && touch tests/__init__.py

app.py

app.py
"""my-extension — Imperal Cloud Extension."""
from __future__ import annotations

from imperal_sdk import Extension
from imperal_sdk.chat import ChatExtension, ActionResult  # noqa: F401 — re-exported

ext = Extension(
    "my-extension",
    version="1.0.0",
    capabilities=["my-extension:read"],
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)

chat = ChatExtension(ext, tool_name="my_extension_chat", description="My Extension chat assistant")

handlers_crud.py

handlers_crud.py
"""my-extension — core handlers."""
from __future__ import annotations

from pydantic import BaseModel, Field
from imperal_sdk import ActionResult
from app import chat


class GreetParams(BaseModel):
    name: str = Field(description="Name of the person to greet")


@chat.function(
    "greet",
    description="Send a friendly greeting to a person by their name",
    action_type="read",
)
async def greet(ctx: object, params: GreetParams) -> ActionResult:
    """Greet a user by name."""
    message = f"Hello, {params.name}! Welcome to My Extension."
    return ActionResult.success(
        data={"message": message, "name": params.name},
        summary=f"Greeted {params.name}",
    )

main.py

main.py
"""my-extension — entrypoint."""
from __future__ import annotations

import sys
import os

_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _dir)

# Purge stale module cache so re-imports pick up code changes
for _m in [k for k in sys.modules if k in ("app", "handlers_crud")]:
    del sys.modules[_m]

from app import ext, chat  # noqa: F401
import handlers_crud       # noqa: F401

requirements.txt

imperal-sdk

icon.svg

The validator (V21) requires a valid SVG with a viewBox attribute, no embedded raster images, and a file size ≤100 KB. A minimal placeholder:

icon.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
     stroke="currentColor" stroke-width="2" stroke-linecap="round"
     stroke-linejoin="round">
  <circle cx="12" cy="12" r="10"/>
</svg>

Validate

The imperal validate command runs all V1–V24 validators plus the v1.6.0 AST rules against your extension. It must exit 0 before you submit to the Developer Portal.

# Run from inside the extension directory
imperal validate .

Sample output for a clean extension:

── Imperal Extension Validator v1.0 ────────────────────────────
Extension: my-extension v1.0.0
Tools: 2, Functions: 1, Events: 0

✅ No issues found!

If there are errors, the command exits with code 1 and prints each rule code, location, message, and suggested fix. Fix all ERROR level issues before deploying. WARN level issues are advisory.

validate vs build

imperal validate . checks correctness — it does not write any files. imperal build . regenerates imperal.json from your code. Run build after every structural change (new tool, new panel, version bump), then commit the updated imperal.json.


py_compile check

Run this as a mandatory pre-flight before any validation or deploy step. It catches syntax errors and import-time NameErrors instantly, without loading the full SDK:

python3 -m py_compile app.py handlers_crud.py main.py

Exit code 0 means no syntax errors. Any non-zero exit prints the offending file and line number.

For all Python files in one pass:

python3 -m py_compile *.py

pyright strict check

Install pyright into your virtual environment:

pip install pyright

Run it against your extension:

pyright app.py handlers_crud.py main.py

For stricter checking, add a pyrightconfig.json:

pyrightconfig.json
{
  "pythonVersion": "3.11",
  "typeCheckingMode": "basic",
  "reportMissingImports": true,
  "reportMissingModuleSource": false
}

Then run:

pyright

ctx type annotation

Handler functions receive ctx: Context at runtime, but typing ctx as object in code samples avoids circular import issues in unit test files. In production code, import and annotate with from imperal_sdk import Context.


Quick test script

You do not need a running web-kernel to test handlers. The SDK ships a testing module with in-memory mock implementations of every ctx.* sub-client.

Create tests/test_handlers_crud.py:

tests/test_handlers_crud.py
"""Tests for my-extension handlers."""
from __future__ import annotations
from pydantic import BaseModel, Field
from imperal_sdk import Extension, ActionResult
from imperal_sdk.chat import ChatExtension
from imperal_sdk.testing import MockContext

# Inline the extension + handler — no cross-module import needed in tests
ext = Extension(
    "my-extension",
    version="1.0.0",
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    icon="icon.svg",
    actions_explicit=True,
)
chat = ChatExtension(ext, tool_name="my_extension_chat", description="My Extension chat assistant")


class GreetParams(BaseModel):
    name: str = Field(description="Name of the person to greet")


@chat.function(
    "greet",
    description="Send a friendly greeting to a person by their name",
    action_type="read",
)
async def greet(ctx: object, params: GreetParams) -> ActionResult:
    """Greet a user by name."""
    message = f"Hello, {params.name}! Welcome to My Extension."
    return ActionResult.success(
        data={"message": message, "name": params.name},
        summary=f"Greeted {params.name}",
    )


async def test_greet_returns_success() -> None:
    ctx = MockContext(user_id="user-1", role="user")
    params = GreetParams(name="Alice")
    result = await greet(ctx, params)

    assert result.status == "success"
    assert result.data["name"] == "Alice"
    assert "Alice" in result.data["message"]
    assert "Alice" in result.summary


async def test_greet_data_shape() -> None:
    ctx = MockContext(user_id="user-2", role="user")
    params = GreetParams(name="Bob")
    result = await greet(ctx, params)

    assert isinstance(result.data, dict)
    assert "message" in result.data
    assert "name" in result.data

Add pyproject.toml to enable async tests project-wide:

pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"

Run the tests:

pytest tests/ -v

Run the extension locally

The SDK does not ship a full local web-kernel server. Extensions are deployed to the Imperal platform and invoked by the ICNLI Worker. The local development cycle is:

  1. Write — edit handlers, panels, skeleton
  2. Compilepython3 -m py_compile *.py
  3. Validateimperal validate .
  4. Testpytest tests/ -v
  5. Build manifestimperal build . (regenerates imperal.json)
  6. Commitgit add . && git commit -m "feat(my-extension): ..."
  7. Deploy — Developer Portal → Deploy tab → redeploy from git URL

The imperal dev command loads your extension and prints a summary of registered tools, signals, and schedules. It is useful for confirming that all modules imported cleanly and decorators registered:

imperal dev
# Extension: my-extension v1.0.0
# Tools: my_extension_chat, skeleton_refresh_*
# Signals: (none)
# Schedules: (none)
# Dev server ready. Ctrl+C to stop.

No hot-reload

imperal dev does not watch for file changes. After editing code, stop the process (Ctrl+C) and run it again. For iterative panel development, the faster loop is: edit → imperal build . → redeploy via Developer Portal → test in the UI.


Watch mode / iteration cycle

There is no watch-mode daemon in the SDK. The recommended iteration loop for chat functions:

edit handler → py_compile → validate → pytest → done

For panel changes (which require a running platform to render):

edit panel → py_compile → validate → imperal build . → commit → redeploy → test in UI

To speed up manifest regeneration during iteration, imperal build . is fast (sub-second) — run it after every structural change and commit the result.


Common errors and fixes

V1 — invalid app_id

ERROR  [V1] app_id 'My Extension' does not match pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$
Fix: Use only lowercase letters, digits, and hyphens (e.g. 'my-extension')

Fix: Extension("my-extension", ...) — lowercase, hyphens only, no spaces or underscores.


V14 / V15 — missing or short display_name / description

ERROR  [V14] description must be ≥40 chars and must not equal app_id
ERROR  [V15] display_name must be ≥3 chars and must not equal app_id

Fix:

ext = Extension(
    "my-extension",
    display_name="My Extension",
    description="A sample extension that greets users by name with a friendly message.",
    # ... other kwargs ...
)

V16 — @chat.function description too short

ERROR  [V16] function 'greet' description must be ≥20 chars

Fix: Use a full sentence: description="Send a friendly greeting to a person by their name".


V17 — @chat.function params not a Pydantic BaseModel

ERROR  [V17] function 'greet' has no Pydantic BaseModel parameter

Fix: Define a BaseModel subclass at module scope (not inside the function) and annotate the second parameter:

class GreetParams(BaseModel):
    name: str

@chat.function("greet", description="...", action_type="read")
async def greet(ctx, params: GreetParams) -> ActionResult:
    ...

Note: Models defined inside functions (function-local scope) fail the auto-detection check — func.__globals__ cannot find them. Always define Pydantic params at module level.


V19 — write function missing chain_callable

ERROR  [V19] write function 'create_item' must have chain_callable=True

Fix:

@chat.function(
    "create_item",
    description="Create a new item in the collection with the given title",
    action_type="write",
    chain_callable=True,
    effects=["create:item"],
)
async def create_item(ctx, params: CreateItemParams) -> ActionResult:
    ...

V21 — missing or invalid SVG icon

ERROR  [V21] icon file 'icon.svg' not found
ERROR  [V21] SVG missing viewBox attribute

Fix: Create icon.svg with a valid <svg> root and viewBox attribute. See the minimal placeholder above.


ImportError: No main.py found

Error: No main.py found with 'ext' Extension object.

imperal validate and imperal build both import from main, not app.py. Ensure main.py exists in the extension directory, sets up sys.path, and imports ext from app.py.


py_compile: SyntaxError

  File "handlers_crud.py", line 14
    async def greet(ctx params: GreetParams) -> ActionResult:
                        ^
SyntaxError: invalid syntax

Fix the syntax at the reported line. Run python3 -m py_compile again to confirm the fix.


pyright: reportMissingImports

error: Import "handlers_crud" could not be resolved

This is expected if you run pyright from the workspace root rather than the extension directory. Either cd my-extension && pyright or configure pythonPath in pyrightconfig.json to point at the extension directory.


Where to go next

On this page