Skip to content

Extension Configuration

An agent extension can take user configuration. You describe it with a JSON Schema file, config.schema.json. The Zelos App turns that schema into a configuration form with widgets, validation, and help text. Your code reads the saved values with load_config().

How Configuration Flows

  1. You ship config.schema.json and name it in extension.toml:

    [config]
    schema = "config.schema.json"
    
  2. A user opens Configure on the installed extension. The form is built from your schema. It starts from the last saved configuration for that agent when one exists, otherwise from the default values in your schema. See Extensions for the user's side.

  3. The user submits the form. Submitting starts a stopped extension or restarts a running one.
  4. The agent validates the configuration against your schema. Invalid configuration stops the start, and a running extension keeps running. A start with no configuration at all skips this check.
  5. The agent writes the configuration to config.json in the extension's install directory and passes its path in ZELOS_CONFIG_PATH.
  6. Your code calls load_config(), which fills in schema defaults and validates again.

From the CLI, pass configuration with zelos extensions start <id> --config '<json>' or --config-file <path>. zelos extensions config <id> prints the schema and the last saved configuration.

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

Where the Schema Is Read

Reader What it reads
Agent (on start and restart) [config].schema when set. Otherwise config.schema.json in the code directory, if it exists. A declared path that does not exist is an error.
zelos extensions package The [config].schema file, plus anything under [package].paths.
Marketplace (at release ingestion) The [config].schema file, only when declared.
load_config() in your code config.schema.json in the working directory, unless you pass schema_path=.

Always declare [config]

The agent finds config.schema.json without [config] during local development. A packaged release leaves the file out unless [config] names it or a [package] path covers it. Declare [config] so local and released behavior match.

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 it in extension.toml:

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

3. Read it in your extension:

from zelos_sdk.extensions import load_config

config = load_config()
interval = config["interval"]  # 60 unless the user changed it

Loading Configuration

load_config() loads, completes, and validates the configuration:

from zelos_sdk.extensions import load_config

config = load_config()

What it does:

  • Reads the configuration from ZELOS_CONFIG_PATH. Without that variable, it reads config.json in the working directory.
  • Reads the schema from config.schema.json in the working directory.
  • Fills in default values from the schema, including nested objects, $ref targets, and array items up to minItems.
  • Validates the result against the schema and raises ConfigValidationError on failure.
  • Returns a plain dict.

A missing configuration file reads as {}. A missing schema file skips defaults and validation.

Optional arguments:

Argument Default Use
config_path "config.json" Read a different configuration file. ZELOS_CONFIG_PATH applies only while this is the default.
schema_path "config.schema.json" Read the schema from another path. Pass it when [config].schema is not config.schema.json or workdir is not the project root.
overrides None A dict deep-merged over the result, then validated again.

ConfigValidationError has errors, a list of messages, and field_errors, a dict from field path to message.

When you run the extension yourself with just dev, put a config.json next to main.py to test with specific values.

Schema Validation

The agent and load_config() validate with a standard JSON Schema validator. The marketplace checks that the schema is valid JSON and a valid JSON Schema (Draft 2020-12) before it publishes a release. A release with an invalid declared schema is not published.

ui:* keys are form hints. Validators ignore them.

Check your schema before you release:

python -m json.tool config.schema.json > /dev/null
zelos extensions install-local .

Then open Configure in the app and submit the form once.

UI Hints

Add these ui:* keys to a property to shape its field:

Key Applies to Effect
ui:widget Any field Pick a widget. See Widgets.
ui:help Any field Help text in a tooltip. Without it, description is used.
ui:placeholder Text and number inputs Placeholder text.
ui:rows textarea Visible rows. Default 4.
ui:inline radio Lay out the options on one line.
ui:showValue range false hides the current value next to the slider.
ui:encoding file, files dataurl (default), base64 (no data: prefix), or text (file contents as text).
ui:maxFiles files Most files a user can pick. Default 10.
ui:hideLabel Any field Hide the field's label.
ui:options Depends on widget Widget options, for example accept, createDirectory, collapsed, action, autoconfig.

On an object property, "ui:options": {"collapsed": true} starts the group closed behind its title.

Widgets

Each JSON type has a default widget. Set ui:widget to pick another. Use the lowercase names below. A widget name the app does not know falls back to the default widget for that field.

ui:widget value Widget Default for
text Single-line text input type: "string", "number", and "integer"
textarea Multi-line text -
password Hidden text -
email Email input format: "email"
url URL input format: "uri"
number, updown Number input with step controls -
range, slider Slider -
checkbox Checkbox type: "boolean"
toggle, switch Toggle switch -
select, dropdown Dropdown enum
radio Radio buttons -
multiselect Searchable multi-select -
file Single file upload format: "data-url"
files Multiple file upload Array of format: "data-url" items
file-picker, filepicker, pathpicker File path picker -
folder-picker, folderpicker, directory-picker, directorypicker Folder path picker -
action-choices Text input with choices from one of your actions -
date Date picker format: "date"
datetime Date and time picker format: "date-time"
time Time picker format: "time"

Text

{
  "notes": {
    "type": "string",
    "title": "Notes",
    "ui:widget": "textarea",
    "ui:rows": 6,
    "ui:placeholder": "Enter a detailed description..."
  },
  "password": {
    "type": "string",
    "title": "Password",
    "minLength": 8,
    "ui:widget": "password",
    "ui:help": "Minimum 8 characters"
  },
  "endpoint": {
    "type": "string",
    "title": "API Endpoint",
    "format": "uri"
  }
}

Numbers

A number field without ui:widget is a text input that accepts numbers. "ui:widget": "number" adds step controls that step by multipleOf when set.

range draws a slider. It needs minimum and maximum. multipleOf sets the step size:

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

The agent accepts floating-point values that are within rounding error of a multipleOf step.

Booleans

{
  "enabled": {
    "type": "boolean",
    "title": "Enable Feature",
    "default": true,
    "ui:widget": "toggle"
  }
}

Choices

An enum renders as a dropdown. Use radio for two to five options:

{
  "priority": {
    "type": "string",
    "title": "Priority",
    "enum": ["low", "normal", "high"],
    "default": "normal",
    "ui:widget": "radio",
    "ui:inline": true
  }
}

For several values from a fixed list, use an array of enum items with "ui:widget": "multiselect":

{
  "features": {
    "type": "array",
    "title": "Enabled Features",
    "items": {
      "type": "string",
      "enum": ["logging", "metrics", "alerts", "reports"]
    },
    "uniqueItems": true,
    "ui:widget": "multiselect"
  }
}

Choices From an Action

action-choices fills a text field's suggestions from a standalone action in your own extension. The agent runs the action on the extension's host, even while the extension is stopped. Use it for values only that host can see, such as network interfaces or CAN buses:

{
  "interface": {
    "type": "string",
    "title": "Interface",
    "ui:widget": "action-choices",
    "ui:options": { "action": "Packet/list_interfaces" }
  }
}
  • ui:options.action names the action as <extension name>/<action path>. The form refuses an action from another extension.
  • ui:options.params is passed to the action as its arguments.
  • The field stays a text input, so a user can type a value the agent cannot see.

For the action side, see Choices for a Config Field.

Auto-Configure

A root-level autoconfig option adds an Auto-configure button above the form. The button runs a standalone action in your extension, and the keys of the config object it returns replace the form's values. Nothing runs until the user clicks the button, and the user reviews the result before saving:

{
  "type": "object",
  "ui:options": { "autoconfig": "Packet/auto_config" },
  "properties": {
    "interfaces": { "type": "array", "items": { "type": "object" } }
  }
}

The action must belong to the same extension. For the action side, see Auto-configure from an Action.

File and Folder Paths

file-picker and folder-picker open a native browser and store the chosen path as a string. They do not upload anything, so the path must make sense on the agent's machine.

{
  "bitstreamPath": {
    "type": "string",
    "title": "Bitstream File",
    "ui:widget": "file-picker",
    "ui:placeholder": "C:/path/to/file.bit"
  },
  "outputDir": {
    "type": "string",
    "title": "Output Directory",
    "ui:widget": "folder-picker",
    "ui:options": { "createDirectory": true }
  }
}

createDirectory lets the user create a new folder from the picker.

File Uploads

format: "data-url" uploads file contents into the configuration.

Widget Schema Value Size limit
file type: "string", format: "data-url" One data URL 10 MB
files type: "array", items type: "string", format: "data-url" Array of data URLs 50 MB total, 10 files unless ui:maxFiles
{
  "dbcFile": {
    "type": "string",
    "title": "DBC File",
    "format": "data-url",
    "ui:options": { "accept": ".dbc" }
  },
  "logFiles": {
    "type": "array",
    "title": "Log Files",
    "items": { "type": "string", "format": "data-url" }
  }
}

ui:options.accept takes a comma-separated list of extensions (.dbc) or MIME types. ui:encoding changes the stored value; see UI Hints.

Decode uploads in your extension and write them to ZELOS_DATA_DIR. The code directory is read-only.

import base64
import os
from pathlib import Path

from zelos_sdk.extensions import load_config

config = load_config()
data_url = config.get("dbcFile")

if data_url and data_url.startswith("data:"):
    # "data:application/octet-stream;base64,SGVsbG8="
    header, encoded = data_url.split(",", 1)
    file_bytes = base64.b64decode(encoded)

    data_dir = Path(os.environ["ZELOS_DATA_DIR"])
    dbc_path = data_dir / "uploaded.dbc"
    dbc_path.write_bytes(file_bytes)

Dates and Times

{
  "startDate": {
    "type": "string",
    "title": "Start Date",
    "format": "date"
  }
}

Common Patterns

API Settings

{
  "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 Groups

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

Lists of Objects

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

Best Practices

  1. Give every optional field a default, so a first start works without edits.
  2. Use description for one line of inline help and ui:help for longer guidance.
  3. Constrain inputs with minLength, minimum, maximum, pattern, and enum.
  4. Group related settings in nested objects.
  5. Pick widgets that match the data: toggles for booleans, radio buttons for a few options, pickers for paths.
  6. Install locally and open Configure to check the form before you release.

Troubleshooting

A widget does not appear:

  • Check that type fits the widget. range needs type: "number" or "integer" plus minimum and maximum. radio needs an enum.
  • Use a lowercase name from the widget table. An unknown name falls back to the default widget.

A multi-value field saves one value:

  • Add "ui:widget": "multiselect" to the array property.

File uploads do not work:

  • Use "format": "data-url", not "format": "file".
  • One file: type: "string" with the format. Several files: type: "array" with items: {"type": "string", "format": "data-url"}.

Start fails with Configuration validation failed:

  • The message lists each failing field path. Fix the value in Configure, or fix the schema if the default itself is invalid.

The extension starts, then exits with Configuration validation failed:

  • It was started with no configuration, so the agent skipped validation and load_config() raised inside the extension. A required field without a value is the usual cause. Start it from Configure, or pass --config or --config-file.

Start fails with Config schema file '<path>' not found in extension:

  • [config].schema names a file that is not in the installed code. For a release, check that the archive contains it with zelos extensions package --list ..