Skip to content

How to Develop Extensions

Overview

Extensions are installable applications built with the Zelos SDK that make it easy to connect devices and decode protocols. They transform complex hardware integrations into one-click installs - users can immediately stream, visualize, and control their systems without writing integration code.

Quick Start

Jump to Creating Your First Extension if you want to dive right in, or continue reading for a comprehensive understanding of the extension system.

Choose an Extension Type

Use the dedicated host-specific guides for the fastest path:

This page remains the shared reference for extension concepts, packaging, manifest rules, runtime details, and publishing.

App-host extensions (desktop APP tabs)

Zelos also supports app-host extensions: sandboxed web UIs loaded by the desktop app with zelos-app:// and declared with [host] type = "app" in extension.toml.

  • Create: zelos extensions create --type app <name> or use Create Extension in the desktop app
  • Standalone dev: npm install && npm run dev in the generated project uses the SDK MockBridge
  • Integrated dev: zelos extensions install-local <extension-dir> plus vite build --watch, then reload or reopen the APP tab
  • Packaging: npm run package in the generated project delegates to zelos extensions package
  • Bridge surface: initial handshake-ack.snapshot plus host-pushed theme.changed and workspace.changed

See:

  • https://github.com/zeloscloud/zelos-extension-templates/tree/main/app/react
  • @zelos-cloud/app-extension-sdk

Why Build Extensions?

Package your integration work into reusable extensions:

  • Share protocol decoders with the community (CAN, Modbus, custom binary formats)
  • Distribute hardware drivers for test equipment, sensors, and actuators
  • Provide workflows like automated test sequences and calibration routines
  • Build integrations for cloud services, databases, and external APIs
  • Accelerate adoption - users install and configure instead of writing code

What Can Extensions Do?

Extensions use the Zelos SDK to:

Data Acquisition

  • Decode bus protocols (CAN/CAN-FD, Modbus, MQTT, OPC-UA, Ethernet/Protobuf, Serial)
  • Interface with test equipment (National Instruments, Opal-RT HIL boxes, oscilloscopes)
  • Stream from custom hardware and sensors
  • Parse proprietary file formats and logs

Device Control

  • Execute commands on hardware via actions
  • Automate test sequences
  • Control parameters in real-time
  • Trigger hardware events from the app

Data Processing

  • Transform and filter data streams
  • Implement custom algorithms
  • Aggregate signals from multiple sources
  • Generate derived metrics

Core Concepts

Extension Identity

Zelos automatically derives extension IDs - you don't specify an internal id field:

Marketplace extensions: owner.repo format - Derived from your GitHub repository (e.g., acme/canbus-decoderacme.canbus-decoder) - Automatically lowercased and normalized

Local extensions: local.<foldername> format - Derived from your project directory name - Example: ~/my-sensor-monitor/local.my-sensor-monitor

ID Validation Rules

Extension IDs must follow these rules:

  1. Format: publisher.name (dot-separated segments)
  2. Character rules:
  3. Lowercase letters, digits, and hyphens only
  4. No uppercase letters
  5. No leading or trailing hyphens in segments
  6. No empty segments
  7. Examples:
  8. acme.canbus-decoder
  9. john-doe.mqtt-bridge
  10. local.sensor-01
  11. Acme.CANBus (uppercase)
  12. acme.-canbus (leading hyphen)
  13. acme..canbus (empty segment)

Local Extension Normalization

For local installations, Zelos normalizes your folder name:

  1. Convert to lowercase
  2. Replace non-alphanumeric characters with hyphens
  3. Collapse multiple consecutive hyphens (---)
  4. Trim leading/trailing hyphens
  5. Prefix with local.

Examples:

Sensor_Monitor/     → local.sensor-monitor
My Extension v2/    → local.my-extension-v2
CAN__BUS___Tool/    → local.can-bus-tool

Runtime Environment

Runtime depends on the host type:

  • Agent extensions run as Python applications using Python 3.10 through 3.14 (major.minor format). UV automatically manages Python downloads and virtual environments.
  • App extensions run as sandboxed web projects inside desktop APP tabs and talk to Zelos through the typed bridge SDK.

Security Model

Security depends on the host type:

Agent extensions operate with:

  • Process isolation - Each runs in its own process
  • File system boundaries - Confined to their installation directory
  • Minimal environment - Limited environment variables for security
  • Graceful lifecycle - Proper startup and shutdown handling
  • Network - No restrictions

App extensions operate with:

  • Iframe sandbox - Sandboxed with allow-scripts allow-same-origin; no form submissions or popups
  • CSP enforcement - Cannot make external network requests (fetch, XMLHttpRequest, or form submissions to http:/https: URLs are blocked)
  • Origin isolation - Each extension runs under its own zelos-app://extensionId origin, isolated from the host and other extensions
  • File system boundaries - Confined to the packaged assets served by zelos-app://

Installation Layout

When an extension installs, the agent creates a dedicated container directory at <data>/extensions/<id>/<version>/:

  • Marketplace installs unpack into a code/ subdirectory. The agent stores runtime files (.venv, .cache, config.json, logs, etc.) alongside the code but outside the tree you shipped, then marks code/ as read-only and records an integrity hash. Any manual edits invalidate that hash and the supervisor refuses to start the extension.
  • Local installs reference your working copy via a .dev_source marker file. The agent reads code directly from your project directory, while runtime artifacts (virtual environment, cache, logs) live in the container directory instead of polluting your repo.

Local Extension Layout

Local installs store a reference to your source directory rather than copying files. Runtime artifacts (virtual environment, cache, logs) are kept separately in the Zelos data directory, so your project stays clean.

Creating Your First Extension

The easiest way to create an extension is using the built-in creator in the Zelos App:

  1. Open the Zelos App and navigate to Extensions view
  2. Click the menu in the Marketplace header
  3. Select Create Extension
  4. Fill in your extension details:
  5. Extension Name - Project name in kebab-case (e.g., "sensor-monitor")
  6. Description - Brief summary of what your extension does
  7. Click Create Extension
  8. The app generates the extension and installs it locally as local.{name}

Alternative: Use the CLI directly:

zelos extensions create my-new-extension
zelos extensions create my-app-extension --type app

Both methods generate a complete project, but the generated tooling depends on the selected host type.

Generated Agent Project

Agent projects include the Python/process toolchain:

  • main.py, pyproject.toml, tests, and a working SDK example
  • developer commands via just
  • Python tooling such as uv, ruff, and pytest
  • CI/release workflows and project docs

Typical agent workflow:

cd path/to/my-agent-extension
just install
just check
just test
just dev

Generated App Project

App projects include the web UI toolchain:

  • src/, package.json, Vite config, and a working APP-tab example
  • the published SDK package @zelos-cloud/app-extension-sdk
  • npm-based build and dev commands
  • CI/release workflows and project docs

Typical app workflow:

cd path/to/my-app-extension
npm install
npm run dev
npm run build

App Workflow

When you create an extension via the app, it attempts a local install automatically. After you rebuild app assets, reopen or reload the APP tab to pick up the latest output.

Publishing to Marketplace

Initial Setup:

  1. Create a repository on GitHub (for example, your-org/my-extension)
  2. Push your code:
    git remote add origin https://github.com/your-org/my-extension.git
    git add .
    git commit -m "Initial commit"
    git push -u origin main
    

Build the release artifact:

  • Agent project: use the generated Python workflow and release commands from that project (for example just release ...)
  • App project: use the generated app workflow and package via npm run package

Submit to marketplace:

  1. Publish your repository and release assets using the workflow included in the generated project
  2. Submit to marketplace:
  3. Via App: Open Extensions -> ... menu -> Publish Extension
  4. Via Web: Visit marketplace.zeloscloud.io
  5. After approval, users can install directly from the Zelos App

Extension Package Structure

Archive Validation and Enforcement

The marketplace and extension manager enforce strict validation to ensure security and reliability:

Size Limits

Limit Value Enforced
Compressed size 500 MiB Marketplace ingestion + Download
Extracted size 2 GiB Extraction time
File count 10,000 files Extraction time

Security Validation

During extraction, Zelos rejects archives containing:

  • Symbolic links or hard links (prevents link-based escapes)
  • Special files (block devices, character devices, FIFOs)
  • Path traversal attempts (../, absolute paths)
  • Hidden directories or files (starting with .)
  • Parent directory references anywhere in paths

Integrity Verification

  • SHA-256 checksum verified for every download
  • Checksum provided by marketplace (computed during ingestion)
  • Mismatch causes installation to fail immediately

When Validation Happens

  1. Marketplace ingestion: Size and format checks before publication
  2. Download: Size and checksum verification
  3. Extraction: Security validation (symlinks, path traversal, special files)

Validation Failures Prevent Installation

Archives that fail any validation check will not install. Ensure your package script creates clean archives without symlinks or hidden files.

Archive Requirements

Extensions must be packaged as compressed archives with specific requirements:

  • Supported formats: .tar.gz, .tgz, .zip
  • Size limit: 500 MiB compressed
  • Extracted size limit: 2 GiB
  • File count limit: 10,000 files
my-extension.tar.gz
├── extension.toml       # REQUIRED: Manifest file
├── main.py              # Entry point (Python)
├── pyproject.toml       # Python dependencies (modern package management)
├── uv.lock              # Locked dependencies (ensures reproducible installs)
├── README.md            # Marketplace documentation (optional but recommended)
├── config.schema.json   # Configuration schema (if applicable)
├── my_extension/        # Python package containing source code
│       └── __init__.py
└── ...                  # Any additional assets you reference (icons, etc.)

No Parent Directory

Files must be at the archive root. Do NOT wrap in an extra directory.

  • ❌ No hidden files/directories (starting with .)
  • ❌ No parent directory references (..)
  • ❌ No symbolic or hard links
  • ❌ No absolute paths
  • ✅ All paths must be relative
  • ✅ Forward slashes only (/)

Hidden files are now rejected during extraction—make sure your packaging script removes artifacts such as .git/, .DS_Store, or editor-specific folders before creating the archive.

The Extension Manifest

The extension.toml file is the heart of your extension - it defines identity, dependencies, runtime configuration, and metadata. Every field is validated to ensure security and compatibility.

Minimal Manifest

Required fields only:

name = "My Extension"
version = "1.0.0"

[host]
type = "agent"

[host.agent]
entry = "main.py"
name = "My Extension"
version = "1.0.0"

[runtime]
type = "python"
entry = "main.py"

Note

The [runtime] section is still supported for backward compatibility. New extensions should use [host].

Defaults

All other fields are optional with sensible defaults:

Manifest defaults (applied when field omitted):

  • python_version"3.11"
  • workdir"." (top-level field)
  • zelos.version → host-aware default
  • agent extensions: ">=25.0.20"
  • app extensions: ">=26.0.3"
  • stop.grace_seconds10
  • config.schema"config.schema.json" (when [config] section present)
  • targets[] (empty = all platforms)
  • env{} (no variables)
  • keywords[], categories[]

Complete Manifest Reference

name = "CANbus Decoder Pro"
version = "2.1.0"
description = "Decode CAN messages with DBC files and stream to Zelos"
author = "Acme Corporation"
repository = "https://github.com/acme/canbus-decoder"
icon = "assets/icon.png"
readme = "README.md"
homepage = "https://acme.com/canbus"
keywords = ["can", "automotive", "dbc", "j1939"]
categories = ["Automotive", "IoT"]

[zelos]
version = ">=25.0.20"

[host]
type = "agent"

[host.agent]
entry = "main.py"
python_version = "3.11"

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

[package]
paths = ["main.py", "canbus_decoder"]
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.3"

[host]
type = "app"

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

[package]
paths = ["dist"]

Manifest Field Reference

Required Fields

All Required Fields Must Be Present

Missing any required field will prevent your extension from being published or installed.

Field Type Description Validation
name string Display name • Cannot be empty or whitespace
• Shows in marketplace and app
• Example: "Data Processor Pro"
version string Semantic version • Strict SemVer format
• Example: 1.2.3 or 2.0.0-beta.1
• Used for updates and compatibility
host or runtime object Execution config • Use [host] (preferred) or [runtime] (legacy)
• See Host Configuration

Where does the extension ID come from?

You do not need to specify an internal id in the manifest. For marketplace installs, Zelos derives the ID from your GitHub repository in the form owner.repo (lowercased, with punctuation normalized). For local development installs, the ID defaults to local.<foldername>.

Optional Fields

Field Type Default Description
description string - Optional summary shown in marketplace
author string - Optional author or organization
zelos.version string Agent: >=25.0.20
App: >=26.0.3
Zelos compatibility
• NPM-style SemVer range
• Examples: >=25.0.20, >=26.0.3, *
• Checked at install time
repository string - GitHub URL
• Must be valid HTTPS URL
• Example: https://github.com/org/repo
• Used for marketplace integration
workdir string "." Working directory for execution
Top-level field (not under runtime)
• Relative to extension root
• No .. or hidden directories
targets array [] (all) Supported platforms
• Empty = all platforms
• See Platform Targets
env object {} Environment variables
• Passed to extension process
• Reserved keys prohibited
stop.grace_seconds integer 10 Graceful shutdown time
• Range: 1-300 seconds
• Process receives SIGTERM
keywords array [] Search terms (max 20)
• Used for marketplace discovery
• No empty strings allowed
categories array [] Marketplace categories (max 10)
• Examples: "IoT", "Automotive"
• No empty strings allowed
homepage string - Extension website
• Must be valid URL
• Shown in marketplace
icon string - Path to icon file
• Relative path in archive
• Optional
readme string - Path to README
• Markdown format
• Optional
config.schema string "config.schema.json" Path to JSON Schema
• Validates user configuration
• Default: config.schema.json

Host Configuration

The [host] section defines how the extension runs. It replaces the legacy [runtime] section with explicit host typing.

Agent Host (type = "agent")

[host]
type = "agent"

[host.agent]
runtime = "python"      # Optional, defaults to "python"
entry = "main.py"       # Required: Python entry point
python_version = "3.11" # Optional: "3.10" through "3.14"
requirements = "requirements.txt"  # Optional: dependency file override
workdir = "."           # Optional: working directory
Field Required Default Description
runtime No "python" Runtime type
entry Yes - Entry point script
python_version No "3.11" Python version (major.minor)
requirements No auto Dependency file override
workdir No "." Working directory

Agent hosts also support targets, env, and stop as sub-fields of [host.agent].

App Host (type = "app")

[host]
type = "app"

[host.app]
kind = "web_app"           # Optional, defaults to "web_app"
entry = "dist/index.html"  # Required: HTML entry file
Field Required Default Description
kind No "web_app" App contribution kind
entry Yes - HTML entry file path

Package Section

The [package] section controls which files are included in the release archive. When present, only files under the declared paths (plus extension.toml and referenced assets) are packaged.

[package]
paths = ["main.py", "my_module"]
Field Required Description
paths Yes Array of file or directory paths to include

Rules:

  • Entry point must be covered by one of the package paths
  • extension.toml is always included automatically
  • Referenced files (icon, readme, config.schema, pyproject.toml, uv.lock) are included if they exist
  • Use zelos extensions package --list <dir> to preview what will be included

Without [package], all non-hidden, non-node_modules files in the directory are packaged (legacy behavior).

Version Compatibility

Zelos uses semantic versioning to ensure compatibility between extensions and the platform.

The zelos.version field specifies which Zelos versions your extension is compatible with:

[zelos]
version = ">=25.0.20"  # Compatible with 25.0.20 and later

Common version patterns:

  • >=25.0.20 - Compatible with agent-hosted Zelos builds from 25.0.20 onward
  • >=26.0.3 - Compatible with app-hosted Zelos builds from 26.0.3 onward
  • >=25.0.0, <26.0.0 - Between 25.0.0 and 26.0.0 (exclusive upper bound)
  • ~25.0.20 - Compatible with 25.0.x only
  • * - Compatible with any version (explicit wildcard)
  • Omit the field entirely - Defaults are host-aware:
  • agent extensions: >=25.0.20
  • app extensions: >=26.0.3

Version Checking

Version requirements are checked at installation time. If the user's Zelos version doesn't meet the requirements, the installation will fail with a clear error message.

Platform Targets

Specify which platforms your extension supports using target strings:

Target String OS Architecture Description
linux-x86_64 Linux x86-64 Standard Linux desktop/server
linux-aarch64 Linux ARM64 Raspberry Pi, embedded Linux
darwin-x86_64 macOS Intel Older Macs (pre-2020)
darwin-arm64 macOS Apple Silicon M1/M2/M3 Macs
windows-x86_64 Windows x86-64 Windows 10/11

Universal Extensions

Leave targets empty or omit it entirely to support ALL platforms. This is ideal for pure Python extensions.

Platform-Specific Packaging

When creating platform-specific releases:

  1. Name your archives with platform identifiers:

    extension-1.0.0-linux-x86_64.tar.gz
    extension-1.0.0-darwin-arm64.tar.gz
    extension-1.0.0-windows-x86_64.zip
    extension-1.0.0-universal.tar.gz  # For all platforms
    
  2. The marketplace automatically detects platforms from filenames:

    • Keywords: linux, darwin/macos, windows
    • Architectures: x86_64/amd64, arm64/aarch64
    • Universal: any, universal, or no platform keywords
  3. Installation priority:

    1. Exact platform match
    2. Universal package
    3. Error if no compatible package

How Platform Detection Works

The marketplace automatically detects platform targets from archive filenames using keyword matching:

Operating System Detection:

  • Keywords darwin, macos, osxmacos
  • Keyword linuxlinux
  • Keywords win32, windows, .exewindows

Architecture Detection:

  • Keywords x64, x86_64, amd64x86_64
  • Keywords arm64, aarch64arm64 (macOS) or aarch64 (Linux)
  • Keywords arm, armv7arm

Fallback Behavior:

  • If both OS and architecture detected → specific platform target
  • If neither detected → universal package ({os: "any", arch: "any"})
  • If only one detected → treated as universal

Examples:

my-ext-1.0.0-linux-x86_64.tar.gz  → linux-x86_64
my-ext-1.0.0-macos-arm64.tar.gz   → darwin-arm64
my-ext-1.0.0-universal.tar.gz     → universal (any/any)
my-ext-1.0.0.tar.gz               → universal (no keywords)

Case Insensitive

All keyword matching is case-insensitive. Linux, LINUX, and linux are equivalent.

Runtime Configuration

Python Runtime

Legacy Section

The [runtime] section is supported for backward compatibility. New extensions should use [host] instead. See Host Configuration.

Python is the recommended runtime for most extensions, providing a rich ecosystem and easy development.

Basic Configuration

[runtime]
type = "python"
entry = "main.py"
python_version = "3.11"
Field Required Description
type Yes Must be "python"
entry Yes Path to your Python entry point (e.g., "main.py")
python_version No Python version in major.minor format (default: "3.11")

Python Version Support

Zelos uses UV for Python environment management. UV automatically downloads the Python version you specify - no pre-installation needed!

Python Version Requirement

The zelos-sdk requires Python ≥3.10. Extensions specifying Python <3.10 will install successfully but fail at runtime with import errors. Always use Python 3.10 or higher.

How It Works

When a user installs your extension, UV automatically:

  1. Downloads the specified Python version if not present
  2. Creates an isolated virtual environment
  3. Installs your dependencies
  4. Runs your extension

This ensures consistent behavior across all platforms and users.

Virtual Environment Details

Every Python extension runs in an isolated virtual environment:

  • Tool: Uses uv (ultra-fast Python package manager)
  • Location: .venv/ in extension directory
  • Dependencies: When uv.lock is present, installs use uv sync --frozen for reproducible, pinned versions. Falls back to uv pip install -r pyproject.toml when no lockfile is available.
  • Cache: .cache/ directory for faster reinstalls
  • Timeout: 15 minutes for dependency installation
  • Isolation: No access to system Python packages
  • Bytecode: Python files are pre-compiled to .pyc during installation for faster startup

Extension Entry Point Patterns

The generated agent project provides a production-ready entry point with proper structure, logging, and signal handling. Here are the core patterns:

The template uses a class-based approach with proper lifecycle management:

#!/usr/bin/env python3
import logging
import signal
import sys
from types import FrameType

import zelos_sdk
from zelos_sdk.hooks.logging import TraceLoggingHandler
from zelos_sdk.extensions import load_config

from my_extension.extension import MyExtension

# Configure basic logging before SDK initialization
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)

# Initialize SDK (required before adding trace logging handler)
zelos_sdk.init(name="my_extension", actions=True)

# Add trace logging handler to send logs to Zelos
handler = TraceLoggingHandler("my_extension_logger")
logging.getLogger().addHandler(handler)

# Load configuration from config.json (with schema defaults applied)
config = load_config()

# Create extension and register actions
extension = MyExtension(config)
zelos_sdk.actions_registry.register(extension)

def shutdown_handler(signum: int, frame: FrameType | None) -> None:
    """Handle graceful shutdown."""
    logger.info("Shutting down...")
    extension.stop()
    sys.exit(0)

signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)

if __name__ == "__main__":
    logger.info("Starting extension")
    extension.start()
    extension.run()

Benefits: - Proper separation of concerns - Testable code structure - Built-in logging integration - Graceful shutdown handling - Action registration support

Most extensions don't need async

The template uses a simpler synchronous pattern that works for most use cases. Only use async if you have specific async I/O requirements (e.g., async HTTP clients, websockets).

For async operations, you can use this pattern:

#!/usr/bin/env python3
import asyncio
import logging
import signal
import sys
from types import FrameType

import zelos_sdk

logger = logging.getLogger(__name__)

class AsyncExtension:
    def __init__(self):
        self.running = False
        self._shutdown = asyncio.Event()
        self.source = zelos_sdk.TraceSource("data")
        self.source.add_event(
            "event",
            [
                zelos_sdk.TraceEventFieldMetadata("value", zelos_sdk.DataType.Float64),
            ],
        )

    def start(self) -> None:
        self.running = True

    def stop(self) -> None:
        self.running = False
        self._shutdown.set()

    async def run_async(self) -> None:
        while self.running:
            try:
                # Your async logic here
                self.source.event.log(value=42.0)

                # Wait with cancellation support
                try:
                    await asyncio.wait_for(self._shutdown.wait(), timeout=1.0)
                except asyncio.TimeoutError:
                    continue
            except asyncio.CancelledError:
                break

extension = AsyncExtension()

def shutdown_handler(signum: int, frame: FrameType | None) -> None:
    extension.stop()

signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)

if __name__ == "__main__":
    extension.start()
    asyncio.run(extension.run_async())

Use the Template

The generated project provides the proper structure automatically. These patterns are shown for reference when customizing your extension.

Managing Dependencies

The generated project uses a simple, opinionated dependency workflow:

  1. Define dependencies in pyproject.toml:
[project]
name = "my-extension"
version = "1.0.0"
dependencies = [
  "zelos-sdk>=0.0.6",
  "aiohttp>=3.9.0",
]

[project.optional-dependencies]
dev = [
  "pytest>=8.4.2",
  "pre-commit>=4.3.0",
  "ruff>=0.13.3",
]
  1. Lock dependencies with uv sync:
uv sync  # Creates/updates uv.lock

This ensures reproducible installations across all environments.

What gets included in your extension archive:

  • pyproject.toml - Dependency specification (used during installation)
  • uv.lock - Locked dependency versions (ensures reproducible installs)
  • ✅ Python packages / modules referenced by entry
  • ✅ Documentation assets you reference in extension.toml (e.g., README, icon)

Template Handles This

The generated project's just package command automatically bundles the correct files.

Environment Variables

Extensions receive a minimal, secure environment:

Variable Description Source
HOME User home directory System
PATH Minimal system PATH System (filtered)
TMPDIR/TEMP Temporary directory System
ZELOS_CONFIG_PATH Path to config.json file Agent
ZELOS_DATA_DIR Writable data directory Agent
Custom Your variables Manifest [env]

Reserved Variables

Variables starting with ZELOS_ are reserved and cannot be set in the manifest.

Zelos-provided directories:

  • ZELOS_CONFIG_PATH: Points to your extension's config.json file. Use zelos_sdk.extensions.load_config() to read it.
  • ZELOS_DATA_DIR: Writable directory for persistent storage (uploaded files, caches, etc.). Use this instead of writing to your code directory, which is read-only.
import os
from pathlib import Path

# Read configuration
config = zelos_sdk.extensions.load_config()  # Uses ZELOS_CONFIG_PATH automatically

# Write persistent data
data_dir = Path(os.environ["ZELOS_DATA_DIR"])
uploaded_file = data_dir / "uploaded.dbc"
uploaded_file.write_bytes(file_data)

Publishing Your Extension

Generated release workflows depend on the host type. The example below is the agent-template flow.

Release Process

  1. Create a release using the template's command:

    just release 0.1.0    # Updates versions, runs checks, commits, and creates tag
    
  2. Push to GitHub:

    git push origin main v0.1.0
    
  3. GitHub Actions automatically:

    • Validates version matches the tag
    • Runs all checks and tests
    • Packages the extension as a tarball
    • Creates a GitHub release with the archive attached
  4. Submit to marketplace:

    Option 1: Via Zelos App (recommended)

    • Open Extensions view in the Zelos App
    • Click menu in Marketplace header
    • Select Publish Extension
    • Enter your repository URL
    • Install the GitHub App (one-time, read-only)
    • Wait for approval

    Option 2: Via Web

    • Visit Zelos Marketplace
    • Click "Submit Extension"
    • Enter your repository URL
    • Install the GitHub App (one-time, read-only)
    • Wait for approval

After approval, users can install your extension directly from the Zelos App.

Agent Template Release Flow

The generated agent project includes:

  • just release command that handles versioning
  • GitHub Actions workflow (.github/workflows/release.yml) for automation
  • Packaging script that creates proper tarballs
  • Version verification to prevent mismatches

You don't need to create archives manually or write workflows - it's all included.

Extension Configuration

Extensions can accept user configuration through a JSON config file. The Zelos app automatically generates a configuration UI with custom widgets, validation, and help text.

How the App Uses Your Schema

The same schema powers the in-app workflow described in Extensions. When a user opens Configure for an installed extension:

  • the form is generated from your config.schema.json
  • the dialog loads the last saved configuration for that agent when one exists
  • otherwise the form starts from the default values in your schema
  • submitting the form starts a stopped extension or restarts a running extension

Design default, required, description, and ui:* fields for that first-run and edit workflow, not just for raw JSON editing.

Quick Start

1. Create config.schema.json:

{
  "type": "object",
  "properties": {
    "apiKey": {
      "type": "string",
      "title": "API Key",
      "description": "Your API key for authentication",
      "minLength": 32,
      "ui:widget": "password",
      "ui:placeholder": "Enter your API key..."
    },
    "interval": {
      "type": "number",
      "title": "Update Interval",
      "description": "How often to check for updates (seconds)",
      "minimum": 1,
      "maximum": 3600,
      "default": 60,
      "ui:widget": "slider"
    },
    "enabled": {
      "type": "boolean",
      "title": "Enable Feature",
      "default": true,
      "ui:widget": "toggle"
    }
  },
  "required": [
    "apiKey"
  ]
}

2. Reference in extension.toml:

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

The Zelos app generates a configuration form from your schema. When users save their configuration, it's written to config.json in your extension's directory.

Schema Defaults

You can extract defaults from your schema to avoid duplicating them in code. The generated project includes this helper.

Schema Validation During Publication

Important: The marketplace validates your config.schema.json during release ingestion, before users can install your extension.

What's validated: - Must be valid JSON - Must be valid JSON Schema (Draft 2020-12) - Schema file must exist at the path specified in manifest - Schema must be a JSON object with standard properties

If validation fails: Your extension will not be published to the marketplace. Test your schema locally before creating a release:

# Validate JSON syntax
python -m json.tool config.schema.json

# Test with actual config values
# Use online validator: https://www.jsonschemavalidator.net/

UI Customization Properties

Embed these ui:* properties in your schema to customize the form:

Property Description Example
ui:widget Choose widget type "slider", "toggle", "password", "textarea", "radio"
ui:help Tooltip with info icon "This setting controls update frequency"
ui:placeholder Input placeholder text "Enter API key..."
ui:rows Textarea rows (default: 4) 8
ui:inline Display radio buttons inline true

Widget Names

Widget names are case-insensitive and support aliases:

Recommended (lowercase): - "toggle" or "switch" → Toggle switch widget - "range" or "slider" → Slider widget - "password" → Password input - "textarea" → Multi-line text - "radio" → Radio button group - "select" or "dropdown" → Dropdown menu

For consistency, use lowercase names in your schemas.

Widget Reference

Choose the right widget for your configuration fields:

Text Input Widgets

TextWidget (default for strings)

  • Single-line text input
  • No ui:widget needed - automatic for type: "string"

TextareaWidget

  • Multi-line text input
  • Use for longer text content
{
  "description": {
    "type": "string",
    "title": "Description",
    "ui:widget": "textarea",
    "ui:rows": 6,
    "ui:placeholder": "Enter detailed description..."
  }
}

Password Widget

  • Hidden password input
  • Use for sensitive data
{
  "password": {
    "type": "string",
    "title": "Password",
    "minLength": 8,
    "ui:widget": "password",
    "ui:help": "Minimum 8 characters"
  }
}

EmailWidget

  • Email input with validation
  • Automatic with format: "email"
{
  "email": {
    "type": "string",
    "title": "Email",
    "format": "email"
  }
}

URLWidget

  • URL input with validation
  • Automatic with format: "uri"
{
  "endpoint": {
    "type": "string",
    "title": "API Endpoint",
    "format": "uri"
  }
}

Numeric Widgets

NumberWidget (default for numbers)

  • Number input with step controls
  • Supports minimum, maximum, multipleOf
{
  "timeout": {
    "type": "integer",
    "title": "Timeout (seconds)",
    "minimum": 1,
    "maximum": 300,
    "default": 30
  }
}

Range Widget

  • Slider for numeric values
  • Requires minimum and maximum
  • Use multipleOf to control step increments
{
  "volume": {
    "type": "number",
    "title": "Volume",
    "minimum": 0,
    "maximum": 100,
    "default": 75,
    "ui:widget": "range"
  }
}

Range with precise increments:

{
  "interval": {
    "type": "number",
    "title": "Sample Interval (seconds)",
    "description": "Data collection interval from 1kHz to 1Hz",
    "minimum": 0.001,
    "maximum": 1.0,
    "multipleOf": 0.001,
    "default": 0.1,
    "ui:widget": "range",
    "ui:help": "Slider steps in 1ms increments"
  }
}

Boolean Widgets

CheckboxWidget (default for booleans)

  • Standard checkbox
  • No ui:widget needed
{
  "enabled": {
    "type": "boolean",
    "title": "Enable Feature",
    "default": true
  }
}

Toggle Widget

  • Toggle switch (better UX)
  • Recommended for boolean settings
{
  "enabled": {
    "type": "boolean",
    "title": "Enable Feature",
    "default": true,
    "ui:widget": "toggle"
  }
}

Selection Widgets

SelectWidget (default for enums)

  • Dropdown select
  • Automatic for enum arrays
{
  "mode": {
    "type": "string",
    "title": "Mode",
    "enum": [
      "auto",
      "manual",
      "scheduled"
    ],
    "default": "auto"
  }
}

RadioWidget

  • Radio button group
  • Better for 2-5 options
{
  "priority": {
    "type": "string",
    "title": "Priority",
    "enum": [
      "low",
      "normal",
      "high"
    ],
    "default": "normal",
    "ui:widget": "radio",
    "ui:inline": true
  }
}

MultiSelectWidget

  • Multi-select dropdown with search
  • Automatic for arrays with enum items
{
  "features": {
    "type": "array",
    "title": "Enabled Features",
    "items": {
      "type": "string",
      "enum": [
        "logging",
        "metrics",
        "alerts",
        "reports"
      ]
    },
    "uniqueItems": true
  }
}

File Widgets

FilePathPickerWidget

  • Opens file browser, returns path as string
  • Does not upload the file
  • Use for remote file paths or when file upload isn't needed
{
  "bitstreamPath": {
    "type": "string",
    "title": "Bitstream File",
    "ui:widget": "file-picker",
    "ui:placeholder": "C:/path/to/file.bit"
  }
}

FolderPickerWidget

  • Opens folder browser, returns directory path as string
  • Does not upload folder contents
  • Useful for selecting output directories
  • Supports optional createDirectory flag to allow creating new folders
{
  "outputDir": {
    "type": "string",
    "title": "Output Directory",
    "ui:widget": "folder-picker",
    "ui:placeholder": "C:/path/to/output"
  }
}

With createDirectory option:

{
  "outputDir": {
    "type": "string",
    "title": "Output Directory",
    "ui:widget": "folder-picker",
    "ui:options": {
      "createDirectory": true
    }
  }
}

FileWidget

  • Single file upload
  • Returns base64 data URL
  • 10MB limit
{
  "configFile": {
    "type": "string",
    "title": "Configuration File",
    "format": "data-url",
    "ui:help": "Upload JSON or YAML config file"
  }
}

FilesWidget

  • Multiple file upload
  • Returns array of data URLs
  • 50MB total limit
{
  "logFiles": {
    "type": "array",
    "title": "Log Files",
    "items": {
      "type": "string",
      "format": "data-url"
    }
  }
}

Processing uploaded files:

File uploads are provided as base64-encoded data URLs. You must decode them and save to the writable data directory using ZELOS_DATA_DIR.

Code Directory is Read-Only

Your extension's code directory is read-only for security. Always write uploaded files to ZELOS_DATA_DIR, not the current directory.

import base64
import os
from pathlib import Path
from zelos_sdk.extensions import load_config

config = load_config()
file_data_url = config.get("configFile")

if file_data_url and file_data_url.startswith("data:"):
    # Split: "data:application/json;base64,SGVsbG8="
    header, encoded = file_data_url.split(",", 1)
    mime_type = header.split(";")[0].replace("data:", "")

    # Decode base64
    file_bytes = base64.b64decode(encoded)

    # Write to data directory (writable, persistent storage)
    data_dir = Path(os.environ["ZELOS_DATA_DIR"])
    output_path = data_dir / "uploaded_config.json"
    output_path.write_bytes(file_bytes)

    # Now use the file
    print(f"Saved uploaded file to: {output_path}")

Date/Time Widgets

DateWidget

  • Calendar date picker
  • Automatic with format: "date"
{
  "startDate": {
    "type": "string",
    "title": "Start Date",
    "format": "date"
  }
}

Configuration Loading

When users save configuration in the Zelos app, it's written to config.json in your extension's working directory.

Loading Configuration

Use the SDK's load_config() helper to load configuration with schema defaults:

from zelos_sdk.extensions import load_config

# Load configuration from config.json (with schema defaults applied)
config = load_config()

# Access configuration values
interval = config.get("interval")  # Gets default from schema if not in config.json
sensor_name = config.get("sensor_name")

What load_config() does: - Loads config.json from your extension directory - Applies default values from your config.schema.json - Validates configuration against your schema - Returns a merged dictionary with all defaults filled in

Benefits: - Automatic defaults - No need to hardcode default values - Schema validation - Catches configuration errors early - Simple API - One function call, everything works

Common Patterns

API Configuration

{
  "type": "object",
  "properties": {
    "apiUrl": {
      "type": "string",
      "title": "API URL",
      "format": "uri",
      "default": "https://api.example.com"
    },
    "apiKey": {
      "type": "string",
      "title": "API Key",
      "minLength": 32,
      "ui:widget": "password"
    },
    "timeout": {
      "type": "integer",
      "title": "Timeout (seconds)",
      "minimum": 1,
      "maximum": 300,
      "default": 30
    }
  },
  "required": [
    "apiUrl",
    "apiKey"
  ]
}

Nested Configuration

{
  "type": "object",
  "properties": {
    "connection": {
      "type": "object",
      "title": "Connection Settings",
      "properties": {
        "host": {
          "type": "string",
          "default": "localhost"
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535,
          "default": 5000
        }
      }
    }
  }
}

Dynamic Arrays

{
  "endpoints": {
    "type": "array",
    "title": "API Endpoints",
    "items": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "title": "Name"
        },
        "url": {
          "type": "string",
          "title": "URL",
          "format": "uri"
        }
      },
      "required": [
        "name",
        "url"
      ]
    },
    "minItems": 1
  }
}

Multi-Event Monitoring

For extensions that collect different types of data, organize events by category:

# Define multiple event types with proper types and units
def _define_schema(self) -> None:
    # Environmental sensors - temperature and humidity
    self.source.add_event(
        "environmental",
        [
            zelos_sdk.TraceEventFieldMetadata("temperature", zelos_sdk.DataType.Float32, "°C"),
            zelos_sdk.TraceEventFieldMetadata("humidity", zelos_sdk.DataType.Float32, "%"),
        ],
    )

    # Power readings - voltage and current
    self.source.add_event(
        "power",
        [
            zelos_sdk.TraceEventFieldMetadata("voltage", zelos_sdk.DataType.Float32, "V"),
            zelos_sdk.TraceEventFieldMetadata("current", zelos_sdk.DataType.Float32, "A"),
        ],
    )

# Log to different events
self.source.environmental.log(temperature=22.5, humidity=55.0)
self.source.power.log(voltage=12.0, current=2.5)

Benefits: - Clean separation of different data types - Users can plot/analyze each category independently - Easier to understand and maintain

Best Practices

  1. Provide defaults - Set sensible default values for all optional fields
  2. Add descriptions - Use description for inline help, ui:help for detailed tooltips
  3. Validate inputs - Use minLength, maximum, pattern, etc.
  4. Group related settings - Use nested objects for organization
  5. Choose appropriate widgets - Toggle switches for booleans, radio for few options
  6. Test your schema - Install your extension and verify the form renders correctly

Validation

The Zelos app validates configuration against your schema:

  • Required fields - Marked with asterisk (*), validation on submit
  • Type checking - Ensures correct data types
  • Range validation - Checks minimum/maximum values
  • Pattern matching - Validates regex patterns
  • Custom formats - email, uri, date validated automatically

Validation errors appear inline below each field.

Troubleshooting

Widget not appearing:

  • Verify type matches widget requirements (e.g., range needs type: "number")
  • For range: must have both minimum and maximum properties
  • For radio: must have enum array with options
  • Widget names are case-insensitive: "toggle", "Toggle", "ToggleWidget" all work

File uploads not working:

  • Use "format": "data-url" (not "file")
  • Single file: type: "string" + format
  • Multiple files: type: "array", items: { type: "string", format: "data-url" }

Validation errors:

Important Notes

  • Widget names are case-insensitive - "range", "Range", "RangeWidget" all work
  • Common aliases supported - "slider""range", "switch""toggle"
  • ui:* properties are frontend-only - Backend validation ignores them (safe to use)
  • File size limits - 10MB single file, 50MB total for multiple files
  • Base64 encoding - File uploads are base64-encoded data URLs

Installation Details

Where Extensions Are Installed

Extensions are installed in platform-specific directories:

Platform Location
Linux ~/.local/share/io.zeloscloud.zelos/extensions/{id}/{version}/
macOS ~/Library/Application Support/io.zeloscloud.zelos/extensions/{id}/{version}/
Windows %LOCALAPPDATA%\io.zeloscloud.zelos\extensions\{id}\{version}\

Installation Directory Structure

acme.sensor-monitor/
└── 1.0.0/
    ├── code/              # Your shipped code (read-only)
    │   ├── extension.toml # Manifest
    │   └── main.py        # Entry point
    ├── .integrity         # Code integrity hash
    ├── config.json        # User configuration
    ├── extension.log      # Runtime logs
    ├── .venv/             # Python virtual environment
    └── .cache/            # Package cache

Installation Process

The Zelos Agent handles installation automatically:

  1. Download - Fetch archive from marketplace (60s timeout)
  2. Verify - Check SHA-256 checksum
  3. Extract - Unpack with security validation
  4. Prepare - Set up runtime environment
  5. Configure - Apply user configuration
  6. Start - Launch the extension

Security Measures

During installation, the system enforces:

  • Path validation - No escaping installation directory
  • Size limits - 500 MiB compressed, 2 GiB extracted
  • File limits - Maximum 10,000 files
  • No symlinks - Symbolic/hard links rejected
  • Checksum verification - SHA-256 integrity check

Extension Lifecycle

Understanding the extension lifecycle helps you build robust, well-behaved extensions.

Shutdown Process

When your extension is stopped, the supervisor follows this precise sequence:

  1. Send SIGTERM to your extension process
  2. Poll status every 50 milliseconds to check if process exited
  3. Wait for up to grace_seconds (default: 10, max: 300)
  4. Force kill with SIGKILL if still running after grace period
  5. Confirm termination by waiting 1 second after SIGKILL
  6. Clean up process tracking (additional delays on Windows)

Timing Details

Phase Duration Platform Notes
Poll interval 50ms Checks if process exited
Grace period 1-300s (default: 10s) Configurable in manifest
Force kill confirmation 1s After SIGKILL sent
Windows cleanup 200ms × grace_seconds Capped at 1000ms total

Example Timeline

For an extension with grace_seconds = 30:

T+0ms:    Send SIGTERM
T+50ms:   Poll #1 - still running
T+100ms:  Poll #2 - still running
...
T+30000ms: Grace period expired
T+30000ms: Send SIGKILL (cannot be caught)
T+31000ms: Confirm termination
T+31000ms: Extension fully stopped

Grace Period is a Hard Limit

If your extension doesn't exit within grace_seconds, it will be forcefully terminated with SIGKILL (which cannot be caught or ignored). Ensure your cleanup code completes within the grace period.

Without Signal Handling

If you don't handle SIGTERM, your extension will be forcefully terminated after the grace period. This is acceptable for:

  • Simple, stateless extensions
  • Prototypes and testing
  • Extensions with no cleanup requirements

For production extensions, handle SIGTERM to shut down gracefully:

import signal
import sys

def cleanup():
    # Save state
    # Close connections
    # Flush buffers
    pass

def signal_handler(signum, frame):
    print(f"Received signal {signum}, shutting down...")
    cleanup()
    sys.exit(0)

# Register handlers
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

Configuring Grace Period

Adjust the shutdown timeout in your manifest based on your cleanup needs:

[stop]
grace_seconds = 30  # 1-300 seconds allowed (default: 10)

Grace Period Behavior

If your extension doesn't exit within grace_seconds, it will be force-killed with SIGKILL (cannot be caught). Make sure your cleanup code completes within the grace period.

Crash Detection and Exit Information

Zelos tracks how extensions exit to help users identify crashes vs. intentional stops:

User-initiated stops (clicking stop button): - No exit information recorded - Extension shows normal "stopped" state

Unexpected stops (crashes, signals, errors): - Exit code and/or signal captured - Extension shows "error" state with amber warning icon - Toast notification alerts the user - "View Logs" action opens extension.log

Exit information captured:

Field Description Example
code Process exit code 0 (success), 1 (error), 137 (OOM killed)
signal Signal that terminated process 9 (SIGKILL), 15 (SIGTERM), 11 (SIGSEGV)

Write Helpful Logs

Since users can view your extension's log file when crashes occur, ensure you log meaningful error messages. Use the TraceLoggingHandler to also stream logs to Zelos for real-time debugging.

Common exit scenarios:

Exit Code/Signal Meaning Typical Cause
Code 0 Success Normal exit (but unexpected if not user-initiated)
Code 1 General error Unhandled exception, assertion failure
Code 137 OOM killed Memory limit exceeded (Linux)
Signal 9 (SIGKILL) Force killed Memory limit, unresponsive process
Signal 11 (SIGSEGV) Segmentation fault Native code crash, invalid memory access
Signal 15 (SIGTERM) Termination Grace period expired during shutdown

Updates and Compatibility

When users update your extension:

  1. Old version receives SIGTERM shutdown signal
  2. Configuration is preserved
  3. New version starts with saved config
  4. Rollback occurs if startup fails

Security Best Practices

Path Security

The extension system enforces strict path security:

✅ DO:

  • Use relative paths only
  • Use forward slashes (/)
  • Keep files within your archive

❌ DON'T:

  • Use absolute paths (/etc/passwd)
  • Use parent references (../../../)
  • Include hidden files (.git/)
  • Create symbolic links

Process Isolation

Your extension runs in a sandboxed environment:

Isolation Level Description
Process Separate process with limited privileges
File System Confined to extension directory
Environment Minimal, sanitized variables
Network No restrictions (use responsibly)

Security Checklist

Before publishing, ensure:

  • No hardcoded secrets or API keys
  • Input validation for all external data
  • Proper error handling without exposing internals
  • Dependencies from trusted sources only
  • No execution of user-provided code
  • Graceful handling of resource limits
  • Regular security updates for dependencies

Error Handling

Common Errors and Solutions

Error Cause Solution
Download timeout Large archive or slow connection Reduce archive size, optimize dependencies
Checksum mismatch Corrupted download Re-upload release assets
Invalid manifest Missing required fields Validate extension.toml before publishing
Dependencies failed Package installation error Pin versions, test in clean environment
Config validation failed Schema mismatch Test schema with various configs

Debugging Tips

  1. Check logs: extension.log in installation directory
  2. Test locally: Run without Zelos Agent first
  3. Validate manifest: Ensure all paths and fields are correct
  4. Minimal dependencies: Start simple, add incrementally
  5. Use debug mode: Add verbose logging when needed

Development Best Practices

Design Principles

Principle Description
Single Responsibility Do one thing well
Fail Gracefully Handle errors without crashing
Resource Efficient Minimize CPU, memory, and network usage
User Friendly Clear logs, helpful error messages
Maintainable Clean code, good documentation

Code Organization

my-extension/
├── extension.toml       # Manifest
├── pyproject.toml       # Dependencies & metadata (recommended)
├── uv.lock              # Lock file for reproducible builds (recommended)
├── main.py              # Entry point
├── config.schema.json   # JSON Schema for configuration UI
├── my_extension/        # Python package (importable modules)
│   ├── __init__.py
│   ├── extension.py     # Main extension class with actions
│   ├── core.py          # Core logic
│   └── utils/           # Your utility functions (if needed)
│       └── __init__.py
├── tests/               # Unit tests
│   └── test_extension.py
├── .vscode/             # VSCode integration
│   ├── settings.json    # Auto-formatting, testing, etc.
│   └── extensions.json  # Recommended extensions (Ruff, Python, etc.)
├── .github/
│   ├── workflows/
│   │   ├── CI.yml       # Continuous Integration
│   │   └── release.yml  # Automated releases
│   └── dependabot.yml   # Automated dependency updates
├── Justfile             # Task runner (install/check/test/package)
└── README.md            # User documentation

Commit uv.lock and pyproject.toml

Always commit both uv.lock and pyproject.toml to your repository. This ensures:

  • Reproducible installs for collaborators and CI
  • The marketplace archive includes both files for end users
  • Consistent dependency versions across all environments

Testing Your Extension

Local Testing

The template's just commands make testing simple:

# Install everything
just install

# Run checks
just check    # Linting + type checking
just test     # Run test suite

# Run extension locally
just dev

# Test with different configs
echo '{"sensor_name": "test", "interval": 0.5}' > config.json
just dev

All dependencies are managed by UV automatically - no manual venv setup needed.

Refreshing Dependencies

After you modify pyproject.toml dependencies (using uv add or manually editing), tell the agent to rebuild the virtual environment without uninstalling the extension:

# Recreate the venv and reinstall dependencies for a running local extension
zelos extensions reinstall local.my-extension

The reinstall command preserves your configuration, invalidates the cached environment, and reapplies the integrity checks used for marketplace extensions.

Configuration lives outside your code tree

When the agent starts an extension it writes the active configuration to <install-dir>/config.json and exposes that path via the ZELOS_CONFIG_PATH environment variable. The default zelos_sdk.extensions.load_config() helper automatically uses that variable.

You can trigger the same operation from the Zelos App by opening the extension detail pane and clicking the Reinstall button. The agent stops you from running the rebuild while the extension is still executing, so stop it first if you need to refresh dependencies.

Troubleshooting Guide

Common Issues

Problem Solution
Extension not appearing in marketplace Ensure extension.toml is in repo root and release has assets
Installation timeout Reduce dependency size, use lightweight packages
Extension won't start Check extension.log for errors
Configuration rejected Validate JSON and match schema
Platform not supported Add platform to targets array

Getting Help

  1. Check the logs - Find extension.log in installation directory
  2. Validate locally - Test extension before publishing
  3. Review examples - Compare with working extensions