Skip to main content
Documentation

Operations

Operations are asynchronous fleet workflows. They let you define a server-side plan, start it, monitor progress per target device, and activate staged rollout rings manually or automatically.

For a task-oriented introduction, see the Operations guide.

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

FieldDescription
projectIdProject whose devices the operation can target.
nameHuman-readable name, 1 to 128 characters.
descriptionOptional description, up to 4096 characters.
enrollmentIntervalSecsOptional interval for re-evaluating the selector. Omit it for snapshot enrollment.
closeWhenConvergedWhether to complete after current targets become terminal. Defaults to true; may be false only for dynamic operations.
selectorDevice selector.
stagesOrdered rollout stages. Must contain at least one stage.
stepsOrdered steps for each selected target. Must not be empty.

A fleet operation never touches a device itself. It is a factory for device operations: it decides which devices get one, when each may run, and when the rollout as a whole is done. A device with a device operation is what this reference calls a target.

New operations start as Draft. Starting an operation changes it to Running.

Anything that changes an operation acts on it at once: starting or advancing a rollout takes effect immediately, and a device reporting a step result immediately releases its next step. A background task handles only what nothing else triggers, all of it time-based: enrolling newly matching devices, re-evaluating guards, noticing that a waited-for property has changed, and timing steps out.

Target Selection

The selector can combine an explicit device allow-list with 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.

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:

{
  "kind": "VersionRequirement",
  "expr": {
    "kind": "NamedValue",
    "name": "dev.nexigon.agent.version"
  },
  "requirement": ">=1.8.0, <2.0.0"
}

A device the selector matches becomes a target, which is to say it is given a device operation. A device the selector cleanly does not match is recorded nowhere.

A device the selector fails to evaluate is neither: the operation cannot establish that it even applies to it, so the device gets no device operation and the error is recorded separately, queryable with fleet_QueryOperationSelectionErrors. This makes a broken selector visible instead of silently narrowing the fleet, and the device is retried on the next enrollment pass. A missing value in a wait predicate keeps the step waiting; other wait evaluation errors fail the step. A missing value in a guard predicate is a non-match, while other guard evaluation errors block the gate and are reported. Update-expression errors fail the step.

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 ever adds: a device that stops matching keeps the device operation it already has, because abandoning half-finished work on a device is worse than finishing it.

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.

Note that an operation set to close on convergence and whose selector errored on every device has no targets, therefore nothing to converge, and completes immediately. A transient selector error, such as a device that has not yet reported the property the selector reads, is only retried by an operation that is still running.

Stages

A stage defines a rollout ring:

{
  "name": "canary",
  "percentage": 5,
  "autoAdvance": true,
  "guard": {
    "minSuccessPercent": 100,
    "maxFailurePercent": 0,
    "gates": []
  }
}
FieldDescription
nameHuman-readable stage name, 1 to 128 characters.
percentagePercent of matching devices included in this stage, 1 to 100. Defaults to 100.
autoAdvanceAutomatically activate the next stage once this stage’s guard passes.
guardOptional 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.

A stage is a gate, not a batch. Every enrolled device gets its device operation immediately, tagged with the stage it belongs to, and one whose stage the rollout has not reached simply sits at Pending. It is not missing or planned; it exists and is gated shut. So the target table shows the whole rollout from the start, and advancing a stage creates nothing: it opens a gate.

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 the hub:

{
  "kind": "SetDeviceProperty",
  "name": "dev.nexigon.ota.desired",
  "value": { "version": "2.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 the hub:

{
  "kind": "UpdateDeviceProperty",
  "name": "dev.nexigon.ota.desired",
  "mutation": {
    "mutation": "MergePatch",
    "path": [],
    "expr": {
      "kind": "Literal",
      "value": {
        "version": "2.1.0",
        "metadata": { "ring": "stable" }
      }
    }
  },
  "protected": true
}

The mutation field uses the shared JSON mutation model.

Supported mutations are:

MutationMeaning
SetReplace the value at path with the expression in expr. An empty path replaces the whole property value.
MergePatchEvaluate expr and apply it as an RFC 7396 JSON Merge Patch at path. An empty path patches the whole property value.
RemoveRemove 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 the 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.current"
      },
      "path": ["version"]
    },
    "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, the hub keeps waiting.

A deadline is noticed by a background tick rather than at the instant it passes, so a step times out within one tick of its deadline rather than exactly on it. Timeouts much shorter than the tick interval will overshoot noticeably. Set timeouts for the work you expect, not to the second.

DeviceCommand

Asks the device agent to run an on-demand command:

{
  "kind": "DeviceCommand",
  "command": "ota.update",
  "input": { "version": "2.0.0" },
  "timeoutSecs": 1800
}

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, the 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
}

Where a command runs to completion in one go, a task may report intermediate progress and durable checkpoints. If the worker is interrupted, by a reboot mid-update for instance, the same work item stays current and is returned with its latest checkpoint on a later poll, so the work resumes instead of restarting.

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.current"
          },
          "path": ["version"]
        }
      },
      {
        "kind": "NotEquals",
        "left": {
          "kind": "Get",
          "expr": {
            "kind": "NamedValue",
            "name": "dev.nexigon.ota.current"
          },
          "path": ["version"]
        },
        "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:

FieldMeaning
minSuccessPercentMinimum percent of frontier-stage targets that must have succeeded.
maxFailurePercentMaximum percent of frontier-stage targets that may have failed or timed out.
gatesAdditional 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:

StatusMeaning
DraftCreated but not started.
RunningThe rollout is progressing.
PausedHalted without being undone. See below.
CompletedThe final stage has been activated and all targets are terminal.
FailedThe rollout converged, but at least one target or selector evaluation failed.
CancelledOperation was cancelled.

Target statuses:

StatusMeaning
PendingThe target has not been released yet, because the rollout has not reached its stage.
RunningThe target is applying steps or waiting.
SucceededThe target completed all operation steps.
FailedA step failed.
TimedOutA step deadline was exceeded.
CancelledThe 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 halts a rollout without undoing any of it. No new devices are enrolled, no target makes progress, and devices are handed no new work. Nothing is cancelled and no state is lost.

A device that already took a work item and has since carried it out can still report the result, and the result is recorded. Pausing stops new work being generated; it does not discard work that was genuinely done.

Resume with fleet_StartOperation, which picks the rollout up exactly where it left off. Time spent paused does not count towards step timeouts: the clock on any step that was in flight restarts on resume, so a long pause cannot time work out.

API Actions

User-facing automation uses these actions:

ActionPurpose
fleet_QueryOperationsList operations for a project.
fleet_GetOperationGet an operation’s definition, stages, steps, and progress.
fleet_QueryOperationTargetsList the devices the operation applies to, and how each is faring.
fleet_QueryOperationSelectionErrorsList the devices the selector could not be evaluated against.
fleet_CreateOperationCreate a draft operation.
fleet_StartOperationStart an operation, or resume a paused one.
fleet_AdvanceOperationStageActivate the next stage if the frontier guard passes.
fleet_PauseOperationPause a running operation.
fleet_CancelOperationCancel a draft, running, or paused operation.

The targets and the selection errors are separate queries rather than part of fleet_GetOperation, because both grow with the fleet and most reads of an operation want neither.

Device agents use these actions internally:

ActionPurpose
devices_ClaimOperationWorkClaim device-executable work.
devices_ReportOperationStepReport the result of a device-executed step.

Project viewers can query operations. Creating, starting, pausing, advancing, and cancelling requires device-management permissions for the project. Polling and reporting are restricted to the device itself.

A device is only ever handed a step the hub has already vetted: the step’s when expression and the operation’s permissions are checked before the step is offered, so a device never receives work that when excludes.

Agent Configuration

Operation polling is configured in Nexigon Agent:

/etc/nexigon/agent.toml
[operations]
enabled = true
poll-interval-secs = 60

enabled 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 the hub and grows with the size of the fleet. Shorten it only if you need idle devices to react faster and you know the fleet is small.

DeviceCommand steps also require commands to be enabled:

/etc/nexigon/agent.toml
[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.