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.functionthat 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, andpyrightall exit 0
Prerequisites
| Tool | Minimum | Recommended |
|---|---|---|
| Python | 3.11 | 3.12 or 3.13 |
| pip | 24.0 | latest |
| pyright | any | latest (pip install pyright) |
| OS | macOS / 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 --versionTo install a specific version with pyenv:
pyenv install 3.12
pyenv local 3.12Install 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 PowerShellInstall 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.
Recommended file layout
Production extensions follow a consistent structure that separates concerns by file. The notes extension is the canonical example:
| File | Purpose |
|---|---|
main.py | Entrypoint — 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.py | Extension(...) 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.svg | Extension icon — must be valid SVG with viewBox, ≤100 KB |
imperal.json | Generated manifest — never hand-edit; regenerate with imperal build . |
requirements.txt | List 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__.pyapp.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
"""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
"""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: F401requirements.txt
imperal-sdkicon.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:
<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.pyExit 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 *.pypyright strict check
Install pyright into your virtual environment:
pip install pyrightRun it against your extension:
pyright app.py handlers_crud.py main.pyFor stricter checking, add a pyrightconfig.json:
{
"pythonVersion": "3.11",
"typeCheckingMode": "basic",
"reportMissingImports": true,
"reportMissingModuleSource": false
}Then run:
pyrightctx 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 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.dataAdd pyproject.toml to enable async tests project-wide:
[tool.pytest.ini_options]
asyncio_mode = "auto"Run the tests:
pytest tests/ -vRun 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:
- Write — edit handlers, panels, skeleton
- Compile —
python3 -m py_compile *.py - Validate —
imperal validate . - Test —
pytest tests/ -v - Build manifest —
imperal build .(regeneratesimperal.json) - Commit —
git add . && git commit -m "feat(my-extension): ..." - 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 → doneFor panel changes (which require a running platform to render):
edit panel → py_compile → validate → imperal build . → commit → redeploy → test in UITo 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_idFix:
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 charsFix: 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 parameterFix: 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=TrueFix:
@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 attributeFix: 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 syntaxFix 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 resolvedThis 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
Testing extensions
MockContext, MockStore, MockHTTP — full unit and integration testing patterns
Building extensions
Panels, skeletons, chat functions — end-to-end guide
@chat.function reference
All kwargs: action_type, chain_callable, effects, id_projection
Validator reference
All V1–V24 rules, levels, and fixes in one table