yuzu

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:

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 Ed25519 signature + publicKey pair over the yaml_source bytes). On a default server, unsigned imports through that endpoint are rejected. Bypassing that with --allow-unsigned-definitions (or YUZU_ALLOW_UNSIGNED_DEFINITIONS=1) is dangerous — unsigned instruction content is a fleet arbitrary-code-execution surface, the server emits a server.unsigned_definitions_allowed audit 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’s InstructionDefinition:Write permission, 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 with Create failed: instruction definition 'hello.system.info' already exists rather 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

  1. The server received your POST /api/command request and authenticated your session.
  2. It constructed a gRPC CommandRequest with plugin=os_info and action=os_name.
  3. The request was dispatched to all connected agents over the bidirectional gRPC stream.
  4. Each agent’s plugin host loaded the os_info plugin, invoked the os_name action, and streamed the result back as a CommandResponse.
  5. The server’s ResponseStore persisted each response in PostgreSQL with the command ID, agent ID, timestamp, status, and output.
  6. 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:

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:

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 the admin role; 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:

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

OperatorExampleDescription
==ostype == "windows"Exact equality (case-insensitive)
!=ostype != "linux"Not equal
LIKEhostname LIKE "web-*"Wildcard match (* = any chars)
<, >, <=, >=os_version > "10"Numeric or string comparison
INostype IN ("windows", "linux")Value in set
CONTAINShostname CONTAINS "prod"Substring match
ANDa == "1" AND b == "2"Both must be true
ORa == "1" OR b == "2"Either must be true
NOTNOT 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

FieldTypeDescription
namestringHuman-readable name for the schedule
definition_idstringID of the instruction definition to execute
frequency_typestring"once", "interval", "daily", "weekly", "monthly"
interval_minutesintMinutes between executions (for interval type), 1-35791394
time_of_daystring"HH:MM" time for daily/weekly/monthly schedules
day_of_weekint0-6 (Sunday=0) for weekly schedules
day_of_monthint1-28 for monthly schedules
scope_expressionstringScope expression to target agents
requires_approvalboolWhether 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:

ParameterDescription
agent_idFilter by specific agent
statusFilter by response status (integer)
sinceEpoch seconds — only responses after this time
untilEpoch seconds — only responses before this time
limitMaximum number of results (default: 100)
offsetPagination 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: