Skip to content

How to Develop Extensions

Overview

Extensions are the primary way to extend Zelos capabilities beyond its core functionality. They enable developers to create reusable, distributable packages that integrate seamlessly with the Zelos ecosystem. Each extension runs in an isolated environment and communicates with the Zelos Agent through well-defined interfaces.

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.

Why Build Extensions?

Extensions allow you to:

  • Share protocols and integrations with the global Zelos community
  • Accelerate development by building on the robust Zelos SDK
  • Package domain expertise into reusable tools that others can install instantly

What Can Extensions Do?

Extensions can:

  • Stream data from any source (hardware, APIs, files, networks)
  • Decode protocols (CAN, Modbus, MQTT, custom binary formats)
  • Control hardware through interactive actions and automation
  • Process and transform data in real-time
  • Integrate with external services and cloud platforms
  • Provide custom visualizations through the Zelos App

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

Extensions run as Python applications using Python 3.10 through 3.14 (major.minor format). UV automatically manages Python downloads and virtual environments.

Security Model

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

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, .last_config, 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 still hot-reload by symlinking code/ to your working copy, but the runtime artifacts now live in the container directory instead of polluting your repo.

Windows Developer Mode required

Local installs rely on symlinks. Enable Windows Developer Mode (or otherwise allow symlink creation) before installing a local extension on Windows.

Creating Your First Extension

Use the official Cookiecutter template to generate a production-ready extension:

uvx cookiecutter https://github.com/zeloscloud/cookiecutter-zelos-extension.git

You'll be prompted for:

  • Author – Your name (e.g., "Jane Doe")
  • Email – Contact email
  • GitHub Handle – Your GitHub username
  • Project Name – Extension name in kebab-case (e.g., "sensor-monitor")
  • Description – Short summary (auto-generated if skipped)

The template automatically generates a complete project with:

  • ✅ Complete extension structure with working example
  • ✅ Development tools: uv, just, ruff, pytest
  • ✅ Pre-commit hooks for code quality
  • ✅ GitHub Actions CI/CD workflows
  • ✅ Professional documentation (README, CONTRIBUTING, CHANGELOG)
  • ✅ Tests and proper project organization
  • ✅ VSCode integration with recommended extensions and settings
  • ✅ Automated dependency updates via Dependabot

Generated Project Structure

my-zelos-extension/
├── extension.toml              # Extension manifest
├── main.py                     # Entry point
├── config.schema.json          # Configuration UI schema
├── pyproject.toml              # Dependencies and metadata
├── my_zelos_extension/         # Application source
│   ├── extension.py            # Core: SensorMonitor class with actions
│   └── utils/
│       └── __init__.py         # Your utility functions (if needed)
├── tests/
│   └── test_extension.py       # Extension tests
├── assets/
│   └── icon.svg                # Extension icon
├── scripts/
│   └── package_extension.py    # Packaging script
├── .vscode/
│   ├── settings.json           # VSCode settings (formatting, testing)
│   └── extensions.json         # Recommended extensions
├── .github/
│   ├── workflows/
│   │   ├── CI.yml              # CI on pushes/PRs
│   │   └── release.yml         # Release automation on tags
│   └── dependabot.yml          # Automated dependency updates
├── Justfile                    # Commands: install/dev/check/test/package/release
├── README.md                   # User documentation
└── CONTRIBUTING.md             # Developer documentation

Development Workflow

# Enter your project
cd my-zelos-extension

# Install dependencies and set up pre-commit hooks
just install

# Run all checks
just check     # Linting with ruff
just test      # Run test suite

# Run extension locally
just dev

# Create a release
just release 0.1.0    # Update version, run checks, commit, and create tag
git push origin main v0.1.0

The template includes a working sensor monitor example that demonstrates:

  • Schema definition with proper types and units
  • Multiple event types - Example sensor monitoring
  • Interactive actions for runtime control
  • Synchronous data streaming with clean lifecycle management
  • Configuration loading with fallback to defaults
  • Graceful shutdown handling with signal handlers
  • Code quality tools - Ruff for linting and formatting
  • VSCode integration - Instant setup with recommended extensions and settings
  • Automated updates - Dependabot keeps dependencies current

Publishing to Marketplace

Initial Setup:

  1. Create a repository on GitHub (e.g., 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
    

Publishing a Release:

  1. Create and tag a release: just release 0.1.0 && git push origin main v0.1.0
  2. GitHub Actions automatically packages and creates a release
  3. Submit your repository URL to the Zelos Marketplace
  4. 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, changelog, 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"                  # Display name
version = "1.0.0"                      # SemVer version

# Runtime
[runtime]
type = "python"                        # Python runtime (only supported type)
entry = "main.py"                      # Entry point file

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"^25.0.20"
  • stop.grace_seconds10
  • config.schema"config.schema.json" (when [config] section present)
  • targets[] (empty = all platforms)
  • env{} (no variables)
  • keywords[], categories[]

Complete Manifest Reference

# === REQUIRED FIELDS ===
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"

[zelos]
version = "^25.0.20"  # SemVer range for agent compatibility (default: "^25.0.20", can be omitted)

# === RUNTIME CONFIGURATION ===
workdir = "."                # Working directory (relative to extension root)

[runtime]
type = "python"              # Python runtime (only supported type)
entry = "main.py"            # Entry point script
python_version = "3.11"      # Supported: "3.10" through "3.14"

# Platform targets (empty = all platforms)
targets = [
  "linux-x86_64",   # Linux Intel/AMD
  "linux-aarch64",  # Linux ARM64
  "darwin-x86_64",  # macOS Intel
  "darwin-arm64",   # macOS Apple Silicon
  "windows-x86_64"  # Windows 64-bit
]

# Environment variables
[env]
LOG_LEVEL = "info"
API_ENDPOINT = "https://api.example.com"

# Shutdown configuration
[stop]
grace_seconds = 30  # Time to gracefully shutdown (1-300 seconds)

# Marketplace metadata
homepage = "https://acme.com/canbus"
keywords = ["can", "automotive", "dbc", "j1939"]  # Max 20
categories = ["Automotive", "IoT"]                 # Max 10

# File references (all relative paths)
icon = "assets/icon.png"
readme = "README.md"
changelog = "CHANGELOG.md"

# Compatibility

# Configuration schema
[config]
schema = "schemas/config.json"  # JSON Schema for validation

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
runtime object Execution config • See Runtime Configuration
• Defines how extension runs

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 ^25.0.20 Zelos compatibility
• NPM-style SemVer range
• Examples: ^25.0.20, >=25.0.0, *
• 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
changelog string - Path to CHANGELOG
• Version history
• Optional
config.schema string "config.schema.json" Path to JSON Schema
• Validates user configuration
• Default: config.schema.json

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.x.x

Common version patterns:

  • ^25.0.20 - Compatible with 25.x.x (recommended for most extensions)
  • >=25.0.0 - Version 25.0.0 or higher
  • >=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 to ^25.0.20 (current version)

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

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: Installed from pyproject.toml and uv.lock
  • Cache: .cache/ directory for faster reinstalls
  • Timeout: 15 minutes for dependency installation
  • Isolation: No access to system Python packages

Extension Entry Point Patterns

The Cookiecutter template 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 Cookiecutter template generates the proper structure automatically. These patterns are shown for reference when customizing your extension.

Managing Dependencies

The Cookiecutter template 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, changelog, icon)

Template Handles This

The Cookiecutter template'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_* Reserved for Zelos Agent
Custom Your variables Manifest [env]

Reserved Variables

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

Publishing Your Extension

The Cookiecutter template handles the entire release workflow for you.

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:

    • 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.

The Template Does It All

The Cookiecutter template 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.

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 Cookiecutter template 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"
  }
}

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:

import base64
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)

    # Use file
    with open("uploaded_config.json", "wb") as f:
        f.write(file_bytes)

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/
    ├── extension.toml      # Manifest
    ├── main.py            # Your code
    ├── config.json        # User configuration
    ├── extension.log      # Runtime logs
    ├── .venv/            # Python virtual environment
    ├── .cache/           # Package cache
    └── .last_config      # Backup configuration

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.

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