Getting Started
A hands-on tutorial for the Yuzu Instruction Engine.
Prerequisites: A running Yuzu server and at least one connected agent. You should be able to log in to the dashboard at http://localhost:8080 and see your agent(s) on the Devices page. All curl commands below assume you have an active admin session cookie — replace $COOKIE with your session token.
Introduction
When you first connect an agent and dispatch a command through the dashboard, Yuzu sends a raw CommandRequest over gRPC: a plugin name, an action name, and optional parameters. This works, but it leaves important questions unanswered. Who is allowed to run this command? What parameters does it accept? What does the output look like? Can it be scheduled? Does it need approval before it runs?
The Instruction Engine answers all of these. It is a governed lifecycle layer that sits between the operator and the plugin execution layer, turning ad-hoc plugin-plus-action-plus-params commands into managed, auditable operations.
Instead of dispatching raw commands, operators define reusable InstructionDefinitions — versioned templates that declare everything about an endpoint operation: what plugin and action to invoke, what typed parameters it accepts, what typed columns it returns, which platforms it supports, whether it needs approval, and how long its results are retained.
Here are the key concepts you will use in this tutorial:
-
InstructionDefinition — A reusable, versioned template for an endpoint operation. It declares the plugin, action, parameters, result schema, approval requirements, and retention policy in one place.
-
Question vs. Action — Definitions have a
typefield. Aquestionis read-only: it gathers information without modifying endpoint state (list services, read OS info, check disk space). Anactionmay modify state (restart a service, install a package, change a registry value). This distinction drives the approval workflow — questions can auto-execute, while actions can require sign-off. -
Parameters — Typed inputs with validation rules. Parameters are declared with a JSON Schema-style syntax (type, required fields, max length, allowed values) and
POST /api/instructions/{id}/executevalidates them before the command is dispatched to any agent (no other dispatch surface does yet). -
Result Schema — Typed output columns (string, int32, int64, bool, datetime) that describe the structure of response data. Typed results enable server-side aggregation (COUNT, SUM, AVG grouped by column) and clean export to CSV, JSON, ClickHouse, or Splunk.
-
Scope Expressions — Target specific devices using a filter expression instead of listing agent IDs. The scope engine supports attribute comparisons (
ostype == "windows"), tag lookups (tag:env == "production"), wildcards (hostname LIKE "web-*"), and boolean combinators (AND,OR,NOT). -
Approval Modes — Control who can execute what.
automeans the instruction runs immediately.role-gatedmeans it enters a pending queue and waits for an admin to approve or reject it.alwaysmeans every execution requires approval, regardless of who submits it. -
InstructionSet — A permission boundary that groups related definitions together and declares which roles can author, execute, and approve the definitions it contains.
The /api/v1 REST routes are described in the API reference at http://localhost:8080/swagger, which lists each operation with its parameters, bodies and responses and opens each group with a guide. It needs the Infrastructure:Read permission (with RBAC switched off, any signed-in account passes), has no menu entry, and is linked from Settings > Developer and API, which only administrators can open. The /api/... dashboard-session routes used in this tutorial (such as /api/command and /api/instructions/yaml) are not in that reference; see the REST API reference for them.
Let’s build all of this up, one step at a time.
Step 1: Hello, Infrastructure!
Start with the simplest possible instruction definition — a read-only question that calls the os_info plugin’s os_name action to return the operating system name from every targeted endpoint.
Define it in YAML
Create a file called hello-system-info.yaml:
apiVersion: yuzu.io/v1alpha1
kind: InstructionDefinition
metadata:
id: hello.system.info
displayName: Get System Info
version: 1.0.0
description: Returns basic OS information from all targeted endpoints.
spec:
type: question
platforms: [windows, linux, darwin]
execution:
plugin: os_info
action: os_name
This YAML is the canonical authoring format. Post the file itself: the yaml_source form field carries the document verbatim, and the server derives the queryable columns (metadata.id, spec.execution.plugin, spec.execution.action, …) from it — the YAML remains the definition’s source of truth.
Import it via the API
Cookie-session writes to the instruction routes must carry an Origin (or Referer) naming the server; token-authenticated requests do not.
curl -s -X POST http://localhost:8080/api/instructions/yaml \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
--data-urlencode "yaml_source@hello-system-info.yaml"
Expected response:
<div class="alert alert-success">Definition created</div>
The server uses metadata.id (hello.system.info) as the definition’s ID, takes its display name from metadata.displayName, and records an audit event: who created it, when, and from where.
Note: there is also a JSON import endpoint,
POST /api/instructions/import, meant for distributing definitions as signed envelopes (an Ed25519signature+publicKeypair over theyaml_sourcebytes). On a default server, unsigned imports through that endpoint are rejected. Bypassing that with--allow-unsigned-definitions(orYUZU_ALLOW_UNSIGNED_DEFINITIONS=1) is dangerous — unsigned instruction content is a fleet arbitrary-code-execution surface, the server emits aserver.unsigned_definitions_allowedaudit event at startup when it is set, and it should not be used outside isolated labs (see Server Administration). The endpoint used above is the authoring surface instead: it trusts your authenticated session’sInstructionDefinition:Writepermission, so it works on a default server with no signing bypass.Re-running the same import is safe: because the definition’s ID comes from
metadata.id, a second attempt returns HTTP 409 withCreate failed: instruction definition 'hello.system.info' already existsrather than creating a duplicate.
Verify the definition exists
curl -s http://localhost:8080/api/instructions/hello.system.info \
-b "$COOKIE" | python -m json.tool
Expected response (abbreviated):
{
"id": "hello.system.info",
"name": "Get System Info",
"version": "1.0.0",
"type": "question",
"plugin": "os_info",
"action": "os_name",
"description": "Returns basic OS information from all targeted endpoints.",
"enabled": true,
"instruction_set_id": "",
"created_at": 1742300000,
"updated_at": 1742300000
}
Execute it
Now dispatch the command to all connected agents:
curl -s -X POST http://localhost:8080/api/command \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{
"plugin": "os_info",
"action": "os_name"
}'
Expected response:
{
"command_id": "os_info-a1b2c3d4e5f6g7h8",
"sent_to": 1,
"status": "dispatched"
}
View the results
curl -s "http://localhost:8080/api/responses/os_info-a1b2c3d4e5f6g7h8" \
-b "$COOKIE" | python -m json.tool
Expected response:
{
"instruction_id": "os_info-a1b2c3d4e5f6g7h8",
"count": 1,
"responses": [
{
"id": 1,
"instruction_id": "os_info-a1b2c3d4e5f6g7h8",
"agent_id": "agent-001",
"timestamp": 1742300005,
"status": 1,
"output": "os_name|Windows 11 Pro",
"error_detail": ""
}
]
}
What happened under the hood
- The server received your
POST /api/commandrequest and authenticated your session. - It constructed a gRPC
CommandRequestwithplugin=os_infoandaction=os_name. - The request was dispatched to all connected agents over the bidirectional gRPC stream.
- Each agent’s plugin host loaded the
os_infoplugin, invoked theos_nameaction, and streamed the result back as aCommandResponse. - The server’s
ResponseStorepersisted each response in PostgreSQL with the command ID, agent ID, timestamp, status, and output. - An audit event was logged with your username, the action taken, and the number of agents targeted.
Step 2: Adding Parameters
The first example had no inputs — it ran the same action on every agent. Most real-world instructions need parameters. Let’s create a definition that inspects a specific service by name.
Define it in YAML
Create a file called service-inspect.yaml:
apiVersion: yuzu.io/v1alpha1
kind: InstructionDefinition
metadata:
id: tutorial.service.inspect
displayName: Inspect Service Status
version: 1.0.0
description: Returns the current state of a named service.
spec:
type: question
platforms: [windows, linux, darwin]
execution:
plugin: services
action: list
parameters:
type: object
required: [serviceName]
properties:
serviceName:
type: string
displayName: Service Name
description: The name of the service to inspect.
validation:
maxLength: 256
readablePayload: "Inspect service '${serviceName}'"
Key additions:
parametersdeclares a typed input schema. TheserviceNamefield is required, must be a string, and is limited to 256 characters.readablePayloadis a human-readable template that interpolates parameter values. When an operator views the execution log, they see “Inspect service ‘sshd’” instead of a raw JSON blob.
Import it
curl -s -X POST http://localhost:8080/api/instructions/yaml \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
--data-urlencode "yaml_source@service-inspect.yaml"
Expected response:
<div class="alert alert-success">Definition created</div>
Execute with parameters
curl -s -X POST http://localhost:8080/api/command \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{
"plugin": "services",
"action": "list"
}'
Expected response:
{
"command_id": "services-f1e2d3c4b5a6a7b8",
"sent_to": 1,
"status": "dispatched"
}
View the results
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8" \
-b "$COOKIE" | python -m json.tool
On a Windows agent, you will see pipe-delimited output with the service name, display name, status, and startup type:
{
"instruction_id": "services-f1e2d3c4b5a6a7b8",
"count": 1,
"responses": [
{
"id": 2,
"instruction_id": "services-f1e2d3c4b5a6a7b8",
"agent_id": "agent-001",
"timestamp": 1742300100,
"status": 1,
"output": "svc|Spooler|Print Spooler|running|automatic\nsvc|sshd|OpenSSH SSH Server|running|automatic\nsvc|W32Time|Windows Time|stopped|manual",
"error_detail": ""
}
]
}
On Linux, the output format is svc|name|status|description.
Step 3: Typed Results
Raw pipe-delimited output works for viewing, but it is opaque to the server. To enable aggregation, filtering, and structured export, add a result schema that tells the server what columns the output contains and what types they are.
Extend the definition
Add a result section to the service inspect YAML:
apiVersion: yuzu.io/v1alpha1
kind: InstructionDefinition
metadata:
id: tutorial.service.inspect
displayName: Inspect Service Status
version: 1.0.0
description: Returns the current state of a named service.
spec:
type: question
platforms: [windows, linux, darwin]
execution:
plugin: services
action: list
parameters:
type: object
required: [serviceName]
properties:
serviceName:
type: string
displayName: Service Name
description: The name of the service to inspect.
validation:
maxLength: 256
readablePayload: "Inspect service '${serviceName}'"
result:
columns:
- name: serviceName
type: string
- name: state
type: string
- name: startupType
type: string
- name: pid
type: int32
aggregation:
groupBy: [state]
operations: [count]
The result.columns block declares four typed columns. The aggregation block declares default aggregation settings: group by the state column and count occurrences.
Use the aggregation endpoint
After executing the instruction and collecting responses, you can aggregate them server-side:
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8/aggregate?group_by=status" \
-b "$COOKIE" | python -m json.tool
Expected response:
{
"instruction_id": "services-f1e2d3c4b5a6a7b8",
"groups": [
{
"group_value": "1",
"count": 1,
"aggregate_value": 0.0
}
],
"total_groups": 1,
"total_rows": 1
}
The group_by parameter accepts status or agent_id. Additional aggregation operations (sum, avg, min, max) can be specified with the op query parameter, along with op_column to specify which column to aggregate.
Export as CSV
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8/export?format=csv" \
-b "$COOKIE" -o service-results.csv
This downloads a CSV file with headers:
id,instruction_id,agent_id,timestamp,status,output,error_detail
An export is capped at 10,000 rows and at 50 MiB of output. A cut CSV ends with one extra record
beginning # result_truncated_by_cap (parse the file as CSV and read the final record before
trusting a bulk pull; a quoted cell can contain text that looks like it, so do not regex-match
lines), and a cut JSON export has a top-level result_truncated_by_cap: true in the response body
on this legacy route (under pagination on the v1 route). A cut export is also saved under a
-truncated name by -OJ and carries X-Result-Truncated-By-Cap: true; a plain -o keeps neither
the name nor the header, so add -D - to print the headers (curl -sS -D - -o service-results.csv ...).
Export as JSON
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8/export?format=json" \
-b "$COOKIE" -o service-results.json
The JSON export returns a structured envelope:
{
"instruction_id": "services-f1e2d3c4b5a6a7b8",
"count": 1,
"responses": [
{
"id": 2,
"instruction_id": "services-f1e2d3c4b5a6a7b8",
"agent_id": "agent-001",
"timestamp": 1742300100,
"status": 1,
"output": "svc|Spooler|Print Spooler|running|automatic\n...",
"error_detail": ""
}
]
}
Typed results are what make Yuzu’s data useful beyond the dashboard. When every column has a declared type, downstream systems — ClickHouse, Splunk, Grafana, or a simple spreadsheet — can ingest the data without guessing.
Step 4: Your First Action
So far, every instruction has been a question — read-only, safe to run at any time. Now let’s define an action that modifies endpoint state: restarting a service.
Because actions change things, they should go through an approval workflow. We will set the approval mode to role-gated, which means the instruction enters a pending queue when submitted, and an admin must approve it before it executes.
Define it in YAML
Create a file called service-restart.yaml:
apiVersion: yuzu.io/v1alpha1
kind: InstructionDefinition
metadata:
id: tutorial.service.restart
displayName: Restart Service
version: 1.0.0
description: Restarts a named service. Requires approval.
spec:
type: action
platforms: [windows, linux, darwin]
execution:
plugin: services
action: restart
parameters:
type: object
required: [serviceName]
properties:
serviceName:
type: string
approval:
mode: role-gated
readablePayload: "Restart service '${serviceName}'"
Key differences from a question:
type: action— marks this as a state-changing operation.approval.mode: role-gated— requires an admin to approve before execution proceeds.
Import it
curl -s -X POST http://localhost:8080/api/instructions/yaml \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
--data-urlencode "yaml_source@service-restart.yaml"
Expected response:
<div class="alert alert-success">Definition created</div>
Walk through the approval flow
You need two accounts for this section. A reviewer account cannot approve or reject its own submission: the server rejects self-review with HTTP 400 (
"reviewer cannot be the same as the submitter"), and the dashboard shows “You submitted this — another reviewer must approve” instead of buttons on your own requests. If you have been following along as a single admin, create a second operator account now and use it for the approve/reject steps below. The approver must be a different human account, and (since #6079) it needs an enrolled MFA factor and a fresh proof, and with RBAC enforcement off (the default) it must hold theadminrole; see Who can approve.
1. Submit the action.
An operator submits the instruction for execution. Because the approval mode is role-gated, it does not execute immediately — it enters the pending queue. (role-gated queues non-admin submitters for approval; admins execute directly. To watch the queue while testing as an admin, use a definition with approval.mode: always instead.)
2. Check pending approvals.
An admin checks how many approvals are waiting:
curl -s http://localhost:8080/api/approvals/pending/count \
-b "$COOKIE"
Expected response:
{"count":1}
3. List pending approvals.
curl -s "http://localhost:8080/api/approvals?status=pending" \
-b "$COOKIE" | python -m json.tool
Expected response:
{
"approvals": [
{
"id": "d4e5f6a7-...",
"definition_id": "tutorial.service.restart",
"status": "pending",
"submitted_by": "operator1",
"submitted_at": 1742300200,
"reviewed_by": "",
"reviewed_at": 0,
"review_comment": "",
"scope_expression": ""
}
]
}
4. Approve it.
Approving needs a signed-in session cookie from a different account than the submitter’s (an API token is refused with 403) and an enrolled MFA factor with a fresh proof (#6079): an account with no enrolled factor is refused with 403 approver_assurance_required:mfa_not_enrolled, so enrol your approvers first. On a default install nobody is enrolled, so this step fails with that 403 until you do. If your approver is an administrator (the usual case in this tutorial), enrol that account under Settings → Security and sign-in → Two-factor sign-in, then send the request again. If your approver is a non-admin, it cannot enrol itself under the default --mfa-enforcement modes; see Enrolling a non-admin approver. Once an enrolled account has no recent proof, the server answers 401 with meta.mfa_step_up_required: post your code to /login/mfa/stepup on the same cookie and retry. A cookie-session POST must also carry an Origin (or Referer) naming the server, as a browser sends; a request with neither is refused with 403. See Who can approve.
curl -s -X POST http://localhost:8080/api/approvals/d4e5f6a7-.../approve \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Origin: http://localhost:8080" \
-H "Content-Type: application/json" \
-d '{"comment": "Approved for maintenance window"}'
Expected response:
{"status":"approved"}
Approving a ticket created by submitting an instruction records the decision but does not yet dispatch the instruction: an instruction-origin ticket has no redemption path (tracked in #6064; see Upgrading). The approval record is preserved in the audit trail — who submitted it, who approved it, when, and with what comment.
5. Reject (alternative).
If the action should not proceed, reject it instead:
curl -s -X POST http://localhost:8080/api/approvals/d4e5f6a7-.../reject \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Origin: http://localhost:8080" \
-H "Content-Type: application/json" \
-d '{"comment": "Not approved -- outside maintenance window"}'
Expected response:
{"status":"rejected"}
The full lifecycle is: submit -> pending -> approve/reject -> execute (if approved). Every step is audited.
Step 5: Organizing with InstructionSets
As your library of definitions grows, you need a way to group related instructions and control who can use them. That is what InstructionSets are for.
An InstructionSet is the permission boundary for instruction definitions. It declares which roles can author definitions in the set, which roles can execute them, and which roles can approve actions.
Define it in YAML
Create a file called service-management-set.yaml:
apiVersion: yuzu.io/v1alpha1
kind: InstructionSet
metadata:
id: tutorial.service-management
displayName: Service Management Tutorial
version: 1.0.0
description: Tutorial set for service inspection and management.
permissions:
executeRoles: [endpoint-operator, endpoint-admin]
authorRoles: [content-author]
approveRoles: [endpoint-admin]
contents:
instructionDefinitions:
- tutorial.service.inspect
- tutorial.service.restart
The permissions block is the key governance feature:
executeRoles— who can dispatch instructions from this set.authorRoles— who can create or modify definitions in this set.approveRoles— who can approve actions that require approval.
Create the set via the API
curl -s -X POST http://localhost:8080/api/instruction-sets \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{
"name": "tutorial.service-management",
"description": "Tutorial set for service inspection and management."
}'
Expected response:
{"id":"e5f6a7b8-..."}
List all sets
curl -s http://localhost:8080/api/instruction-sets \
-b "$COOKIE" | python -m json.tool
Expected response:
{
"sets": [
{
"id": "e5f6a7b8-...",
"name": "tutorial.service-management",
"description": "Tutorial set for service inspection and management.",
"created_by": "admin",
"created_at": 1742300300
}
]
}
Assign definitions to the set
When creating or updating an instruction definition, set the instruction_set_id field to the set’s ID. This binds the definition to the set’s permission rules.
curl -s -X PUT http://localhost:8080/api/instructions/tutorial.service.inspect \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{
"name": "tutorial.service.inspect",
"type": "question",
"plugin": "services",
"action": "list",
"description": "Returns the current state of a named service.",
"instruction_set_id": "e5f6a7b8-..."
}'
Once the granular RBAC system is fully implemented (Phase 3 on the roadmap), these permission declarations will be enforced server-side. For now, the structure is in place so that your definitions are organized and ready for access control from day one.
Step 6: Targeting with Scope Expressions
So far, every command has been broadcast to all connected agents. In production, you need to target specific devices — all Windows machines, all web servers in production, everything except quarantined endpoints.
Yuzu’s scope engine provides a filter expression language for this. It is a recursive-descent parser that supports attribute comparisons, tag lookups, wildcards, and boolean combinators.
Scope expression examples
Target all Windows endpoints:
ostype == "windows"
Target Linux web servers:
ostype == "linux" AND hostname LIKE "web-*"
Target production endpoints that are not quarantined:
tag:env == "production" AND NOT tag:quarantined == "true"
Validate a scope expression
Before using a scope expression in a command, you can validate its syntax:
curl -s -X POST http://localhost:8080/api/scope/validate \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{"expression": "ostype == \"windows\""}'
Expected response for a valid expression:
{"valid":true}
Expected response for an invalid expression:
{"valid":false,"error":"expected comparison operator at position 8"}
Estimate how many agents match
Before dispatching to a large fleet, check how many agents the scope expression matches:
curl -s -X POST http://localhost:8080/api/scope/estimate \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{"expression": "ostype == \"windows\""}'
Expected response:
{"matched":1,"total":3}
This tells you that 1 out of 3 connected agents match the expression. No command is dispatched — this is a dry run.
Dispatch with a scope expression
Use the scope field in the command request to target specific agents:
curl -s -X POST http://localhost:8080/api/command \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{
"plugin": "os_info",
"action": "os_name",
"scope": "ostype == \"windows\""
}'
Expected response:
{
"command_id": "os_info-c4d5e6f7g8h9i0j1",
"sent_to": 1,
"status": "dispatched"
}
Only agents matching the scope expression receive the command. The server evaluates the expression against each agent’s attributes (OS type, hostname, architecture) and tags.
Supported operators
| Operator | Example | Description |
|---|---|---|
== | ostype == "windows" | Exact equality (case-insensitive) |
!= | ostype != "linux" | Not equal |
LIKE | hostname LIKE "web-*" | Wildcard match (* = any chars) |
<, >, <=, >= | os_version > "10" | Numeric or string comparison |
IN | ostype IN ("windows", "linux") | Value in set |
CONTAINS | hostname CONTAINS "prod" | Substring match |
AND | a == "1" AND b == "2" | Both must be true |
OR | a == "1" OR b == "2" | Either must be true |
NOT | NOT tag:quarantined == "true" | Negation |
Step 7: Scheduling Recurring Checks
Running instructions manually is fine for one-off tasks, but many operations need to run on a schedule — service health checks every 15 minutes, disk space audits every hour, compliance scans every night.
Yuzu’s ScheduleEngine supports recurring execution with configurable frequency types.
Create a schedule via the API
Schedule the service inspect instruction to run every 15 minutes:
curl -s -X POST http://localhost:8080/api/schedules \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{
"name": "Service health check (every 15m)",
"definition_id": "tutorial.service.inspect",
"frequency_type": "interval",
"interval_minutes": 15,
"scope_expression": "ostype == \"windows\"",
"requires_approval": false
}'
Expected response:
{"id":"f6a7b8c9-..."}
List all schedules
curl -s http://localhost:8080/api/schedules \
-b "$COOKIE" | python -m json.tool
Expected response:
{
"schedules": [
{
"id": "f6a7b8c9-...",
"name": "Service health check (every 15m)",
"definition_id": "tutorial.service.inspect",
"enabled": true,
"frequency_type": "interval",
"next_execution_at": 1742301200,
"last_executed_at": 0,
"execution_count": 0
}
]
}
Disable a schedule
curl -s -X POST http://localhost:8080/api/schedules/f6a7b8c9-.../enable \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
Expected response:
{"enabled":false}
enabled must be a JSON boolean (or the integer 0 or 1); a string such as "false" is answered 400 and the schedule is left as it was.
Re-enable a schedule
curl -s -X POST http://localhost:8080/api/schedules/f6a7b8c9-.../enable \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
Delete a schedule
curl -s -X DELETE http://localhost:8080/api/schedules/f6a7b8c9-... \
-H "Origin: http://localhost:8080" \
-b "$COOKIE"
Expected response:
{"deleted":true}
Schedule parameters
| Field | Type | Description |
|---|---|---|
name | string | Human-readable name for the schedule |
definition_id | string | ID of the instruction definition to execute |
frequency_type | string | "once", "interval", "daily", "weekly", "monthly" |
interval_minutes | int | Minutes between executions (for interval type), 1-35791394 |
time_of_day | string | "HH:MM" time for daily/weekly/monthly schedules |
day_of_week | int | 0-6 (Sunday=0) for weekly schedules |
day_of_month | int | 1-28 for monthly schedules |
scope_expression | string | Scope expression to target agents |
requires_approval | bool | Whether each scheduled run needs approval |
Step 8: Exporting Results
Yuzu stores every response in its PostgreSQL-backed ResponseStore. You can query, aggregate, and export this data through the REST API.
Query responses for an instruction
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8" \
-b "$COOKIE" | python -m json.tool
Optional query parameters for filtering:
| Parameter | Description |
|---|---|
agent_id | Filter by specific agent |
status | Filter by response status (integer) |
since | Epoch seconds — only responses after this time |
until | Epoch seconds — only responses before this time |
limit | Maximum number of results (default: 100) |
offset | Pagination offset |
Example with filters:
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8?status=1&limit=50&since=1742300000" \
-b "$COOKIE" | python -m json.tool
Export as CSV
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8/export?format=csv" \
-b "$COOKIE" -o responses.csv
(A cut export is marked; the first “Export as CSV” section in this guide shows how to tell a cut export from a complete one.)
The CSV file contains one row per agent response with headers:
id,instruction_id,agent_id,timestamp,status,output,error_detail
1,services-f1e2d3c4b5a6a7b8,agent-001,1742300100,1,"svc|Spooler|Print Spooler|running|automatic",""
Export as JSON
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8/export?format=json" \
-b "$COOKIE" -o responses.json
Aggregate responses
Group responses by status and count:
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8/aggregate?group_by=status" \
-b "$COOKIE" | python -m json.tool
Expected response:
{
"instruction_id": "services-f1e2d3c4b5a6a7b8",
"groups": [
{"group_value": "1", "count": 3, "aggregate_value": 0.0},
{"group_value": "2", "count": 1, "aggregate_value": 0.0}
],
"total_groups": 2,
"total_rows": 4
}
Group by agent and compute aggregates:
curl -s "http://localhost:8080/api/responses/services-f1e2d3c4b5a6a7b8/aggregate?group_by=agent_id&op=count" \
-b "$COOKIE" | python -m json.tool
Supported aggregation operations: count, sum, avg, min, max. Specify the target column with op_column when using sum, avg, min, or max.
Execution tracking
For a higher-level view, query the execution tracker:
curl -s http://localhost:8080/api/executions?definition_id=tutorial.service.inspect \
-b "$COOKIE" | python -m json.tool
Get a summary with progress percentage:
curl -s http://localhost:8080/api/executions/EXECUTION_ID/summary \
-b "$COOKIE" | python -m json.tool
View per-agent status:
curl -s http://localhost:8080/api/executions/EXECUTION_ID/agents \
-b "$COOKIE" | python -m json.tool
Rerun an execution (optionally targeting only failed agents):
curl -s -X POST http://localhost:8080/api/executions/EXECUTION_ID/rerun \
-H "Origin: http://localhost:8080" \
-b "$COOKIE" \
-H "Content-Type: application/json" \
-d '{"scope": "failed_only"}'
What’s Next
This tutorial covered the core instruction lifecycle: define, import, execute, parameterize, type results, govern with approvals, organize into sets, target with scopes, schedule, and export. Here is where to go from here:
-
Instruction Engine architecture —
docs/Instruction-Engine.mdcovers the full design, including the content model hierarchy (ProductPack > InstructionSet > InstructionDefinition), the execution substrate, error taxonomy, and concurrency model. -
Capability map —
docs/capability-map.mdtracks all 139 planned capabilities and which ones are implemented. Use this to understand the full scope of what Yuzu is building toward. -
Roadmap —
docs/roadmap.mdhas the 126 issues across 17 tracked phases (0–16). The features introduced in this tutorial span Phases 1-3. -
PolicyFragments and compliance automation (Phase 5) — PolicyFragments compose a check instruction, a compliance expression, and a fix instruction into a single unit. Policies bind fragments to triggers and scopes for continuous compliance enforcement.
-
TriggerTemplates for event-driven automation (Phase 4) — Instead of schedules, fire instructions in response to events: file changes, service status transitions, event log entries, registry modifications, or agent startup.
-
ProductPacks for content distribution (Phase 7) — Bundle InstructionSets, PolicyFragments, and static content into signed, versioned packages that can be distributed to servers and automatically applied.
-
Grafana dashboards —
docs/grafana/contains dashboard templates for visualizing Yuzu metrics. The server exposes a Prometheus-compatible/metricsendpoint withyuzu_server_*metrics. -
Analytics integration —
docs/clickhouse-setup.mdanddocs/analytics-events.mdcover how to forward Yuzu’s structured events and response data to ClickHouse or Splunk for long-term analytics.