Skip to content

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:

name = "My Extension"
version = "1.0.0"

[agent]
entry = "main.py"

An extension with a tab needs a name, a version, and an HTML entry file:

name = "Device Dashboard"
version = "1.0.0"

[app]
tab = "dist/tab/index.html"

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"]
name = "Device Dashboard"
version = "1.0.0"
description = "Custom dashboard for device health monitoring"
author = "Acme Corporation"
icon = "assets/icon.svg"
readme = "README.md"

[zelos]
version = ">=26.0.10"

[app]
tab = "dist/tab/index.html"

[package]
paths = ["dist"]

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:

[zelos]
version = ">=26.0.10"

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.

[app]
tab = "dist/tab/index.html"

The short form is the HTML entry file inside the package. The table form also sets the source:

[app]
tab = { entry = "https://dashboard.zeloscloud.io/", source = "external" }
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 accepts or companions is a non-empty string of at most 120 characters.
  • One panel cannot list the same prefix in both accepts and companions.
  • 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, icon and options_schema follows 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.

[agent.env]
LOG_LEVEL = "debug"
DEVICE_PORT = "/dev/ttyUSB0"

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]:

[config]
schema = "config.schema.json"

Declare it whenever your extension has a configuration schema, even at the default path:

  • The agent validates configuration against config.schema.json in the code directory when the file exists, with or without [config]. A schema path you declare must exist, or start fails.
  • zelos extensions package adds 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:

[package]
paths = ["main.py", "my_extension"]
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 with source = "external" is exempt.
  • When workdir is 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:

[extras.usb]
description = "USB adapters"
packages = ["pyusb>=1.2", "libusb-package"]
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:

[extras]
usb = ["pyusb>=1.2", "libusb-package"]

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 when requirements names another file, such as requirements.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 -. So usb_adapter and USB-Adapter are 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 package refuses a mismatch. The error names the extra and both package lists.
  • zelos extensions install-local installs 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:

[host]
type = "app"

[host.app]
kind = "web_app"
source = "package"
entry = "dist/index.html"

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.