yuzu

Instructions

The instruction engine is the core of Yuzu’s content plane. Every ad-hoc query, scheduled task, policy check, and remediation action flows through the same pipeline: a YAML-defined InstructionDefinition is dispatched to agents via the existing gRPC wire protocol, tracked by the ExecutionTracker, stored by the ResponseStore, and audited by the AuditStore.

This guide covers everything an operator needs to author, manage, execute, schedule, and govern instructions.


Table of Contents

  1. Core Concepts
  2. YAML Authoring
  3. InstructionDefinition Reference
  4. InstructionSets
  5. Parameter Type System
  6. Result Schema and Aggregation
  7. Approval Workflows
  8. Concurrency Model
  9. Scheduling
  10. Execution Lifecycle
  11. Error Codes
  12. REST API Reference
  13. Dashboard UI
  14. Planned Features

1. Core Concepts

Content Hierarchy

ProductPack          (distribution boundary — Phase 7)
 └── InstructionSet  (permission boundary)
      └── InstructionDefinition  (the atomic unit)
           ├── Parameter Schema   (typed inputs)
           ├── Result Schema      (typed outputs)
           └── Execution Spec     (plugin + action + concurrency)

A definition can exist standalone (without a set or pack). Sets group definitions and enforce shared permissions. Packs bundle everything for signed distribution.

Design Principles

Two Definition Types

TypePurposeDefault ApprovalDashboard Treatment
questionRead-only query (inventory, status checks)autoNo confirmation prompt
actionState-changing operation (restart, install, delete)role-gatedConfirmation required

2. YAML Authoring

All instruction content is authored as YAML documents with apiVersion: yuzu.io/v1alpha1. The server stores the verbatim YAML as the source of truth (yaml_source column) alongside denormalized columns for efficient queries.

Minimal Definition

apiVersion: yuzu.io/v1alpha1
kind: InstructionDefinition
metadata:
  id: crossplatform.process.list
  displayName: List Running Processes
  version: 1.0.0
spec:
  type: question
  platforms: [windows, linux, darwin]
  execution:
    plugin: processes
    action: list

Saving YAML via API

Cookie-session writes to the instruction routes must carry an Origin (or Referer) naming the server; token-authenticated requests do not.

# Save a definition from a YAML file (form-encoded; the yaml_source field
# carries the document verbatim)
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instructions/yaml \
  --data-urlencode "yaml_source@definitions/process-list.yaml"

# Validate YAML without saving
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instructions/validate-yaml \
  --data-urlencode "yaml_source@definitions/process-list.yaml"

Dashboard YAML Editor

The Instruction Management page provides two authoring modes:

Both modes are served as HTMX fragments.

Saving keeps what the YAML does not carry. The editor’s Save writes only the fields the YAML expresses (name, plugin, action, description, and the optional type, version, concurrency and approval). Everything else stored on the definition — its instruction set, enabled flag, TTLs, result schema, visualization, response templates, platforms, minimum agent version, required plugins and readable payload — is kept. When the YAML omits type, version, concurrency or approval, the stored value is kept as well; a value that differs from the stored one is an intentional edit and is written, and a change of approval is recorded in the audit detail as approval_mode: <old> -> <new>.

Library list. The Definitions tab lists up to 100 definitions by default. GET /fragments/instructions takes name, plugin, type and limit (maximum 500); a cut list says “Showing the first N. Narrow the filter.” The “Needs approval” column and the instruction sets panel appear for callers who hold InstructionDefinition:Read and InstructionSet:Read respectively.

Page controls follow permissions. New, Edit, Import, Delete and the set controls appear for callers who hold InstructionDefinition:Write, InstructionDefinition:Delete, InstructionSet:Write and InstructionSet:Delete respectively, not for a role. Each capability is probed once per page render, and a denied probe is audited as a denial, so loading the page as a caller who lacks a capability writes one denial row for it.

Sets. The sets panel lists each set with the number of definitions in it. New set asks for a name and description. Delete set states that its definitions stay and become unassigned. Each library row has a set select that assigns the definition to a set or clears it.

Response templates. Templates on a library row opens a panel directly under the library filters, scrolled into view, with that definition’s templates: the built-in default (never editable) first, then the authored ones. Close empties the panel. New, Edit and Delete open dialogs that call /api/v1/definitions/{id}/response-templates. Filters are written one per line as column | op | value.

Import and export. Import definition takes the JSON document that Export produces (Ed25519-signed or not); unsigned documents can be refused by policy and a duplicate id is refused. When the audit record could not be written the page says so. Export saves the definition as <id>.json.

Dispatch. Selecting a definition on the Executions tab reads its parameters from GET /api/v1/instructions/{id}; grant_secret and grant_id are write-only password inputs. The Target select offers a scope expression with Validate (syntax only) and Preview (needs Infrastructure:Read). Dispatch always confirms, naming the instruction, plugin:action, type and target; a definition whose approval mode is not auto says it will be submitted for approval and not run, and an expression that was not previewed needs DISPATCH typed to send.


3. InstructionDefinition Reference

Full Example

apiVersion: yuzu.io/v1alpha1
kind: InstructionDefinition
metadata:
  id: crossplatform.service.inspect
  displayName: Inspect Service Status
  version: 1.2.0
  description: >-
    Returns the current state, startup type, and PID of a named service
    across Windows (SCM), Linux (systemd), and macOS (launchd).
  tags:
    - services
    - operations
spec:
  type: question
  platforms:
    - windows
    - linux
    - darwin

  execution:
    plugin: services
    action: inspect
    concurrency: per-device
    stagger:
      maxDelaySeconds: 0
      fixedDelaySeconds: 0
    minSuccessPercent: 100

  parameters:
    type: object
    required:
      - serviceName
    properties:
      serviceName:
        type: string
        displayName: Service Name
        description: The name of the service to inspect.
        validation:
          maxLength: 256
      verbose:
        type: boolean
        default: false

  result:
    columns:
      - name: serviceName
        type: string
      - name: state
        type: string
      - name: startupType
        type: string
      - name: pid
        type: int32
    aggregation:
      groupBy: [state]
      operations: [count]

  readablePayload: "Inspect service '${serviceName}'"

  gather:
    ttlSeconds: 300

  response:
    retentionDays: 90

  approval:
    mode: auto

  permissions:
    executeRoles:
      - endpoint-operator
      - endpoint-admin
    authorRoles:
      - content-author

  compatibility:
    minAgentVersion: 0.9.0
    requiredPlugins:
      - services

  legacy_shim:
    enabled: true

Metadata Fields

FieldTypeRequiredDescription
idstringYesGlobally unique ID. Convention: <scope>.<domain>.<action> (e.g. crossplatform.service.inspect).
displayNamestringYesHuman-readable name shown in the dashboard.
versionstringYesSemantic version (e.g. 1.2.0). Used for compatibility checks.
descriptionstringNoDetailed description of what this definition does.
tagslistNoFreeform tags for categorization and search.

Spec Fields

FieldTypeRequiredDefaultDescription
typestringYes—question (read-only) or action (may modify state).
platformslistYes—Target platforms: windows, linux, darwin.
executionobjectYes—Plugin, action, concurrency, stagger. See Section 8.
parametersobjectNo{}Input parameter schema. See Section 5.
resultobjectNo{}Output column schema. See Section 6.
readablePayloadstringNo""Template for audit display. Supports ${paramName} interpolation.
gatherobjectNo—ttlSeconds: agent response window (default 300).
responseobjectNo—retentionDays: how long to keep response data (default 90).
approvalobjectNo—mode: auto, role-gated, or always. See Section 7.
permissionsobjectNo—executeRoles and authorRoles arrays.
compatibilityobjectNo—minAgentVersion and requiredPlugins.
legacy_shimobjectNo—enabled: true for auto-generated definitions from plugin descriptors.

Execution Spec

FieldTypeDefaultDescription
pluginstring—Plugin identifier (must match a registered plugin’s name).
actionstring—Action name (must exist in the plugin’s actions[] array). Case-insensitive; normalized to lowercase at creation time and at dispatch.
concurrencystringper-deviceOne of: per-device, per-definition, per-set, global:<N>, unlimited. Only per-device is enforced — see Section 8.
stagger.maxDelaySecondsint0Max random delay per agent before execution. 0 = no stagger.
stagger.fixedDelaySecondsint0Fixed delay per agent before execution, added before the random stagger. 0 = no fixed delay. Total wait = fixedDelaySeconds + random(0, maxDelaySeconds).
minSuccessPercentint100Minimum success percentage. 0 = best-effort.

Compatibility

FieldDescription
minAgentVersionDeclared and exported; not compared at dispatch today, so an older agent is NOT skipped and error 4003 is not raised.
requiredPluginsAgents missing any listed plugin are skipped (error 4001).

4. InstructionSets

An InstructionSet groups definitions under a shared permission boundary. Sets control who can execute, author, and approve definitions within them.

Full Example

apiVersion: yuzu.io/v1alpha1
kind: InstructionSet
metadata:
  id: core.crossplatform.services
  displayName: Cross-Platform Service Management
  version: 1.0.0
  description: Inspect, start, stop, and restart services across all platforms.
  platforms:
    - windows
    - linux
    - darwin
  permissions:
    executeRoles:
      - endpoint-operator
      - endpoint-admin
    authorRoles:
      - content-author
      - endpoint-admin
    approveRoles:
      - endpoint-admin
  contents:
    instructionDefinitions:
      - crossplatform.service.inspect
      - crossplatform.service.start
      - crossplatform.service.stop
      - crossplatform.service.restart
    policyFragments: []
    workflowTemplates: []
  defaults:
    approvalMode: role-gated
    responseRetentionDays: 30
    targetEstimationRequiredAbove: 500
  publishing:
    signed: true
    visibility: org

Set-Level Permissions

Role TypePurpose
executeRolesRoles permitted to run definitions in this set.
authorRolesRoles permitted to create or modify definitions in this set.
approveRolesRoles permitted to approve executions in this set.

Set Defaults

FieldDefaultDescription
approvalModeautoDefault approval mode for definitions that do not specify their own.
responseRetentionDays90Default retention period for response data.
targetEstimationRequiredAbove—When estimated target count exceeds this, the UI requires confirmation.

Managing Sets via API

# Create an instruction set
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instruction-sets \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Core Service Operations",
    "description": "Service inspect, start, stop, restart"
  }'

# Delete a set (cascades to contained definitions)
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X DELETE http://localhost:8080/api/instruction-sets/abc123

5. Parameter Type System

Parameters use a JSON Schema subset. The root object describes the input shape; each property declares a typed parameter with optional validation.

Supported Types

TypeDescriptionWire FormatExample
stringUTF-8 textString"Spooler"
booleanBoolean flagtrue / falsetrue
int3232-bit signed integerNumeric string"42"
int6464-bit signed integerNumeric string"9223372036854775807"
datetimeISO 8601 timestampString"2026-03-17T18:20:00Z"
guidUUID / GUIDString"550e8400-e29b-41d4-a716-446655440000"

Parameters are transmitted as map<string, string> in the CommandRequest protobuf message. POST /api/instructions/{id}/execute validates the request’s params against the declared types and constraints before dispatch (see Parameter validation); no other dispatch surface does yet.

Validation Constraints

ConstraintApplicable TypesDescription
maxLengthstringMaximum length in bytes (UTF-8).
minLengthstringMinimum length in bytes (UTF-8).
patternstringRegex the value must match.
enumstringList of allowed values.
minimumint32, int64Minimum value (inclusive).
maximumint32, int64Maximum value (inclusive).

YAML Example

parameters:
  type: object
  required:
    - serviceName
    - action
  properties:
    serviceName:
      type: string
      displayName: Service Name
      description: The system service name (not display name).
      validation:
        maxLength: 256
        pattern: "^[a-zA-Z0-9_.-]+$"
    action:
      type: string
      displayName: Action
      description: What to do with the service.
      validation:
        enum: [start, stop, restart]
    timeout:
      type: int32
      default: 30
      description: Seconds to wait for the operation.
      validation:
        minimum: 5
        maximum: 300
    verbose:
      type: boolean
      default: false

Parameter validation

POST /api/instructions/{id}/execute validates the request’s params against the definition’s stored parameter_schema before the approval gate, strictly (details and error bodies: REST API). A refused call creates no approval ticket, execution row or dispatch. Enforced as declared:

A definition that stores no schema (empty, whitespace or {}, which is what the YAML editor stores for a definition created there) is not validated. The YAML editor, PUT /api/instructions/{id} and response-template changes keep the stored schema: editing spec.parameters in the YAML editor does not change what execute enforces.

Limits on a stored schema (a schema over a limit is refused when it is written, and a stored one that is over a limit makes execute answer 500):

LimitValue
schema text262144 bytes
properties per schema128
members per enum256
pattern length1024 bytes
compiled pattern size32768 RE2 instructions each, 65536 per schema, and RE2’s 512 KiB memory budget per pattern
integer minimum/maximummagnitude below 2^53
string matched against a pattern64 KiB
matching work per request (or per schema, for defaults)(bytes + 1) x pattern size, plus one unit per enum member compared, up to 16777216

enabled: false hides a definition from the discovery catalog and stops its schedules (the schedule poller records definition_disabled), but it does not stop POST /api/instructions/{id}/execute: that route does not read enabled.

Replacing a stored parameter schema

No route replaces a stored schema in place: PUT /api/instructions/{id}, the YAML editor and response-template changes keep it, and POST /api/instructions/import answers 409 for an id that already exists. To replace one (a wrong or legacy schema, a 500 from execute, or a corrected bundled definition on an existing install), export the definition, delete it and import it again with a corrected parameter_schema:

  1. GET /api/instructions/{id}/export (InstructionDefinition:Read) and keep the document. It carries the fields import reads, including yaml_source, instruction_set_id and response_templates_spec, which the delete removes (the delete and re-import also reset created_at, which import never reads). It carries no signature or publicKey.
  2. Edit its parameter_schema (a string holding the schema JSON). Unless the server runs with --allow-unsigned-definitions, the edited document must also be signed before import, or the import answers 400 as unsigned: add a hex signature and publicKey (an Ed25519 signature over the exact bytes of yaml_source, 128 and 64 hex characters). The signature covers yaml_source only, so plugin, action, type, approval_mode and concurrency_mode are read from it and must agree with the envelope. A definition whose yaml_source is empty cannot be signed (import answers an error about a signature without yaml_source), so only the unsigned flag works for it.
  3. Rehearse the import, because step 5 cannot be rehearsed (an existing id answers 409) and a refused import leaves the definition deleted: POST /api/instructions/import the corrected, signed document once with a different id (import reads id from the document and accepts any id that is free), confirm it is accepted, then DELETE that temporary id. The signature covers only the yaml_source bytes, so it stays valid. The temporary definition is live until it is deleted.
  4. DELETE /api/instructions/{id} (InstructionDefinition:Delete, audit action instruction.delete).
  5. POST /api/instructions/import with the edited document (InstructionDefinition:Write, audit action instruction.import). The schema is checked as it is written, so a bad one is refused with a 400.

The signature covers only the yaml_source bytes (not the schema, plugin, action, approval_mode or created_by), and the verifier takes publicKey from the request, so it is an integrity check and not publisher authentication. See REST API.


6. Result Schema and Aggregation

Result schemas declare typed columns for structured output. These types enable downstream consumption by ClickHouse, Splunk, CSV export, and the dashboard aggregation engine.

Result Column Types

TypeDescriptionExample Value
boolBooleantrue, false
int3232-bit signed integer4592
int6464-bit signed integer1710700800000
stringUTF-8 text"running"
datetimeISO 8601 timestamp"2026-03-17T18:20:00Z"
guidUUID / GUID"550e8400-e29b-41d4-..."
clobCharacter large objectMulti-line log output, stack traces

The clob type indicates a potentially large text field. The server may apply configurable truncation to limit storage.

Server-Side Aggregation

The ResponseStore supports aggregation across agent responses using GROUP BY and aggregate functions.

result:
  columns:
    - name: serviceName
      type: string
    - name: state
      type: string
    - name: pid
      type: int32
  aggregation:
    groupBy: [state]
    operations: [count, min, max, sum, avg]
OperationDescription
countNumber of rows per group.
sumSum of numeric column values.
avgAverage of numeric column values.
minMinimum value per group.
maxMaximum value per group.

7. Approval Workflows

Yuzu supports three approval modes that control whether an execution requires human sign-off before dispatch.

Approval Modes

ModeBehavior
autoNo approval required. Execution dispatches immediately.
role-gatedNon-admin users must submit for approval. Admins and users with approveRoles can execute directly.
alwaysEvery execution requires explicit approval, regardless of the user’s role.

Rules

YAML Configuration

spec:
  approval:
    mode: role-gated    # auto | role-gated | always

Executor-Side Behavior

When a user (or MCP tool) executes an instruction that requires approval:

  1. HTTP 202 response. The server returns {"status": "pending_approval", "approval_id": "...", "definition_id": "..."} instead of dispatching immediately. Nothing runs yet: the request waits as a pending approval ticket. Approving a ticket created this way records the decision but does not yet dispatch the instruction, because an instruction-origin ticket has no redemption path (tracked in #6064; see Upgrading). An MCP client is different: it re-calls the same tool with the approval_id once the ticket is approved, and that call runs it.
  2. Dashboard toast notification. A toast appears in the web dashboard confirming the request was submitted for approval. The toast includes the approval ID for reference.
  3. Approvals tab or page. The pending request is visible in the Approvals tab of the dashboard and on the Approvals page (/approvals). The executor can check its status there at any time. Once an approver approves or rejects the request, the status updates in the tab (a rejected request is never run; an approved instruction-origin ticket is recorded but not dispatched, as in step 1). On the Approvals page, Approve and Reject open a dialog with an optional reviewer comment (capped at 4000 bytes); the sidebar pending-approvals badge refreshes after each decision.

The submitting account cannot approve its own request. A different account with the appropriate role must review it (the check compares account names; see the known limitation under Who can approve).

Who can approve

Approving or rejecting a request is an act of human review, so the server asks for more than the Approval:Approve permission. The same rules apply to the dashboard Approvals page, which posts to the same route, and to POST /api/approvals/{id}/approve and POST /api/approvals/{id}/reject:

MCP clients cannot approve. MCP callers authenticate with a bearer token and the MCP transport runs no MFA step-up, so the MCP tools approve_request and reject_request always return a typed refusal, whatever credential the caller used (a browser cookie session that reaches /mcp/v1/ is refused too). This is an exception recorded in ADR-1005’s exception ledger (entry 2026-10-18): human review is a human-session act. The tools stay registered, and listing pending approvals through MCP (list_pending_approvals, get_pending_approval_count) is unchanged. A supervised-tier agentic worker still gets its ticket from the first call and still recalls the tool with the approval_id; a person approves the ticket in the dashboard or over REST with a signed-in session.

Failure behaviour.

If the dialog says you do not have permission. For an approver refusal (approver_assurance_required:<reason>, whether a class or an MFA refusal) the Approvals dialog shows the refusal’s own remediation text, which names the next step. For any other 403 it shows the generic “You do not have permission” text, and the response’s own message names the cause. The causes, in the order the server checks them: the request carried no same-site Origin or Referer (for example a reverse proxy rewrites Host and the public address is not listed in --csrf-trusted-origin; the server-wide same-site check answers this first, before any step-up); you signed in through SAML while step-up is on (the central step-up check, before the route handler); then, in the handler, you lack Approval:Approve; your account has no enrolled MFA factor; your account has been deleted or deactivated; your single sign-on login carried no MFA claim; or you signed in through SAML with step-up switched off (--mfa-step-up-window-secs=0), which the approver rule still refuses. The Approve and Reject buttons are a render hint only: an unenrolled or SAML session is shown them and the route still refuses it. The Approvals tab on Instructions has no dialog: on a 403 it shows the same remediation text in a toast for an approver refusal and the server’s message for any other 403, “Your session expired; sign in again.” on a plain 401, and the same code prompt as the Approvals page on a step-up 401, then repeats the decision once after a correct code.

Approval API

Approve and reject need a signed-in session cookie (see Who can approve); a bearer token is refused. The examples assume a session that has completed MFA step-up. A cookie-session POST must carry an Origin (or Referer) naming the server, as a browser sends; a request with neither, or naming another site, is refused with 403 (--csrf-trusted-origin entries are accepted). The error table is in the REST API reference.

# Check pending approval count
curl -s -b cookies.txt \
  http://localhost:8080/api/approvals/pending/count

# Approve an execution request
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/approvals/REQ-001/approve \
  -H "Origin: http://localhost:8080" \
  -H "Content-Type: application/json" \
  -d '{"comment": "Reviewed — safe to proceed."}'

# Reject an execution request
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/approvals/REQ-001/reject \
  -H "Origin: http://localhost:8080" \
  -H "Content-Type: application/json" \
  -d '{"comment": "Not approved for production during change freeze."}'

8. Concurrency Model

Corrected 2026-08-31 (ADR-1007) — only per-device is actually enforced. See docs/yaml-dsl-spec.md §12 for the full corrected model (mode-by-mode status, why enforcement is server-side rather than agent-side, the plugin: server-class catalog-definition caveat, what remains unenforced and why) and docs/Instruction-Engine.md §10 for the implementation summary. This section previously described a fully-implemented 5-mode system, including a wait queue; none of that was ever built except the one mode below.

Modes

ModeEnforcementScopeStatus
per-deviceServer-sideOne execution of this definition per device at a timeEnforced (default; 191 real shipped definitions use it).
per-definitionNone—Not enforced — used only on catalog-only definitions with no live dispatch path. Do not rely on this for dangerous global operations (schema migration, bulk delete) — nothing prevents concurrent execution.
per-setNone—Not enforced, unspecified — no grouping key defined anywhere. Zero real usage.
global:<N>None—Not enforced — not used in any shipped definition.
unlimitedNoneNo limitsNo enforcement needed.

How Enforcement Works

per-device (the only enforced mode): enforced server-side. At dispatch, the server holds a claim on (definition_id, agent_id) in a dedicated Postgres table (execution_tracker.concurrency_claims, race-free via a partial unique index), excludes any agent already holding an open claim for this definition, and releases the claim when that agent reaches a terminal status. There is no wait queue — an excluded agent is simply not dispatched to on this attempt; retry once the in-flight execution completes. Error code 3003 is registered for this condition but not yet surfaced through the dispatch response. Not covered by this gate: raw MCP/REST dispatch (no definition_id in scope) and the fleet-broadcast arm of dispatch.

One release exception worth knowing: cancelling an execution does not release its claim (there is no way to tell the agent to stop, so a cancelled-but-still-running execution’s claim behaves like a normal in-flight one until a terminal response or the reconciler’s TTL bound releases it — worst case, one hour). A per-device definition dispatched via a workflow step (as opposed to a direct execute call or a schedule) releases and renews the same way as any other dispatch path — it just won’t show progress in the executions drawer, since workflow-step dispatch doesn’t yet correlate a real execution id there.

All other modes are accepted and stored but have no enforcement effect — they behave exactly like unlimited in practice.

YAML Syntax

spec:
  execution:
    concurrency: per-device          # default — the only mode actually enforced
    # concurrency: unlimited         # no limits (also the practical effect of any unsupported value)

Stagger

For large-fleet dispatch, stagger introduces a per-agent delay before execution to avoid thundering herd problems. Each agent calculates: total wait = fixedDelaySeconds + random(0, maxDelaySeconds).

spec:
  execution:
    stagger:
      maxDelaySeconds: 60       # each agent delays 0-60s randomly
      fixedDelaySeconds: 5      # plus a fixed 5s base delay

If the command has an expiration (expires_at) and the total delay causes it to expire, the agent sends a REJECTED response and skips execution.

The REST API also supports stagger/delay via the stagger and delay integer fields on POST /api/command:

curl -X POST http://server:8080/api/command \
  -H "Content-Type: application/json" \
  -d '{"plugin":"hardware","action":"cpu-info","stagger":30,"delay":5}'

9. Scheduling

The ScheduleEngine supports recurring instruction executions with four frequency types. The scope expression is evaluated at dispatch time, so newly enrolled devices are automatically included.

Frequency Types

TypeFields UsedDescription
dailytime_of_dayRuns once per day at the specified time.
weeklytime_of_day, day_of_weekRuns once per week. day_of_week: 0=Sunday through 6=Saturday.
monthlytime_of_day, day_of_monthRuns once per month. day_of_month: 1-28 (no 29/30/31 to avoid month-length issues).
intervalinterval_minutesRuns every N minutes. Minimum: 1 minute; maximum: 35791394.

Execution semantics

A server-side poller checks for due schedules every 30 seconds, so a fire can land up to ~30 seconds after its due time — but firing is now two stages, not one. At fire time the poller creates a tracked execution (attributed to the schedule’s creator, appearing immediately in the Executions history) and commits a durable occurrence to the command outbox, emitting an instruction.schedule_fired audit event with result queued. A leader-gated delivery loop then drains the outbox and performs the actual dispatch, within ~5 seconds of enqueue — so up to ~35 seconds total from due time to dispatch. Delivery emits its own command.outbox_delivered audit event recording the real send outcome: success (dispatched, with the agents-reached count), failure (no agents currently in scope — recorded and skipped, not retried; or a malformed occurrence), or denied (authority was revoked between enqueue and delivery). The execution row’s targeted-agent count fills in at delivery, once the real outcome is known. A systemic delivery-gate failure (for example, containment status unreadable) is treated as transient: the same occurrence is retried with back-off rather than failed outright.

Approval-gated runs (the schedule’s requires_approval flag, or a definition whose approvalMode is not auto — there is no operator session on the scheduled path, so role-gated also requires a ticket) submit one approval request per occurrence and hold at the due time. Approving the ticket fires the run within one poller cycle; rejecting it skips that occurrence, and the next occurrence submits a fresh request. Approvals follow the standard four-eyes rule, so the schedule’s creator cannot approve their own scheduled runs under the same account name (the check compares account names, not people; see the known limitation under Who can approve). Poller health is observable via the yuzu_schedule_fires_total, yuzu_schedule_fire_failures_total, yuzu_schedule_approvals_submitted_total and yuzu_schedule_tick_errors_total counters.

Schedule Properties

FieldTypeDescription
namestringHuman-readable schedule name.
definition_idstringThe instruction definition to execute.
frequency_typestringdaily, weekly, monthly, or interval.
interval_minutesintMinutes between executions (for interval type), 1-35791394.
time_of_daystringHH:MM format (for daily, weekly, monthly).
day_of_weekint0-6 (for weekly).
day_of_monthint1-28 (for monthly).
scope_expressionstringScope DSL expression evaluated at dispatch time.
requires_approvalboolIf true, each scheduled run goes through the approval workflow.
enabledboolToggle the schedule on or off without deleting it.

Schedule API

# Create a schedule — daily service check at 06:00
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/schedules \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Daily Service Health Check",
    "definition_id": "crossplatform.service.inspect",
    "frequency_type": "daily",
    "time_of_day": "06:00",
    "scope_expression": "ostype == \"windows\" AND tag:environment == \"Production\"",
    "requires_approval": false,
    "enabled": true
  }'

# Create a schedule — every 30 minutes
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/schedules \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Process inventory sweep",
    "definition_id": "crossplatform.process.list",
    "frequency_type": "interval",
    "interval_minutes": 30,
    "scope_expression": "ostype IN [\"windows\", \"linux\"]",
    "enabled": true
  }'

# Enable or disable a schedule
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/schedules/SCHED-001/enable \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

# Delete a schedule
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X DELETE http://localhost:8080/api/schedules/SCHED-001

Execution Tracking for Schedules

Each scheduled dispatch creates a normal Execution record. The schedule tracks:


10. Execution Lifecycle

States

An execution progresses through these states:

pending → dispatching → gathering → completed
                                  → failed
                                  → cancelled

Per-Agent Tracking

Each agent targeted by an execution has its own status record:

FieldDescription
agent_idThe targeted agent.
statuspending, dispatched, running, success, failure, timeout, rejected.
dispatched_atWhen the command was sent to the agent.
first_response_atWhen the agent first responded (streaming).
completed_atWhen the agent finished.
exit_codePlugin exit code.
error_detailError message if the agent reported failure.

Aggregate Counters

The execution record maintains real-time counters:

CounterDescription
agents_targetedTotal agents in scope at dispatch time.
agents_respondedAgents that have sent at least one response.
agents_successAgents that completed successfully.
agents_failureAgents that reported failure.
progress_pct(agents_responded / agents_targeted) * 100.

Rerun

Rerun creates a new execution linked to the original via rerun_of. Two modes:

# Rerun all agents
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/executions/EXEC-001/rerun \
  -H "Content-Type: application/json" \
  -d '{"scope": "all"}'

# Rerun only failed agents
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/executions/EXEC-001/rerun \
  -H "Content-Type: application/json" \
  -d '{"scope": "failed_only"}'

The scope field controls which agents are included. Set "scope": "failed_only" to target only agents that reported failure; any other value (or omitting the field) reruns all agents.

Cancel

Cancellation records the requesting user for audit:

curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/executions/EXEC-001/cancel

Parent/Child Executions

Executions can form hierarchies using the parent_id field. This supports:


11. Error Codes

Errors are categorized into four domains with non-overlapping numeric ranges.

1xxx — Plugin Errors

CodeNameDescriptionRetryable
1001PLUGIN_ACTION_NOT_FOUNDPlugin does not support the requested actionNo
1002PLUGIN_PARAM_INVALIDParameter validation failedNo
1003PLUGIN_PERMISSION_DENIEDOS-level permission denied (e.g. non-admin)No
1004PLUGIN_RESOURCE_MISSINGTarget resource does not existNo
1005PLUGIN_OPERATION_FAILEDAction executed but failedTransient
1006PLUGIN_CRASHPlugin segfaulted or threw unhandled exceptionNo
1007PLUGIN_TIMEOUTPlugin did not complete within gather TTLOnce

2xxx — Transport Errors

CodeNameDescriptionRetryable
2001TRANSPORT_DISCONNECTEDAgent lost connection during executionAlways
2002TRANSPORT_STREAM_ERRORgRPC stream broken mid-responseAlways
2003TRANSPORT_RESPONSE_TOO_LARGEResponse exceeds max message sizeNo

3xxx — Orchestration Errors

CodeNameDescriptionRetryable
3001DefinitionNotFoundThe referenced InstructionDefinition does not existNo
3002ApprovalRequiredExecution blocked pending approvalAwaits human
3003ConcurrencyBlockedRegistered for per-device claim exclusion; not yet surfaced through the dispatch response (ADR-1007)After slot frees
3004ScopeEmptyThe resolved target scope matched no agentsNo
3005ScheduleExpiredThe schedule’s execution window has passedNo

Names above are the taxonomy’s actual registered names (server/core/src/error_codes.cpp) — this row set was corrected from a stale naming scheme this table had carried (ORCH_* prefixes that never matched the code). Like 3003, none of 3001/3002/3004/3005 currently has a production call site that emits it (error_codes.cpp’s registration is not the same as being wired to a real error path yet) — the retry-semantics column above states the DESIGNED behavior per the taxonomy entry, not an observed one.

4xxx — Agent Errors

CodeNameDescriptionRetryable
4001AGENT_PLUGIN_NOT_LOADEDRequired plugin not available on agentNo
4002AGENT_SHUTTING_DOWNAgent is in shutdown sequenceOn reconnect
4003AGENT_VERSION_INCOMPATIBLEAgent version below minAgentVersionNo

Retry Semantics

CategoryDefault RetryMax AttemptsBackoff
Transport (2xxx)Always3Exponential (1s, 2s, 4s)
Transient plugin (1005, 1007)If retryable: true2Linear (5s)
Deterministic plugin (1001-1004, 1006)Never0—
Agent (4001-4003)Never (except 4002)1—
Orchestration (3001-3005)Per-code as noted——

12. REST API Reference

All API endpoints require session-cookie authentication. Obtain a session by posting to /login first.

There are two API surface areas with different response envelopes:

/api/* endpoints (instruction engine, registered in instruction_routes.cpp/execution_routes.cpp and server.cpp) use domain-keyed responses:

{
  "definitions": [ ... ],
  "count": 42
}

The key name varies by resource type ("definitions", "executions", "schedules", "approvals", "sets", "agents", "children"). Error responses use {"error": "message"}.

/api/v1/* endpoints (versioned REST API, registered in rest_api_v1.cpp) use a standard envelope with metadata:

{
  "data": { ... },
  "meta": { "api_version": "v1" }
}

List endpoints on /api/v1/* include pagination:

{
  "data": [ ... ],
  "pagination": { "total": 42, "start": 0, "page_size": 50 },
  "meta": { "api_version": "v1" }
}

Error responses on /api/v1/*:

{
  "error": "descriptive message",
  "code": 1002,
  "meta": { "api_version": "v1" }
}

Definitions

List definitions

GET /api/instructions

Query parameters: name, plugin, type, set_id, enabled_only, limit.

# List all definitions
curl -s -b cookies.txt http://localhost:8080/api/instructions

# Filter by plugin
curl -s -b cookies.txt "http://localhost:8080/api/instructions?plugin=services"

# Filter by type
curl -s -b cookies.txt "http://localhost:8080/api/instructions?type=question"

Get a single definition

GET /api/instructions/{id}
curl -s -b cookies.txt \
  http://localhost:8080/api/instructions/crossplatform.service.inspect

Create a definition (JSON)

POST /api/instructions
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instructions \
  -H "Content-Type: application/json" \
  -d '{
    "id": "crossplatform.service.inspect",
    "name": "Inspect Service Status",
    "version": "1.2.0",
    "type": "question",
    "plugin": "services",
    "action": "inspect",
    "description": "Returns current state of a named service.",
    "platforms": "windows,linux,darwin",
    "approval_mode": "auto",
    "concurrency_mode": "per-device",
    "parameter_schema": "{\"type\":\"object\",\"required\":[\"serviceName\"],\"properties\":{\"serviceName\":{\"type\":\"string\"}}}",
    "result_schema": "[{\"name\":\"state\",\"type\":\"string\"},{\"name\":\"pid\",\"type\":\"int32\"}]"
  }'

Update a definition

PUT /api/instructions/{id}
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X PUT http://localhost:8080/api/instructions/crossplatform.service.inspect \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Inspect Service Status",
    "version": "1.3.0",
    "description": "Updated description with new fields."
  }'

Delete a definition

DELETE /api/instructions/{id}
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X DELETE http://localhost:8080/api/instructions/crossplatform.service.inspect

Save from YAML

POST /api/instructions/yaml

Accepts Content-Type: application/x-yaml. Parses the YAML, extracts metadata and spec fields, and creates or updates the definition. The verbatim YAML is stored as yaml_source.

curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instructions/yaml \
  -H "Content-Type: application/x-yaml" \
  --data-binary @my-definition.yaml

Validate YAML (dry run)

POST /api/instructions/validate-yaml

Returns validation errors without saving.

curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instructions/validate-yaml \
  -H "Content-Type: application/x-yaml" \
  --data-binary @my-definition.yaml

Import from JSON

POST /api/instructions/import
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instructions/import \
  -H "Content-Type: application/json" \
  -d @exported-definition.json

Export to JSON

GET /api/instructions/{id}/export
curl -s -b cookies.txt \
  http://localhost:8080/api/instructions/crossplatform.service.inspect/export \
  -o exported-definition.json

Execute an instruction definition

POST /api/instructions/{id}/execute

Dispatches the instruction definition to agents. Requires Execution:Execute permission.

Request body:

{
  "agent_ids": ["agent-uuid-1"],
  "params": {"path": "C:\\Windows\\System32\\notepad.exe"}
}

Response (200):

{
  "command_id": "filesystem-a1b2c3",
  "execution_id": "exec-d4e5f6",
  "agents_reached": 3,
  "definition_id": "filesystem.exists"
}
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instructions/filesystem.exists/execute \
  -H "Content-Type: application/json" \
  -d '{"params":{"path":"C:\\Windows"}}'

Approval gate: The response varies based on the definition’s approval_mode. Definitions with approval_mode: auto return HTTP 200 with an immediate execution result. Definitions with approval_mode: role-gated or always return HTTP 202 with a pending_approval status and an approval_id when the caller requires approval. See Section 7 for details.

List definitions (v1 endpoint)

GET /api/v1/definitions

The versioned v1 endpoint. Returns all definitions with the {"data": [...], "pagination": {...}, "meta": {...}} envelope. Note: this endpoint does not currently accept query parameters for filtering — it returns all definitions. Use /api/instructions with query parameters (name, plugin, type, set_id, enabled_only, limit) if filtering is needed.

curl -s -b cookies.txt http://localhost:8080/api/v1/definitions

Instruction Sets

List instruction sets

GET /api/instruction-sets
curl -s -b cookies.txt http://localhost:8080/api/instruction-sets

Create a set

POST /api/instruction-sets
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/instruction-sets \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Service Operations",
    "description": "All service management definitions"
  }'

Delete a set

DELETE /api/instruction-sets/{id}

Cascade-deletes all definitions in the set.

curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X DELETE http://localhost:8080/api/instruction-sets/SET-001

Schedules

List schedules

GET /api/schedules

Query parameters: definition_id, enabled_only.

# List all schedules
curl -s -b cookies.txt http://localhost:8080/api/schedules

# List only enabled schedules
curl -s -b cookies.txt "http://localhost:8080/api/schedules?enabled_only=true"

The dashboard’s Schedules tab (see Dashboard UI below) is backed by the versioned GET /api/v1/schedules twin, not the legacy endpoint above. If the schedule store is temporarily unreachable, the tab shows “Schedule list temporarily unavailable — retry shortly” instead of an empty list. Schedule reads (dashboard, REST v1, and MCP list_schedules alike) are capped at 100 rows; if a fleet has more schedules than that, the tab shows a partial-list notice above the table, and GET /api/v1/schedules’s JSON response carries result_truncated_by_cap: true (see REST API) — the two are the same underlying signal.

Create a schedule

POST /api/schedules

See Section 9 for the full field reference and examples.

Delete a schedule

DELETE /api/schedules/{id}
curl -s -b cookies.txt -X DELETE http://localhost:8080/api/schedules/SCHED-001 -H "Origin: http://localhost:8080"

Enable/disable a schedule

POST /api/schedules/{id}/enable
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/schedules/SCHED-001/enable \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'

Approvals

List approvals

GET /api/approvals

Query parameters: status, submitted_by.

# List all approvals
curl -s -b cookies.txt http://localhost:8080/api/approvals

# List pending approvals
curl -s -b cookies.txt "http://localhost:8080/api/approvals?status=pending"

Pending approval count

GET /api/approvals/pending/count
curl -s -b cookies.txt http://localhost:8080/api/approvals/pending/count

Approve

POST /api/approvals/{id}/approve

Approve and reject need a signed-in session cookie, an enrolled MFA factor with a fresh proof (#6079) and a same-site Origin/Referer; an API or MCP token is refused with 403. See Who can approve and the error table in the REST API reference.

The reviewer’s account must differ from the submitter’s (account names are compared, so one person holding two identities is not caught; see Who can approve): self-approval with the same account is rejected with HTTP 400 ("reviewer cannot be the same as the submitter"), and the denied attempt is recorded in the audit log. The comment is capped at 4,000 bytes (HTTP 400 beyond that).

curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/approvals/REQ-001/approve \
  -H "Origin: http://localhost:8080" \
  -H "Content-Type: application/json" \
  -d '{"comment": "Approved."}'

Reject

POST /api/approvals/{id}/reject
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/approvals/REQ-001/reject \
  -H "Origin: http://localhost:8080" \
  -H "Content-Type: application/json" \
  -d '{"comment": "Denied — change freeze in effect."}'

Executions

All routes below are management-group confined (#3789) — see rest-api.md’s “Executions” section for the per-route permission and confinement/404 behavior.

List executions

GET /api/executions

Query parameters: definition_id, status, limit.

# List all executions
curl -s -b cookies.txt http://localhost:8080/api/executions

# Filter by definition
curl -s -b cookies.txt "http://localhost:8080/api/executions?definition_id=crossplatform.service.inspect"

# Filter by status
curl -s -b cookies.txt "http://localhost:8080/api/executions?status=completed"

Get a single execution

GET /api/executions/{id}
curl -s -b cookies.txt http://localhost:8080/api/executions/EXEC-001

Get execution summary

GET /api/executions/{id}/summary

Returns aggregate counters including progress_pct.

curl -s -b cookies.txt http://localhost:8080/api/executions/EXEC-001/summary

Get per-agent statuses

GET /api/executions/{id}/agents
curl -s -b cookies.txt http://localhost:8080/api/executions/EXEC-001/agents

Get child executions

GET /api/executions/{id}/children
curl -s -b cookies.txt http://localhost:8080/api/executions/EXEC-001/children

Rerun

POST /api/executions/{id}/rerun
# Rerun failed agents only
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/executions/EXEC-001/rerun \
  -H "Content-Type: application/json" \
  -d '{"scope": "failed_only"}'

Cancel

POST /api/executions/{id}/cancel
curl -s -b cookies.txt \
  -H "Origin: http://localhost:8080" \
  -X POST http://localhost:8080/api/executions/EXEC-001/cancel

Cancelling an execution that already finished (succeeded, completed or cancelled) answers 409 and changes nothing; the Executions page offers Cancel only for executions that are still running. Per-device results and the live view are on the page’s detail pane.


13. Dashboard UI

The Instruction Management page is accessible from the main dashboard. It uses HTMX to load content fragments without full page reloads.

Tabs

TabDescription
DefinitionsBrowse, search, create, edit, and delete instruction definitions. Supports both form mode and CodeMirror YAML editor.
ExecutionsExecute instructions and view results. The top section provides an execution form: select a definition from the dropdown (grouped by plugin), fill in parameters (auto-populated from the definition’s schema), choose a scope (all agents, a group, or an individual agent), and click Execute. Below the form, the execution history table shows each run with a 4-segment status sparkbar (succeeded / failed / running / pending — length encodes count, hue encodes status), the resolved definition name, the wall-clock dispatch time in the operator’s local timezone (HH:MM:SS.mmm <TZ>, e.g. 12:22:33.251 BST; full ISO-8601 UTC on hover), and — on failed rows — an 80-character preview of the most recent agent error. The “Fan-out” cell shows succeeded / failed / targeted counts that update in real time as agents respond. Click any row to expand an inline drawer that live-updates via SSE as responses arrive — no page reload required. The drawer shows a KPI strip (Total / Succeeded / Failed / p50 / p95 duration), a small-multiples agent grid colored by status (decile-bucketed when the execution targets more than 1024 agents), a per-agent table sorted failed-first with inline duration bars, and a responses table with a Time column showing the server-side response arrival time at millisecond precision (same wall-clock format as above). The drawer is keyboard-reachable via Tab and Enter/Space. The Executions page (/executions) takes ?definition_id=<id> and ?status=<s> to pre-filter the list and ?id=<execution> to open one. The drawer’s responses table, its live updates and the page detail omit the rows of Forensics-tier plugins (app_usage, browser_inventory, execution_artifacts, privacy_permissions, remote_access_audit) for a caller without Forensics:Read, with no marker, and the failure preview of such a command is a short machine token; see “Forensics-tier rows” in rest-api.md.
SchedulesCreate, enable/disable, and delete recurring schedules. Shows next and last execution times.
ApprovalsReview pending approval requests. Approve or reject with comments. The sidebar’s Approvals entry shows the pending count. A submitting account cannot review its own request (account names are compared; see Who can approve). A pending row you submitted shows a “You submitted this” hint saying another reviewer must approve, instead of buttons, and any backend denial (self-review, already-reviewed) surfaces as an error toast.

Executions and Approvals pages. /executions and /approvals are read-only pages in the sidebar under Instructions: Executions lists the dispatch history with the same status counters as the tab, and Approvals lists the approval queue, pending first. Both honour your access scope and show a note, not an empty list, when a store cannot be read. Executing stays on the Instructions tab. On /approvals, pending rows carry Approve and Reject buttons (dialog with an optional comment). On /executions, selecting a row shows per-device results; a running execution updates live over SSE (a chip shows the stream state); and Cancel execution (dialog, shown when you may cancel and the execution is not finished) cancels it. Fleet-wide figures (today, all time, device success rate, average duration) appear for unconfined operators only and are hidden when the store cannot be read.

Workflows page. /workflows is a page in the sidebar under Instructions (its workflow actions are described below): the workflow definitions (name, description, step count; selecting one lists its steps with each step’s failure policy) and the recurring schedules. Schedules run a single instruction on the cadence the scheduler really applies (once, every N minutes, daily, weekly or monthly; time of day, weekday and day of month are stored but not used) and are not linked to workflows. A schedule you did not create shows only whether it targets all devices or a subset.

The definitions table also shows each workflow’s Last run and Result (the newest run you may see: “Not run” when there is none, “None visible” when runs exist only on devices outside your scope, “Unknown” when the list of newest runs was cut off before it reached that workflow). Selecting a workflow loads its latest run in the Latest run panel (status, start, duration, device count and one row per step) and the Recent runs disclosure lists its ten newest runs; selecting one loads it. A step shown as Dispatched means the command was sent: it says nothing about the result on a device, and for a caller confined to a device scope it reads Dispatched (run-wide) because the send covered the run’s whole target list. A skipped step says why only for the three reasons the engine writes (condition not met, previous step failed, workflow aborted); a failed step shows its error text when the engine stored one, otherwise “No detail.”. A run is visible to you only if at least one of its target devices is in your scope; a run you may not see and a run that does not exist look the same.

Workflow actions. New workflow (shown when you hold Workflow:Write) opens a dialog for the workflow’s YAML; the server’s message is shown if the YAML is refused, and nothing runs until you run it. Each row’s Run now (needs Workflow:Execute) opens a device checklist built from the connected devices you may see (filter box, the first 200 matches shown, at most 500 selected); without permission to list devices it falls back to a box of device ids, one per line. Each step is sent to every selected device in order; a step whose instruction needs approval is refused, and device results are not collected on this page. The server runs the whole workflow before it answers, so the page waits 8 seconds, then says the run may keep running on the server, disables that workflow’s Run now and checks its newest run every 5 seconds for up to 10 minutes. Check Recent runs before running it again. Delete in the Steps panel (needs Workflow:Delete) retires the workflow after a confirmation; run history is kept and there is no undelete in the console. A control you may not use is shown disabled with the permission it needs. Instruction names appear on step rows only if you also hold InstructionDefinition:Read; otherwise the step’s label or instruction id is shown. Schedule actions. Each schedule row has Enable/Disable and Delete, enabled only on schedules you created (a schedule with no recorded creator is nobody’s, so both stay disabled). Enable needs Schedule:Write and Execution:Execute and asks for confirmation because the schedule then runs without anyone present; Disable needs Schedule:Write and is one click; Delete needs Schedule:Delete and confirms first. After a change the list is re-read and the stored state shown. New schedule (needs Schedule:Write and Execution:Execute; the instruction list needs InstructionDefinition:Read) takes a name, an enabled instruction, a cadence (once runs at the next scheduler tick, about now; the scheduler ignores time of day), targets, whether each run needs approval and parameters as JSON. Leaving targets empty needs an explicit acknowledgement that it reaches every device. A final summary shows the instruction’s approval mode before the schedule is created enabled; a run can still be refused when it fires. Not verified by running a workflow against a live fleet: that the server keeps running a workflow after the page stops waiting.

Who sees what: Administrator, Operator, PlatformEngineer, Viewer and ITServiceOwner hold Workflow:Read and see all definitions; runs follow the fleet-read scope of the account. A caller scoped only to management groups is refused the definitions and schedules panels (the existing global-grant rule) but sees runs that include a device in scope. ITServiceOwner can create, enable and delete its own schedules elsewhere but has no Run, New workflow or Delete workflow control, and a service-scoped token is refused the schedules panel outright.

Operational notes: listing definitions reads each workflow’s steps with one query per row (up to 101 per page load, newest first), so the page asks for 101 and never renders the YAML. Opening the page or refreshing a denied panel writes a denial audit row for each refused panel, so a denied panel is not polled; reading a run’s detail writes workflow_execution.detail.fetch. Not verified by running it: that a run keeps going on the server when the browser disconnects, and the content of the perm_fn denial audit rows for the refused panels.

YAML Editor

The definition editor supports two modes:

Both modes save through the same /api/instructions/yaml endpoint.

Inline Parameters (CLI-style)

The dashboard instruction bar supports CLI-style inline parameters using key=value syntax:

filesystem exists path=C:\Windows\System32\notepad.exe
registry get_value hive=HKLM key=SOFTWARE\Microsoft\Windows value=ProductName

Syntax rules:

This differs from YAML-parameterised instructions where parameters are defined in the parameter_schema of an InstructionDefinition. Inline parameters are ad-hoc and bypass schema validation — they are sent directly to the plugin action. YAML-defined parameters provide type constraints, descriptions, default values, and validation.

Response Visualization (chart deck)

When the dispatched (plugin, action) reverse-resolves to an enabled InstructionDefinition that declares spec.visualization or spec.visualizations, a chart deck auto-renders above the results table — no operator action required. The dashboard:

  1. Resolves (plugin, action) to the first enabled definition with a configured visualization. The reverse-lookup is gated on InstructionDefinition:Read in addition to Execution:Execute; an operator without InstructionDefinition:Read sees the dispatch succeed but no chart appears.
  2. Emits an OOB <div id="chart-deck-host"> with a 2-second deferred load trigger so responses have time to arrive.
  3. Fetches /fragments/results?...&definition_id=<id> which returns one chart-card placeholder per configured chart in a <div class="yuzu-chart-deck"> container.
  4. The Yuzu chart adapter (/static/yuzu-charts.js) — built on Apache ECharts 5 (/static/echarts.min.js) and themed from Yuzu design tokens — populates each placeholder from GET /api/v1/executions/{id}/visualization?definition_id=<id>&index=<N>.

Limitations operators should expect:

See docs/yaml-dsl-spec.md § spec.visualization for the chart configuration schema and docs/user-manual/rest-api.md § Execution Visualization for the underlying REST API.

Worked examples (default-shipped). Six chart-bearing definitions ship in the in-tree content library as the default demo set:

DefinitionChart
security.vuln_scan.summarypie — vulnerabilities by severity
security.antivirus.defender_statuspie — Defender real-time protection
security.encryption.statepie — volume protection (BitLocker / LUKS / FileVault)
security.firewall.statecolumn (multi-series) — firewall state per profile
security.certificates.listpie — certificates by issuer (top-N + Other)
device.os_info.os_namepie — OS distribution across the fleet

They are bundled as InstructionSet demo.visualization.fleet-posture (content/definitions/visualization_demo_set.yaml) and as ProductPack pack.demo.visualization (content/packs/visualization-demo-pack.yaml) and are auto-imported on server startup — no manual import step is required on a fresh install. The 217 InstructionDefinitions and 10 InstructionSets under content/ are converted to JSON envelopes at build time by server/core/scripts/embed_content.py (PyYAML), embedded in the server binary as kBundledDefinitions / kBundledSets, and upserted on every startup via import_definition_json / create_set. Conflicts on already-existing IDs are silently skipped — operator-customised definitions are never overwritten by the auto-import. Each successful or errored import emits an audit_events.action="content.bundled_import" row with principal=system for SOC 2 traceability.

On upgrade from a release prior to the build-time content embed, definitions an operator previously DELETED will reappear after restart. To permanently suppress a shipped definition, set enabled: false via the dashboard or PATCH /api/v1/definitions/{id} rather than deleting the row.

To force-install on an existing deployment via the operator UI (e.g. for a custom InstructionSet not in the in-tree library), still use the dashboard YAML import view or:

curl -X POST -H "Authorization: Bearer $TOKEN" \
     --data-binary @content/packs/visualization-demo-pack.yaml \
     https://yuzu.example/api/v1/product-packs

Then dispatch any of the six from the dashboard “Send” panel — the chart deck auto-renders above the results table.

Response Templates (saved views)

A response template is a named bundle of column subset + sort order + filter presets attached to an InstructionDefinition. When the operator opens the execution results view for a definition, the filter bar’s View dropdown lists every available template; selecting one re-renders the table with the chosen columns visible, the chosen sort applied, and any equals-op filters auto-populated.

Synthesised default. Every definition has at least one template — the engine synthesises a __default__ view from spec.result.columns (or, when omitted, from the plugin’s hard-coded column schema) so the dropdown is never empty. The synthesised default lists all columns, applies no sort, and applies no filters — i.e. it matches the legacy “show everything” behaviour.

Authoring. Operators can author additional templates either by including a spec.responseTemplates array in the YAML definition (see docs/yaml-dsl-spec.md § spec.responseTemplates) or via the REST CRUD surface:

VerbPathPurpose
GET/api/v1/definitions/{id}/response-templatesList, with the synthesised default auto-prepended when no operator default is set
GET/api/v1/definitions/{id}/response-templates/{tid}Fetch one
POST/api/v1/definitions/{id}/response-templatesCreate (auto-assigns id when absent)
PUT/api/v1/definitions/{id}/response-templates/{tid}Replace in place
DELETE/api/v1/definitions/{id}/response-templates/{tid}Remove. Rejects __default__

Reads are gated on InstructionDefinition:Read, mutations on InstructionDefinition:Write.

Limitations operators should expect:


14. Planned Features

The following instruction engine capabilities are on the roadmap but not yet implemented.

Workflow Primitives (Phase 7.14)

Conditional logic, iteration, and retry within instruction execution:

These will be expressed as additional YAML DSL constructs within an InstructionDefinition’s spec.workflow block.

Product Packs (Phase 7.9)

Signed distribution bundles containing instruction sets, definitions, policy fragments, and documentation. Product packs already carry Ed25519 signatures: a pack’s signature is verified at install against the key inside the pack, a pack that fails verification is rejected, and an unsigned pack is refused unless the server runs with --allow-unsigned-packs. A trust chain anchored to an organization root key, and staged review before activation, are still planned.