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