Extension Manifest Reference¶
Every extension has an extension.toml file at the root of its project. The manifest names the extension, says how Zelos runs it, and says what goes into its release archive.
Zelos parses and validates the manifest when you install an extension, install it locally, or package it. The marketplace parses it again when it ingests a release. A manifest that fails validation stops each of these steps with an error that names the problem.
Minimal Manifests¶
A manifest declares what the extension contributes: an agent runtime in [agent], a tab and panels in [app], or both.
An extension with an agent runtime needs a name, a version, and an entry point:
An extension with a tab needs a name, a version, and an HTML entry file:
An extension with a panel needs one [[app.panels]] entry:
name = "Probe"
version = "1.0.0"
[[app.panels]]
id = "view"
name = "Probe View"
entry = "dist/panels/view.html"
These manifests install. To package a release archive, also add a [package] section.
Complete Examples¶
name = "Sensor Kit"
version = "0.3.0"
description = "Capture, decode and view sensor telemetry"
author = "Acme Corporation"
repository = "https://github.com/acme/sensor-kit"
icon = "assets/icon.png"
readme = "README.md"
keywords = ["sensor", "telemetry"]
categories = ["Industrial"]
[zelos]
version = ">=26.0.10"
[config]
schema = "config.schema.json"
[agent]
runtime = "python"
entry = "main.py"
python_version = "3.11"
[agent.env]
LOG_LEVEL = "info"
[agent.stop]
grace_seconds = 10
[[app.panels]]
id = "telemetry"
name = "Telemetry"
description = "Latest sensor readings"
icon = "assets/telemetry.svg"
entry = "dist/panels/telemetry.html"
accepts = ["acme.sensor.reading."]
companions = ["acme.sensor.stats."]
binds = "any"
default_height = 2
options_schema = "dist/panels/telemetry.options.json"
ai_guidance = "Latest sensor readings: one latched row per sensor."
[package]
paths = ["main.py", "sensor_kit", "pyproject.toml", "uv.lock", "dist", "assets"]
Top-Level Fields¶
| Field | Required | Default | Rules |
|---|---|---|---|
name |
Yes | - | Display name. Must not be empty or whitespace. At most 60 characters. |
version |
Yes | - | Strict SemVer, for example 1.2.3 or 2.0.0-beta.1. |
[agent] |
One of [agent] and [app] |
- | The agent runtime. See [agent]. |
[app] |
One of [agent] and [app] |
- | The tab and the panels. See [app]. |
description |
No | - | Summary shown in the marketplace and the app. |
author |
No | - | Author or organization. |
repository |
No | - | Project URL. Zelos accepts any value that parses as a URI. |
homepage |
No | - | Project website. Zelos accepts any value that parses as a URI. |
icon |
No | - | Relative path to an icon file. |
readme |
No | - | Relative path to a Markdown README. |
keywords |
No | [] |
Search terms. At most 20. No empty strings. |
categories |
No | [] |
Marketplace categories, for example "IoT". At most 10. No empty strings. |
[zelos] |
No | >=26.0.10 |
Zelos version requirement. See Version Compatibility. |
[config] |
No | - | Configuration schema for the agent runtime. Needs [agent]. See [config]. |
[package] |
For packaging | - | Release archive contents. See [package]. |
[extras] |
No | {} |
Optional dependency groups a user picks at install. Needs [agent]. See [extras]. |
A manifest declares [agent], [app], or both. A manifest with neither fails with Manifest must declare [agent], [app], or both.
Do not set an id field. Zelos derives the extension ID from the GitHub repository for marketplace installs and from name for local installs. See Extension Identity.
Path Rules¶
Every path in the manifest (entry, icon, readme, config.schema, requirements, workdir, the tab entry, each panel's entry, icon and options_schema, and each [package] path) must follow these rules:
- The path is relative to the project root.
- The path is not empty or blank.
- Segments are separated by
/. A backslash fails on every OS. - No absolute paths, and no drive prefix such as
C:. - No
..segments. - No segment starts with
., so hidden files and directories are not allowed.
When you install or package, Zelos also checks that these files exist and that workdir exists as a directory. A tab with source = "external" skips the check of its entry. A panel's files live in the built web project, so build it before you package or install locally.
Version Compatibility¶
The [zelos] table sets the Zelos versions your extension supports:
The value is a SemVer range:
| Range | Meaning |
|---|---|
>=26.0.10 |
26.0.10 and later |
>=26.0.10, <27.0.0 |
26.0.10 and later 26.x releases |
~26.1.0 |
26.1.0 and later 26.1.x releases |
Join comparators with commas only. ||, hyphen ranges such as 26.0.3 - 26.1.0, and comparators separated by spaces fail with Invalid version: ….
When you omit [zelos], the default is >=26.0.10. Zelos 26.0.10 is the first release that reads [agent] and [app], and the first that renders extension panels.
A range you set must also start at 26.0.10 or later. >=26.0.10, >26.0.9, ^26.0.10, ~26.1.0 and 26.1.* pass. *, ^26, <27, 26.0.* and >26.0.9-rc.1 fail with The [agent]/[app] form needs Zelos 26.0.10 or later, but [zelos] version "<range>" allows older versions.
Zelos checks the range when you install the extension, from the marketplace or locally. An install on a Zelos version outside the range fails with Extension requires Zelos version matching '<range>', but current version is <version>. A pre-release Zelos build is compared as its release version, so 26.1.0-beta.2 counts as 26.1.0.
[agent]¶
[agent] declares a Python process that runs under the Zelos Agent:
[agent]
runtime = "python"
entry = "main.py"
python_version = "3.11"
requirements = "requirements.txt"
workdir = "."
targets = ["linux-x86_64", "darwin-arm64"]
[agent.env]
LOG_LEVEL = "info"
[agent.stop]
grace_seconds = 10
| Field | Required | Default | Description |
|---|---|---|---|
runtime |
No | "python" |
Runtime kind. "python" is the only value. |
entry |
Yes | - | Python entry point, for example "main.py". |
python_version |
No | "3.11" |
Python version in major.minor form. The zelos-sdk package needs 3.10 or later. See Python Environment. |
requirements |
No | - | Dependency file override, for example "requirements.txt". Without it, Zelos installs from uv.lock when present, otherwise from pyproject.toml. |
workdir |
No | "." |
Working directory for the process, relative to the project root. |
targets |
No | [] |
Platforms the runtime supports. See Platform Targets. |
env |
No | {} |
Environment variables for the process. See Environment Variables. |
stop.grace_seconds |
No | 10 |
Seconds to wait after a stop request before Zelos force-kills the process. 1 to 300. See Stop and Shutdown. |
[agent] cannot be combined with the older [host] or [runtime] tables, or with a top-level workdir, targets, env or [stop]. Such a manifest fails with [agent]/[app] cannot be combined with [host], [runtime] or top-level agent fields.
[app]¶
[app] declares what the extension shows in the desktop app: a tab, panels, or both. It must declare at least one, or it fails with [app] must declare tab or at least one [[app.panels]] entry. The app parts have no process, so targets, env, and stop do not apply to them.
tab¶
A tab opens in the sidebar rail's ⌄ menu and in its own tab in the app.
The short form is the HTML entry file inside the package. The table form also sets the source:
| Field | Required | Default | Description |
|---|---|---|---|
entry |
Yes | - | With source = "package", the HTML entry file inside the package. With source = "external", an https:// URL. |
source |
No | "package" |
"package" loads entry from the installed package. "external" loads an https:// URL on zeloscloud.io or one of its subdomains. Other hosts are not allowed. |
An extension with an external tab cannot have panels: [[app.panels]] requires source = "package".
[[app.panels]]¶
Each [[app.panels]] entry declares one panel type that users add to a layout. An extension can declare several. For how to build one, see Develop Panel Extensions.
[[app.panels]]
id = "events"
name = "Events"
description = "Recorded sensor events"
icon = "assets/events.svg"
entry = "dist/panels/events.html"
accepts = ["acme.sensor.event."]
companions = ["acme.sensor.stats."]
binds = "accepted"
default_height = 2
live_only = false
options_schema = "dist/panels/events.options.json"
ai_guidance = "Recorded sensor events: a list with time, sensor, kind and detail."
| Field | Required | Default | Description |
|---|---|---|---|
id |
Yes | - | Lowercase letters, digits and single hyphens, 1 to 40 characters, unique in the manifest. Saved layouts store it, so keep it stable across releases. |
name |
Yes | - | The panel's title in the layout, in Add Panel, and in the command palette. 1 to 24 characters, so it fits one line of an Add Panel card. |
description |
No | - | The two lines under the name in Add Panel. At most 60 characters. |
icon |
No | - | Relative path to an SVG glyph, such as a Lucide icon. The app draws its shape in the text color, like the built-in panel icons, so color in the file is ignored. Without it, the panel shows a puzzle-piece icon. |
entry |
Yes | - | The panel's HTML file inside the package. |
accepts |
No | [] |
Event-type prefixes that a drop routes to this panel, for example "acme.sensor.reading.". |
companions |
No | [] |
Event-type prefixes that join this panel when the same drop holds an accepted signal. Dragged alone, they plot. |
binds |
No | "any" |
What a drop onto an existing panel keeps. "any" keeps every dropped signal. "accepted" keeps only signals that match the panel's accepts or companions. |
default_height |
No | 1 |
Height of a new panel, in layout rows. 0.5 to 4. |
live_only |
No | false |
true shows the panel only in live workspaces. |
options_schema |
No | - | A JSON Schema for the panel's options. The panel's Edit sheet renders it as a form in the Config tab. |
ai_guidance |
No | - | One line that tells the AI when to pick this panel. At most 200 characters. |
The panel rules:
- A manifest declares at most 16 panels.
- A prefix in
acceptsorcompanionsis a non-empty string of at most 120 characters. - One panel cannot list the same prefix in both
acceptsandcompanions. - Two panels of one manifest cannot list the same prefix.
- When several panels match a dropped signal, the longest matching prefix wins. Only panels of different extensions can list equal prefixes, and between them the first extension by ID wins.
- Each panel's
entry,iconandoptions_schemafollows the path rules, and a[package]path must cover each one. - What the manifest contributes (the agent runtime and its targets, the tab, and the panels) takes at most 64 KiB as JSON. The panels' prefix lists and text make up nearly all of it.
Platform Targets¶
targets lists the platforms the agent runtime supports. An empty list, or no targets key, means every platform.
| Target | OS | Architecture |
|---|---|---|
linux-x86_64 |
Linux | x86-64 |
linux-aarch64 |
Linux | ARM64, for example Raspberry Pi and embedded Linux |
darwin-arm64 |
macOS | Apple Silicon |
darwin-x86_64 |
macOS | Intel |
windows-x86_64 |
Windows | x86-64 |
Zelos rejects any other value. An install does not read the list, so the extension installs on any platform, and its tab and panels work there. On a platform the list leaves out, Zelos does not prepare the agent runtime, and Start fails with This extension's agent runs on <targets>; this machine is <platform>. Zelos picks the release archive for a machine from the archive's file name. To ship different archives per platform, see Platform-Specific Archives.
Environment Variables¶
env sets variables for the extension process. Values are strings.
Names that start with ZELOS_ are reserved. A manifest that sets one fails with Environment variable '<name>' uses reserved prefix 'ZELOS_'. For the variables Zelos sets and passes through, see Process Environment.
[config]¶
[config] points at the JSON Schema that defines the agent's configuration form. Only the agent reads its configuration, so [config] needs an [agent]:
Declare it whenever your extension has a configuration schema, even at the default path:
- The agent validates configuration against
config.schema.jsonin the code directory when the file exists, with or without[config]. Aschemapath you declare must exist, or start fails. zelos extensions packageadds the schema file to the archive only when[config]names it, or when it falls under a[package]path.- The marketplace validates the schema only when
[config]names it.
For the schema format and form widgets, see Extension Configuration.
[package]¶
[package] lists what goes into the release archive:
| Field | Required | Description |
|---|---|---|
paths |
Yes | Files and directories to include. A directory includes everything under it. |
zelos extensions package refuses to run without this section: Packaging requires a [package] section in extension.toml. Add [package] with explicit paths to define the package boundary. Installing locally does not need it.
Rules for paths:
- The list must not be empty.
- No empty or blank entries, no
".", and no glob characters (*,?,[,{). - Each entry follows the path rules.
- One entry must cover the agent
entry, the tab entry, and each panel's files. A tab withsource = "external"is exempt. - When
workdiris not".", one entry must cover it.
For what else goes into the archive and how to preview it, see Package and Publish Extensions.
[extras]¶
[extras] declares optional Python dependency groups for the agent runtime. A user picks the ones they need at install, for example the drivers for one kind of adapter. Each extra is a table with a description and its packages:
| Field | Required | Description |
|---|---|---|
description |
For new releases | What the extra enables, in plain words. One line of at most 60 characters. The app shows it next to the extra's checkbox, and the AI reads it to tell when a user needs the extra. |
packages |
Yes | The Python requirements the extra installs, for example "pyusb>=1.2". At least one, and no blank entries. |
Packaging needs a description on each extra. zelos extensions package refuses an extra without one with Extra "<name>" needs a description: …. The shorthand form, a list of packages with no description, still parses, so a release you already published keeps installing:
The app shows such an extra as Adds pyusb>=1.2, libusb-package in place of a description.
Rules for Extras¶
[extras]needs an[agent]. Extras install into the agent runtime's Python environment.- The agent's dependencies live in
pyproject.toml. uv installs extras only from there, so a manifest with extras fails whenrequirementsnames another file, such asrequirements.txt. - A name follows the PEP 508 rule for extra names. It starts and ends with a letter or digit, and uses only letters, digits,
.,-and_. - Zelos compares names the way uv does. Case does not count, and each run of
.,-and_counts as one-. Sousb_adapterandUSB-Adapterare one name, and a manifest that lists both fails.
Match pyproject.toml¶
uv installs an extra from pyproject.toml, and the manifest tells the app, the AI and the marketplace what the extension offers. So each extra in the manifest must also appear in pyproject.toml under [project.optional-dependencies], with the same packages. The order of the packages does not matter.
zelos extensions packagerefuses a mismatch. The error names the extra and both package lists.zelos extensions install-localinstalls anyway and prints the same message as a warning.
pyproject.toml can define more groups than the manifest lists. A group the manifest leaves out stays developer-only: users never see it, and Zelos never installs it. Use one for your test tools, for example a dev group.
Example¶
extension.toml:
name = "Sensor Kit"
version = "0.4.0"
[agent]
entry = "main.py"
[extras.usb]
description = "USB adapters"
packages = ["pyusb>=1.2", "libusb-package"]
[package]
paths = ["main.py", "sensor_kit", "pyproject.toml", "uv.lock"]
pyproject.toml:
[project]
name = "sensor-kit"
version = "0.4.0"
requires-python = ">=3.11"
dependencies = ["zelos-sdk"]
[project.optional-dependencies]
usb = ["pyusb>=1.2", "libusb-package"]
dev = ["pytest"]
Users can choose usb. The dev group is not in the manifest, so it stays developer-only.
A first install has no extras unless the user chooses them. A reinstall or update keeps the installed ones. For how users choose extras in the app and the CLI, see Choosing What to Install.
Defaults¶
| Field | Default |
|---|---|
zelos.version |
>=26.0.10 |
agent.runtime |
"python" |
agent.python_version |
"3.11" |
agent.workdir |
"." |
agent.stop.grace_seconds |
10 |
agent.targets |
[] (all platforms) |
agent.env |
{} |
app.tab source |
"package" |
app.panels[].binds |
"any" |
app.panels[].default_height |
1 |
app.panels[].live_only |
false |
keywords, categories |
[] |
Older Manifests¶
Manifests written before Zelos 26.0.10 use [host] or [runtime] in place of [agent] and [app]. Zelos still installs and runs them, so an extension you already published keeps working.
Packaging needs [agent] and [app]. zelos extensions package refuses an older manifest with extension.toml uses the old [host]/[runtime] form. Move to [agent] and [app]; see docs.zeloscloud.io/sdk/how-to/extension-manifest/. A release in the new form needs Zelos 26.0.10 or later; an older app that tries to install it is told to update Zelos. Move your manifest before your next release:
| Older form | Current form |
|---|---|
[host] type = "agent" and [host.agent] |
[agent] |
[host.agent.env], [host.agent.stop] |
[agent.env], [agent.stop] |
[host] type = "app" and [host.app] entry = "dist/index.html" |
[app] tab = "dist/index.html" |
[host.app] source = "external" |
[app] tab = { entry = "https://…", source = "external" } |
[runtime] with top-level workdir, targets, [env], [stop] |
[agent] with the same fields inside it |
An older manifest cannot declare panels. Without [zelos], it defaults to >=25.0.20 with an agent runtime only, and to >=26.0.3 with a tab.
The older agent form:
[host]
type = "agent"
[host.agent]
entry = "main.py"
python_version = "3.11"
targets = ["linux-x86_64"]
[host.agent.env]
LOG_LEVEL = "info"
The older app form:
The oldest agent form, [runtime], keeps workdir, targets, [env] and [stop] at the top level:
workdir = "."
targets = []
[runtime]
type = "python"
entry = "main.py"
python_version = "3.11"
[env]
LOG_LEVEL = "info"
[stop]
grace_seconds = 10
With [host] type = "agent", a top-level workdir, targets, env or [stop] set away from its default is an error: Top-level fields [...] conflict with [host.agent]; move them inside [host.agent]. With [host] type = "app", Zelos ignores those fields. A manifest with both [host] and [runtime] is an error.
Common Validation Errors¶
| Error | Fix |
|---|---|
Manifest must declare [agent], [app], or both |
Add an [agent] table, an [app] table, or both. |
[agent]/[app] cannot be combined with [host], [runtime] or top-level agent fields |
Move the older tables and fields into [agent]. See Older Manifests. |
[app] must declare tab or at least one [[app.panels]] entry |
Add tab = "…" or an [[app.panels]] entry. |
[[app.panels]] requires source = "package" |
Ship the tab in the package, or move the panels to another extension. |
The [agent]/[app] form needs Zelos 26.0.10 or later, but [zelos] version "<range>" allows older versions |
Set [zelos] version = ">=26.0.10" or stricter, or remove it. |
Panel id "<id>" is invalid; use lowercase letters, digits and single hyphens |
Rename the panel id, for example telemetry. |
Duplicate panel id "<id>" |
Give each panel its own id. |
Manifest declares <count> panels; the limit is 16 |
Keep 16 panels or fewer, or move some to another extension. |
Panel "<id>" default_height must be between 0.5 and 4.0 |
Pick a height from 0.5 to 4. |
[config] needs an [agent]: only the agent reads its configuration |
Remove [config], or add the [agent] that reads it. |
[extras] needs an [agent]: … |
Remove [extras], or add the [agent] it installs into. |
[extras] needs the agent's dependencies in pyproject.toml; uv cannot install extras from <file> |
Move the agent's dependencies to pyproject.toml and remove requirements. |
Extra "<name>" must start and end with a letter or digit and use only letters, digits, '.', '-' and '_' |
Rename the extra, for example usb. |
Extras "<first>" and "<second>" are the same name to uv; keep one |
Remove one of the two, or give them distinct names. |
Extra "<name>" lists no packages or Extra "<name>" has an empty package requirement |
List at least one package, and remove blank entries. |
Extra "<name>" description must not be blank or Extra "<name>" description must be one line of at most 60 characters |
Write one line of at most 60 characters on what the extra enables. |
Extra "<name>" needs a description: … |
Write the extra as [extras.<name>] with description and packages. |
Extra "<name>" is not in pyproject.toml [project.optional-dependencies], so uv cannot install it |
Add the group to pyproject.toml. See Match pyproject.toml. |
Extra "<name>" lists <packages> in extension.toml but <packages> in pyproject.toml; make them match |
Make both lists name the same packages. |
Panel "<id>" icon must be an SVG file, a single-color glyph such as a Lucide icon |
Point icon at an .svg file, such as one from Lucide. |
Panel "<id>" path '<path>' is not covered by [package].paths |
Add the panel's directory, usually dist, to [package].paths. |
Panel "<id>" path '<path>': <reason> |
Fix the panel's entry, icon or options_schema path. The reason is one of the path errors below. |
Contributions take <n> bytes as JSON; the limit is 65536 |
Shorten the panels' accepts and companions lists, descriptions or ai_guidance. |
a path string, or a table with `entry` and optional `source` |
Write tab = "dist/index.html" or tab = { entry = "…", source = "external" }. |
Display name must be at most 60 characters |
Shorten name. |
Python version must be in major.minor format (e.g., '3.11') |
Write python_version = "3.12", not "3.12.1" or "3.". |
Repository '<value>' must be a valid URI |
Use a full URI, for example https://github.com/org/repo. |
Environment variable '<name>' uses reserved prefix 'ZELOS_' |
Rename the variable. |
stop.grace_seconds must be between 1 and 300 seconds |
Pick a value from 1 to 300. |
Entry point '<entry>' is not covered by [package].paths |
Add the entry file or its directory to [package].paths. |
path contains hidden segments |
Rename paths that start with .. |
path contains parent directory references |
Remove .. from the path. |
path contains backslashes; use forward slashes |
Write the path with /, for example dist/index.html. |
path contains absolute path components |
Make the path relative to the project root, with no leading / or drive prefix. |
Path must not be empty |
Fill in the path, or delete an optional field. |