Operations
An operation coordinates ordered steps across selected devices in a project. This reference defines selectors, stages, steps, guards, lifecycle states, and API actions.
For the model, read Fleet Operations. To create and run one, follow Run a Fleet Operation.
The Nexigon Hub’s Operations capability is off by default. A self-hosted instance must enable instance.features.operations; subscription entitlements still apply when subscriptions are required.
Operation Model
An operation contains:
| Field | Description |
|---|---|
projectId | Project whose devices the operation can target. |
name | Human-readable name, 1 to 128 characters. |
description | Optional description, up to 4096 characters. |
enrollmentIntervalSecs | Optional interval for re-evaluating the selector. Omit it for snapshot enrollment. |
closeWhenConverged | Whether to complete after current targets become terminal. Defaults to true; may be false only for dynamic operations. |
selector | Device selector. |
stages | Ordered rollout stages. Must contain at least one stage. |
steps | Ordered steps for each selected target. Must not be empty. |
Each selected device receives a device operation, which records its ordered steps and progress. That device is a target of the fleet operation.
New operations start as Draft. Starting an operation changes it to Running.
Starting or advancing an operation takes effect immediately. A device reporting a result can release its next step immediately. Background reconciliation handles new enrollment, guard checks, property waits, and timeouts.
Target Selection
The target selector combines an explicit device allow-list, group membership, search, status, and JSON predicate expressions:
{
"deviceIds": ["d_..."],
"predicates": [
{
"kind": "Equals",
"left": {
"kind": "NamedValue",
"name": "com.example.rollout.ring"
},
"right": { "kind": "Literal", "value": "stable" }
}
]
}
If deviceIds is present, only those devices can match. All predicate expressions must evaluate to JSON true.
Optional groups conditions contain group IDs: all requires every listed group,
any requires at least one, and none excludes every listed group. Conditions
are combined with AND. Empty all and none impose no restriction; empty any
matches no devices. Omitting a condition imposes no restriction. Groups without
assignments, including deleted groups, are absent from every device.
Optional search matches a case-insensitive substring of device name, hostname,
or ID. Optional status matches the combined device status (healthy, warning,
error, or offline). These metadata conditions run before property predicates.
See Groups for catalog and membership management.
Predicates use the shared JSON value model. In operations, device properties are exposed as named values, so {"kind":"NamedValue","name":"..."} reads the current value of that device property.
Use Get with a JSON path to match inside a JSON property value:
{
"kind": "Equals",
"left": {
"kind": "Get",
"expr": {
"kind": "NamedValue",
"name": "dev.nexigon.ota.status"
},
"path": ["state"]
},
"right": { "kind": "Literal", "value": "completed" }
}
Version requirements are useful for version-gated rollouts. For example,
assume your application publishes its installed version in
com.example.app.version as a JSON string, such as "1.8.2". The following
predicate selects versions in the supported range:
{
"kind": "VersionRequirement",
"expr": {
"kind": "NamedValue",
"name": "com.example.app.version"
},
"requirement": ">=1.8.0, <2.0.0"
}
A matching device becomes a target. A non-matching device is not recorded.
Evaluation Errors
A selector evaluation error does not create a target. The error is available
through fleet_QueryOperationSelectionErrors, and a later enrollment pass
retries the device while the operation is running.
For example, a device that has not yet reported a referenced property produces a selection error. Reporting it separately makes incomplete selection visible to an operator, rather than silently narrowing the fleet to devices whose properties happened to be available.
| Context | Missing Value or Path | Other Evaluation Error |
|---|---|---|
| Selector | Record a selection error. | Record a selection error. |
| Wait predicate | Keep waiting. | Fail the step. |
| Guard predicate | Treat as a non-match. | Block the gate and report the error. |
| Update expression | Fail the step. | Fail the step. |
Previewing Targets
fleet_PreviewOperationTargets evaluates a selector against the current
devices of a project, given by projectId, without creating an operation. It
returns:
matchingCount: the number of matching devices.devices: the matching devices most recently first seen.errorCount: the number of devices whose predicates could not be evaluated.selectionErrors: those devices with their evaluation errors.
The optional limit, from 1 to 1000, bounds devices and selectionErrors
and defaults to 20. matchingCount covers the whole selection before stage
percentages apply, so a 5% stage receives only part of it. A preview does not
freeze membership; the operation evaluates the selector again when it enrolls
devices. The operation editor shows this preview while you define targets.
Target Modes
The target mode is a view of the operation’s enrollment interval rather than a separate setting.
Snapshot has no enrollment interval: the selector is evaluated exactly once, which freezes the rollout set. Use this when you want a fixed population.
Dynamic keeps re-evaluating the selector on its enrollment interval, so devices that start matching later are picked up. Enrollment only adds work. A device that stops matching retains its assigned
device operation. Retaining that work avoids abandoning a partially completed
workflow when a device’s properties or group membership change during rollout.
Set enrollmentIntervalSecs to make an operation dynamic and control how often it looks for new devices. Omitting it makes the operation a snapshot; there is no API-level one-hour default. The current UI initially fills this field with 3600, but clients choose whether to send it. An enrollment pass has to look at every device the operation has not enrolled yet, so it is proportional to the size of the project, not to the size of the result. The instance enforces a minimum interval, and an operation asking for a shorter one is simply enrolled no more often than policy allows.
closeWhenConverged defaults to true. For a dynamic operation, set it to false to keep enrolling future matches after the current targets finish. Snapshot operations always close on convergence.
An operation with no targets and no selection errors completes once it reaches convergence. A dynamic operation remains running for future matches only when closeWhenConverged is false.
If the selector fails for every device and the operation closes on convergence, it has no target work to wait for and finishes immediately. Check selection errors as well as target results. Transient errors are retried only while the operation remains running.
Stages
A stage defines a rollout ring:
{
"name": "canary",
"percentage": 5,
"autoAdvance": true,
"guard": {
"minSuccessPercent": 100,
"maxFailurePercent": 0,
"gates": []
}
}
| Field | Description |
|---|---|
name | Human-readable stage name, 1 to 128 characters. |
percentage | Percent of matching devices included in this stage, 1 to 100. Defaults to 100. |
autoAdvance | Automatically activate the next stage once this stage’s guard passes. |
guard | Optional guard checked before advancing to the next stage. |
Stage percentages are deterministic per operation and device. A sequence such as 1, 10, 100 behaves like rollout rings: the first stage selects roughly 1 percent, the second expands to roughly 10 percent, and the final stage expands to all matching devices. Because the percentages grow, the stages nest, and a device is only ever run once: it belongs to the first stage whose percentage contains it.
Enrolled targets are created immediately and assigned to a stage. Targets in
unactivated stages stay Pending. The target table therefore includes the
whole enrolled population before all stages have been activated.
The operation’s activeStageIndex is the highest activated stage index. All stages up to and including that index can progress at the same time. Advancing activates the next stage; it does not wait for targets in earlier activated stages to finish.
Steps
Each selected target executes the operation’s steps in order.
SetDeviceProperty
Sets a device property from Nexigon Hub:
{
"kind": "SetDeviceProperty",
"name": "dev.nexigon.ota.config",
"value": { "path": "my-repo/my-package/v2.0.0" },
"protected": true
}
protected defaults to false. The step succeeds once the property write succeeds.
UpdateDeviceProperty
Updates the current value of a device property from Nexigon Hub:
{
"kind": "UpdateDeviceProperty",
"name": "dev.nexigon.ota.config",
"mutation": {
"mutation": "MergePatch",
"path": [],
"expr": {
"kind": "Literal",
"value": {
"path": "my-repo/my-package/stable"
}
}
},
"protected": true
}
The mutation field uses the shared JSON mutation model.
Supported mutations are:
| Mutation | Meaning |
|---|---|
Set | Replace the value at path with the expression in expr. An empty path replaces the whole property value. |
MergePatch | Evaluate expr and apply it as an RFC 7396 JSON Merge Patch at path. An empty path patches the whole property value. |
Remove | Remove a value at a path. Missing values are ignored. |
protected is optional. When omitted, the existing protection flag is preserved; if the property does not exist yet, it defaults to false.
Path mutations look like this:
{
"kind": "UpdateDeviceProperty",
"name": "com.example.config",
"mutation": {
"mutation": "Set",
"path": ["features", "ota"],
"expr": { "kind": "Literal", "value": true }
}
}
If a mutation cannot be applied, for example because a path traverses a scalar value as if it were an object, the step and target fail with the mutation error.
RemoveDeviceProperty
Removes a device property from Nexigon Hub:
{
"kind": "RemoveDeviceProperty",
"name": "com.example.temporary-config"
}
The step succeeds once the property has been removed.
WaitForDeviceProperty
Waits until a predicate expression matches the device properties:
{
"kind": "WaitForDeviceProperty",
"predicate": {
"kind": "Equals",
"left": {
"kind": "Get",
"expr": {
"kind": "NamedValue",
"name": "dev.nexigon.ota.status"
},
"path": ["currentVersion"]
},
"right": { "kind": "Literal", "value": "2.0.0" }
},
"timeoutSecs": 1800
}
If timeoutSecs is set and the predicate does not match before the deadline, the step and target become TimedOut. If no timeout is set, Nexigon Hub keeps waiting.
Background reconciliation detects expired deadlines. Recording a timeout can lag behind its deadline by the reconciliation delay. When the requested timeout is shorter than the reconciliation interval, that delay can be large relative to the requested duration. Use timeouts to bound waiting, rather than to schedule work at an exact time.
DeviceCommand
Asks the device agent to run an on-demand command. For example, the following
step assumes a custom ota.update handler that accepts a version input:
{
"kind": "DeviceCommand",
"command": "ota.update",
"input": { "version": "2.0.0" },
"timeoutSecs": 1800
}
The supplied Rugix integration exposes nexigon.ota.check; select its desired
version through dev.nexigon.ota.config as shown in
Deploy System Updates.
The ota.update examples here illustrate an application-defined command contract.
The device must run Nexigon Agent with both commands and operation polling enabled. The command name must exist in the device command manifest. The command’s structured output is stored on the target when the step succeeds.
If the device does not report before the deadline, Nexigon Hub marks the step as TimedOut. timeoutSecs defaults to one hour and is also passed to the agent as the command handler’s execution timeout. Valid values are 1 second through 1 day.
DeviceTask
A device task is durable work scoped to one operation step:
{
"kind": "DeviceTask",
"task": "ota.state-machine",
"input": { "version": "2.0.0" },
"timeoutSecs": 7200
}
The task protocol supports intermediate progress and durable checkpoints. If a worker is interrupted while a step remains current, a later poll returns the same work item with its latest checkpoint.
Step Conditions
Every step can include a when expression:
{
"kind": "DeviceCommand",
"when": {
"kind": "Or",
"exprs": [
{
"kind": "Not",
"expr": {
"kind": "Exists",
"expr": {
"kind": "NamedValue",
"name": "dev.nexigon.ota.status"
},
"path": ["currentVersion"]
}
},
{
"kind": "NotEquals",
"left": {
"kind": "Get",
"expr": {
"kind": "NamedValue",
"name": "dev.nexigon.ota.status"
},
"path": ["currentVersion"]
},
"right": { "kind": "Literal", "value": "2.0.0" }
}
]
},
"command": "ota.update",
"input": { "version": "2.0.0" }
}
when runs the step only when the expression evaluates to JSON true. A false result records the step as Skipped and continues with the next step. An evaluation error fails the step.
Guards
Guards are checked for the frontier stage, which is the stage at activeStageIndex. Manual advancement checks the latest persisted frontier guard status before activating the next stage. When a stage sets autoAdvance to true, background reconciliation performs the same guard check and activates the next stage once it passes. A scheduled gate can hold advancement until a specific timestamp, so stages can be opened automatically after both the schedule and the observed criteria pass.
A guard can require:
| Field | Meaning |
|---|---|
minSuccessPercent | Minimum percent of frontier-stage targets that must have succeeded. |
maxFailurePercent | Maximum percent of frontier-stage targets that may have failed or timed out. |
gates | Additional observed gates that must pass. |
A device property gate requires enough frontier-stage targets to match a property predicate:
{
"kind": "DeviceProperty",
"predicate": {
"kind": "Equals",
"left": {
"kind": "Get",
"expr": {
"kind": "NamedValue",
"name": "dev.nexigon.ota.status"
},
"path": ["state"]
},
"right": { "kind": "Literal", "value": "completed" }
},
"minMatchPercent": 95
}
A NotBefore gate passes once hub server time has reached the scheduled timestamp:
{
"kind": "NotBefore",
"notBefore": "2026-07-01T09:00:00Z"
}
Use it with autoAdvance to schedule a stage transition, or without autoAdvance to prevent manual advancement before the timestamp.
Status Values
Operation statuses:
| Status | Meaning |
|---|---|
Draft | Created but not started. |
Running | The rollout is progressing. |
Paused | Halted without being undone. See below. |
Completed | The final stage has been activated and all targets are terminal. |
Failed | The rollout converged, but at least one target or selector evaluation failed. |
Cancelled | Operation was cancelled. |
Target statuses:
| Status | Meaning |
|---|---|
Pending | The target has not been released yet, because the rollout has not reached its stage. |
Running | The target is applying steps or waiting. |
Succeeded | The target completed all operation steps. |
Failed | A step failed. |
TimedOut | A step deadline was exceeded. |
Cancelled | The target was cancelled, or its device was deleted. |
A target as a whole is never Skipped. Individual steps are, when a step condition excludes them, which advances the target to the next step. Step statuses are Pending, Running, Succeeded, Failed, TimedOut, and Skipped.
Pausing
Pausing stops enrollment, target advancement, and new work delivery. Completed work and operation state are retained.
Results for work already received by a device are still recorded.
Resume with fleet_StartOperation. An in-flight step’s timeout clock restarts
on resume, so the pause itself cannot cause the step to time out.
API Actions
User-facing automation uses these actions:
| Action | Purpose |
|---|---|
fleet_QueryOperations | List operations for a project. |
fleet_GetOperation | Get an operation’s definition, stages, steps, and progress. |
fleet_QueryOperationTargets | List the devices the operation applies to, and how each is faring. |
fleet_QueryOperationSelectionErrors | List the devices the selector could not be evaluated against. |
fleet_PreviewOperationTargets | Count and sample the devices a selector currently matches. |
fleet_CreateOperation | Create a draft operation. |
fleet_StartOperation | Start an operation, or resume a paused one. |
fleet_AdvanceOperationStage | Activate the next stage if the frontier guard passes. |
fleet_PauseOperation | Pause a running operation. |
fleet_CancelOperation | Cancel a draft, running, or paused operation. |
Targets and selection errors have separate queries; fleet_GetOperation
returns the operation definition and summary.
Device agents use these actions internally:
| Action | Purpose |
|---|---|
devices_ClaimOperationWork | Claim device-executable work. |
devices_ReportOperationStep | Report the result of a device-executed step. |
Project viewers can query operations and preview targets. Creating, starting, pausing, advancing, and cancelling requires device-management permissions for the project. Polling and reporting are restricted to the device itself.
Nexigon Hub checks permissions and the step’s when expression before offering
device-executable work.
Nexigon Agent Configuration
Operation polling is configured in Nexigon Agent:
[operations]
enabled = true
poll-interval-secs = 60enabled defaults to false; agents must opt in explicitly. poll-interval-secs defaults to 60.
The poll interval is an idle interval. Whenever a poll returns work, the agent carries it out and polls again straight away, so a multi-step operation runs at the speed of its steps rather than of this interval. What the interval bounds is how soon an idle device notices work that appeared while it had nothing to do.
Every device polls on this interval whether or not there is anything for it, so the cost falls on Nexigon Hub and grows with the size of the fleet. Choose a shorter interval only when faster response justifies the additional load across your fleet.
DeviceCommand steps also require commands to be enabled:
[commands]
enabled = true
directory = "/etc/nexigon/agent/commands"See Device Commands for command definitions and command result semantics. The bundled agent does not execute DeviceTask steps.