Commands: On-Demand Device Operations, the Primitives-First Way
Nexigon is built around composable primitives with ready-made features layered on top. Commands add a new primitive for imperative, on-demand device operations: run diagnostics, restart services, deploy apps, all remotely with real-time log streaming, structured results, and audit logging.
- commands
- architecture
- engineering
Nexigon is built around a primitives-first architecture. At the foundation are composable building blocks (properties, events, repositories, device tokens) that you combine into exactly the workflows your deployment needs. We ship ready-made features on top of these primitives, things like OTA update recipes, app lifecycle management, and service monitoring. But because every feature is built from the same primitives, you can always look under the hood, understand what it does, and customize or replace it when your requirements diverge. The primitives are the foundation. The features are convenience layered on top.
Most of these primitives are declarative. You set a property, the device reads it. You emit an event, the cloud observes it. But not everything fits that mold. Sometimes you need to reboot a device right now. Run a diagnostic to troubleshoot a connected component while the customer is on the phone. Trigger an OTA check without waiting for the next polling interval. Deploy an application container to a specific device to test a new release.
These are imperative actions. They have a start, an end, a result, and often log output you want to see in real time. Trying to shoehorn them into a property-based state machine adds complexity without adding clarity. That is why we built commands: a new primitive for on-demand, remotely-executed operations on devices.
What Commands Are
A command is a named operation that a device exposes. It receives a JSON input, executes logic on the device, and returns a JSON output along with a status and log output. Commands are invoked from the cloud, either through the UI, the API, or programmatically via an SDK, and run on the device through the Nexigon Agent.
Commands are disabled by default. You opt in explicitly:
/etc/nexigon/agent.toml[commands] enabled = true
Once enabled, the agent publishes a manifest of all available commands as the device property dev.nexigon.commands. This makes commands discoverable through the same properties system used for all other device metadata. No separate discovery mechanism, no special API. Just a property.
Defining a Command
A command is a TOML file in /etc/nexigon/agent/commands. Each file defines one command with its metadata, an optional input schema, and a handler executable.
Here is a simple diagnostic command that reports system uptime:
/etc/nexigon/agent/commands/uptime.toml[command] name = "uptime" description = "Get system uptime and load average" category = "diagnostics" [exec] handler = ["/usr/libexec/nexigon/commands/uptime.sh"] timeout = 5
And the handler:
/usr/libexec/nexigon/commands/uptime.sh#!/usr/bin/env bash echo "Collecting uptime info..." >&2 uptime_secs=$(cat /proc/uptime | awk '{print int($1)}') load=$(cat /proc/loadavg | awk '{print $1, $2, $3}') echo "{\"type\": \"Output\", \"data\": {\"uptime_secs\": ${uptime_secs:-0}, \"load\": \"${load:-unknown}\"}}"
The Handler Protocol
The protocol between the agent and the handler is deliberately simple:
- Stdin: The JSON input as a single line. If no input is provided, stdin is closed immediately.
- Stdout: NDJSON lines. Each line is a JSON object with a
typefield. TheOutputtype carries the command’s result. If multiple output lines are written, the last one wins. - Stderr: Captured as a log tail (last 8 KB) and streamed back to the caller in real time. Use it for progress messages and diagnostics.
- Exit code: Zero means success, non-zero means error.
This means your handler can be a Bash script, a Python program, a compiled binary, anything that reads stdin and writes to stdout. The agent does not care.
Schemas
Commands can declare JSON Schemas for their input and output. The schemas are published as part of the command manifest, making them available to UI and API clients for validation and documentation. Here is an example with an input schema:
/etc/nexigon/agent/commands/restart-service.toml[command] name = "restart-service" description = "Restart a systemd service" category = "services" [input] schema = ''' { "type": "object", "properties": { "unit": { "type": "string", "description": "Name of the systemd unit to restart" } }, "required": ["unit"] } ''' [exec] handler = ["/usr/libexec/nexigon/nexigon-systemd-restart"] timeout = 30
The handler reads the input from stdin:
/usr/libexec/nexigon/nexigon-systemd-restart#!/usr/bin/env bash set -euo pipefail read -r input unit=$(echo "$input" | jq -r '.unit') systemctl restart "$unit"
The schema is not just documentation. It describes what fields exist, what types to expect, and which fields are required. API clients can use it to construct valid input, and UIs can render input forms from it. Output schemas work the same way: add an [output] section with a schema field and the manifest will describe the shape of the result. And because schemas are part of the command manifest published as a device property, they are always in sync with what the device actually supports.
Pre-Made Commands
Commands are a primitive, but you do not have to build everything yourself. As with the other primitives, we layer convenience on top. Our Yocto and Rugix Bakery integrations ship pre-made commands that cover common operations out of the box. These are not a separate class of built-in commands. They are regular TOML definitions and handler scripts, the same format you use for your own commands, packaged as recipes you can include in your image. Use them as-is, or treat them as a reference for building your own.
Power Management
The nexigon-power recipe gives you nexigon.power.reboot and nexigon.power.shutdown:
[command]
name = "nexigon.power.reboot"
description = "Reboot the system"
category = "power"
[exec]
handler = ["reboot"]
timeout = 10
Simple enough. The handler is just the reboot command. No wrapper script needed. The nexigon.power.shutdown command works the same way.
Systemd Service Management
The nexigon-systemd recipe adds commands for listing, inspecting, and restarting systemd units. The Nexigon UI builds on these to provide a Services tab where you can see the state of all services on a device and restart them with a click. Under the hood, that tab is just invoking the same commands you could call from the API or SDK.
Rugix App Lifecycle
The nexigon-rugix-apps recipe is where things get interesting. It ships seven commands that cover the full lifecycle of Rugix Apps: list, info, deploy, start, stop, rollback, and remove.
The deploy command is a good example of how commands compose with other primitives. Its handler script takes a Nexigon package version ID as input, resolves the download URL through the repository system, streams progress to stderr for real-time log output, and installs the app via rugix-ctrl. It is a short shell script that you can read, modify, or replace entirely if your deployment workflow differs.
OTA Update Check
Both our Rugix and RAUC OTA recipes ship a nexigon.ota.check command that triggers an immediate update check. The Rugix variant, for example, is a one-liner that kicks off the OTA service:
[command]
name = "nexigon.ota.check"
description = "Trigger an OTA update check"
category = "system"
[exec]
handler = ["systemctl", "start", "--no-block", "nexigon-rugix-ota.service"]
timeout = 10
The actual update logic is a shell script that composes properties, events, and repositories into a full update workflow. We covered it in detail in Primitives-First: Why Nexigon Doesn’t (Yet) Ship a Magic Update Button. Commands do not replace that workflow. They give you a way to trigger it on demand. And if you use a different update framework entirely, you can define your own nexigon.ota.check that does whatever your setup requires.
Invoking Commands
From the UI
The Nexigon Hub UI shows available commands for each device based on the command manifest. You can select a command, provide the input, and invoke it. Log output streams back in real time, and the result is displayed when the command finishes.
From the API
Commands use a WebSocket-based protocol. You connect to the command endpoint, send an Invoke frame with the command name and input, and receive Log frames (real-time stderr output) and a terminal Done frame with the status, output, and log tail.
From the Python SDK
The Python SDK provides a simple invoke-and-wait method and a low-level streaming interface:
# Simple invocation
output = client.invoke_device_command(
"d_...",
"nexigon.systemd.status",
input={"unit": "nginx.service"},
timeout_secs=15,
)
print(output.status) # Ok or Error
print(output.data) # Structured JSON output
# With real-time log streaming
def handle_log(log_frame):
print(f"[device] {log_frame.message}")
output = client.invoke_device_command(
"d_...",
"nexigon.rugix-apps.deploy",
input={"versionId": "pv_..."},
stream_log=True,
timeout_secs=120,
on_log=handle_log,
)
This makes it straightforward to integrate command invocations into CI/CD pipelines, automation scripts, or custom tooling.
Primitives All the Way Down
Commands are the fifth primitive in Nexigon, joining properties, events, repositories, and device tokens. And like the other four, they follow the same pattern: a simple, well-defined building block at the foundation, with ready-made functionality layered on top.
The pre-made commands we ship through our Yocto and Rugix Bakery integrations are the convenience layer. They give you power management, service control, app lifecycle, and OTA triggers out of the box. But they are not a black box. They are regular TOML definitions and handler scripts, the same format you use for your own commands. If nexigon.systemd.restart does not match your needs, you define a custom command with the same name and it takes precedence. The pre-made version is a starting point, not a boundary.
The command manifest is published as a device property, making it discoverable through the same mechanism everything else in Nexigon uses. The handler protocol is stdin/stdout/stderr, so any language works. The input and output are JSON, so they integrate naturally with everything from jq on the command line to structured API clients.
There is no command framework to learn. No agent plugin system to implement. No opinionated orchestration layer deciding when and how your commands run. You define what the command does, Nexigon gives you authenticated remote execution with real-time log streaming and audit logging, and the rest is up to you.
Getting Started
Commands are available today. To start using them:
- Enable commands in your agent configuration.
- Drop a TOML file and a handler script into the commands directory.
- If you are using our Yocto or Rugix Bakery integrations, include the pre-made command recipes for power management, systemd, app lifecycle, and OTA triggers.
The commands reference covers the full configuration and protocol details. The Python SDK guide shows how to invoke commands programmatically.