Skip to content

How to Use Actions

Actions let you expose Python functions as interactive operations in the Zelos App, with rich input validation, helpful UI widgets, and clean results.

Python Only

The following APIs are only available in the Python SDK.

Quick Start

from zelos_sdk import init, action

@action("Read Voltage", "Read voltage from a power rail")
@action.select("rail", choices=["3v3", "5v0", "12v0"])
def read_voltage(rail: str):
    values = {"3v3": 3.28, "5v0": 5.02, "12v0": 11.95}
    return values[rail]

# Serve actions to the Agent (blocks this process)
init(actions=True, block=True)

Run your script, then open the Actions panel in the Zelos App.

Core Concepts

  • Decorate a function with @action(title, description) to expose it.
  • Add input decorators (e.g., @action.number, @action.select) to define typed parameters with validation and UI hints.
  • Serve actions via init(actions=True, block=True) or the Actions client.

Fields and Validation

All field decorators support common options: required (default True), default, title, description, and widget.

  • @action.text(name, min_length, max_length, pattern)
  • @action.email(name)
  • @action.number(name, minimum, maximum)
  • @action.integer(name, minimum, maximum)
  • @action.boolean(name)
  • @action.date(name)
  • @action.select(name, choices, depends_on)
  • @action.file(name, accept)
  • @action.files(name, accept)
  • @action.array(name, item, min_items, max_items, unique_items)
  • @action.object(name, properties)
  • Generic: @action.input(name, type="...", ...) for custom/registered types

Example:

from zelos_sdk import action

@action("Configure Supply", "Set output and limits")
@action.number("voltage", minimum=1.0, maximum=30.0)
@action.number("current_limit", minimum=0.1, maximum=5.0, default=1.0)
@action.boolean("enable_output", default=False)
def configure_supply(voltage: float, current_limit: float = 1.0, enable_output: bool = False):
    return {"voltage": voltage, "current_limit": current_limit, "enabled": enable_output}

UI Widgets

Select the best widget for the input using widget="...":

Widget Field Types Purpose
text text, email Single-line input
textarea text Multi-line input
password text Hidden input
number, updown, range number/integer Numeric entry, spinner, slider
select, radio, multi_select select Dropdown, radio, multi-choice
checkbox, toggle boolean Boolean inputs
date date Date picker
file, files file(s) Uploads

Example with widgets:

from zelos_sdk import action

@action("Configure Logging")
@action.text("log_prefix", max_length=32)
@action.text("log_filter", widget="textarea")
@action.text("admin_password", widget="password")
@action.boolean("enable_remote", widget="toggle", default=False)
@action.select("log_level", choices=["DEBUG","INFO","WARN","ERROR"], widget="radio")
@action.integer("rotation_size_mb", minimum=1, maximum=1000, widget="updown", default=100)
@action.number("compression_ratio", minimum=0.1, maximum=1.0, widget="range", default=0.8)
def configure_logging(log_prefix: str, log_filter: str, admin_password: str,
                      enable_remote: bool = False, log_level: str = "INFO",
                      rotation_size_mb: int = 100, compression_ratio: float = 0.8):
    return {
        "prefix": log_prefix,
        "level": log_level,
        "rotation_mb": rotation_size_mb,
        "compression": compression_ratio,
        "remote_enabled": enable_remote,
    }

Dynamic Fields

Use functions as choices and declare dependencies with depends_on.

from zelos_sdk import action

def get_channels(sensor_type: str):
    return {
        "temperature": ["ch1_cpu", "ch2_ambient"],
        "pressure": ["ch1_intake", "ch2_exhaust"],
    }.get(sensor_type, [])

@action("Read Sensor Channel")
@action.select("sensor_type", choices=["temperature", "pressure"])
@action.select("channel", choices=get_channels, depends_on="sensor_type")
def read_sensor_channel(sensor_type: str, channel: str):
    return {"sensor_type": sensor_type, "channel": channel}

Organizing and Registration

Class-based actions keep state and are namespaced when registered.

from zelos_sdk import action, actions_registry, init

class Battery:
    def __init__(self):
        self.status = {"soc_percent": 85.2, "voltage": 398.4}

    @action("Get Status")
    def get_status(self):
        return self.status

    @action("Set Mode")
    @action.select("mode", choices=["normal", "fast", "storage"])
    def set_mode(self, mode: str):
        self.status["mode"] = mode
        return {"mode": mode}

bms = Battery()
actions_registry.register(bms, name="bms")  # bms/get_status, bms/set_mode
init(actions=True, block=True)

Manual registration for standalone functions:

from zelos_sdk import actions_registry
actions_registry.register(read_voltage, name="sensors")
namespaces = actions_registry.list_namespaces()  # e.g., {"sensors": ["read_voltage"], ...}

Custom registries and serving:

from zelos_sdk.actions import ActionsRegistry, ActionsClient

custom_registry = ActionsRegistry()
custom_registry.register(read_voltage, name="power")

client = ActionsClient()
client.serve("power_management", actions_registry=custom_registry)

Errors and Validation

Built-in validators enforce ranges and formats. For cross-field rules, raise a ValidationError.

from zelos_sdk import action
from zelos_sdk.actions.types import ValidationError

@action("Configure Converter")
@action.number("vin", minimum=12, maximum=400)
@action.number("vout", minimum=3.3, maximum=48)
@action.select("kind", choices=["boost", "buck"])
def configure_converter(vin: float, vout: float, kind: str):
    if kind == "boost" and vout <= vin:
        raise ValidationError("vout", "Boost requires vout > vin")
    if kind == "buck" and vout >= vin:
        raise ValidationError("vout", "Buck requires vout < vin")
    return {"vin": vin, "vout": vout, "kind": kind}

Structured success/error using ActionResult:

from zelos_sdk import action
from zelos_sdk.actions import ActionResult

@action("Check Power Safety")
@action.number("voltage", minimum=300, maximum=500)
@action.number("current", minimum=-200, maximum=200)
def check_power_safety(voltage: float, current: float) -> ActionResult:
    power_w = voltage * abs(current)
    if power_w > 50_000:
        return ActionResult.error(f"Power too high: {power_w:.0f}W (max 50kW)")
    return ActionResult.success(f"OK: {power_w:.0f}W")

Return Types

Actions can return numbers, strings, booleans, dicts/lists (JSON-serializable), or ActionResult.

@action("Examples")
def examples():
    return {
        "number": 25.3,
        "message": "System OK",
        "flags": True,
        "data": {"cpu_temp": 45.2, "fan": 2400}
    }

File Uploads

Files are received as data URLs; decode to bytes as needed.

from zelos_sdk import action
import base64

@action("Upload Firmware")
@action.file("firmware", accept=".bin,.hex,.elf")
def upload_firmware(firmware: str):
    header, data = firmware.split(",", 1)
    content = base64.b64decode(data)
    return {"size_bytes": len(content)}

Custom Field Types

Register custom types for domain-specific validation.

from zelos_sdk import action
from zelos_sdk.actions.types import FieldType
from zelos_sdk.actions.fields import BaseField, register_field_type, ValidationError

@register_field_type("can_id")
class CANIdField(BaseField):
    def __init__(self, name: str, extended: bool = False, **kwargs):
        super().__init__(name, **kwargs)
        self.field_type = FieldType.STRING
        self.extended = extended
    def validate(self, value, form_data=None):
        value = super().validate(value, form_data)
        if value is None:
            return value
        clean = value.replace("0x", "").replace("0X", "").upper()
        import re
        if not re.match(r"^[0-9A-F]+$", clean):
            raise ValidationError(self.name, "CAN ID must be hex", "format")
        limit = 0x1FFFFFFF if self.extended else 0x7FF
        if int(clean, 16) > limit:
            raise ValidationError(self.name, "CAN ID out of range", "range")
        return value

@action("Configure CAN Message")
@action.input("message_id", type="can_id", extended=False)
@action.text("signal_name", pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$")
@action.integer("data_length", minimum=1, maximum=8)
def configure_can_message(message_id: str, signal_name: str, data_length: int):
    return {"message_id": message_id, "signal_name": signal_name, "dlc": data_length}

Auto-Discovery via Entry Points

Expose actions automatically when your package is installed.

Add to pyproject.toml:

[project.entry-points."zelos_sdk.actions"]
my_tools = "my_tools.actions"

In my_tools/actions.py:

from zelos_sdk import action, init

@action("Read VIN")
@action.text("vin", pattern=r"^[A-HJ-NPR-Z0-9]{17}$")
def decode_vin(vin: str) -> dict:
    return {"vin": vin, "manufacturer": vin[:3]}

init(actions=True, block=True)

Best Practices

  • Keep actions responsive; offload long-running work to background tasks.
  • Prefer explicit validators (minimum, pattern, choices) for better UX.
  • Use classes to persist hardware/session state across calls.
  • Namespace registrations to keep large action sets organized.