ReferenceSpec
Workflow
Skill grants, skill manifests, and signed workflow receipts for WORKFLOW.md version 1.0, with the WorkflowAuthority lifecycle the chio-workflow crate implements.
Source
This page normatively reflects spec/WORKFLOW.md in the chio repository. Status: Normative. Version 1.0, dated 2026-04-14. The keywords MUST, SHOULD, and MAY are normative.
Lifecycle behavior is read from crates/platform/chio-workflow/src/authority.rs, and the object shapes from grant.rs, manifest.rs, and receipt.rs in the same crate. Where the specification and the crate differ, the page describes the crate and names the function.
Synopsis
The schema ids and the lifecycle type; the last column reads spec/schemas/registry.json at the pin.
| Object | Schema id | Top-level shape | Registry |
|---|---|---|---|
SkillGrant | chio.skill-grant.v1 | schema, skill_id, skill_version, authorized_steps[], max_executions?, budget_envelope?, max_duration_secs?, strict_ordering | not listed |
SkillManifest | chio.skill-manifest.v1 | schema, skill_id, version, name, description?, steps: SkillStep[], budget_envelope?, max_duration_secs?, author? | not listed |
WorkflowReceipt | chio.workflow-receipt.v1 | the WorkflowReceiptBody fields, then signature and vendor_signatures[] | not listed |
WorkflowAuthority | none | begin, validate_step, record_step, finalize | none |
Purpose
A skill is an ordered sequence of tool invocations that composes multiple tools into a single authorized unit of work. The crate defines these types:
- SkillGrant. Extends the capability model for ordered tool sequences with budget envelopes and execution limits.
- SkillManifest. Declares tool dependencies, I/O contracts between steps, and budget requirements.
- WorkflowReceipt. Records the execution trace in one signed receipt.
- WorkflowAuthority. Validates each step against declared scope, ordering, budget, and time constraints.
SkillGrant
A SkillGrant authorizes an agent to execute a named skill. Unlike individual tool grants, a skill grant binds an entire tool sequence under a single authorization with a shared budget envelope.
Schema
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
schema | string | yes | none | MUST be "chio.skill-grant.v1" |
skill_id | string | yes | none | Unique skill identifier |
skill_version | string | yes | none | Version of the skill manifest this grant authorizes |
authorized_steps | string[] | yes | none | Tool steps in declared order; format "server_id:tool_name" |
max_executions | u32 | no | null (unlimited) | Maximum number of complete skill executions |
budget_envelope | MonetaryAmount | no | null | Budget for the entire execution |
max_duration_secs | u64 | no | null | Maximum wall-clock seconds per execution |
strict_ordering | bool | no | true | Whether steps MUST execute in declared order |
The Rust struct is SkillGrant in grant.rs. The optional fields are omitted from the JSON when absent, and a missing strict_ordering deserializes as true.
Step authorization
A step is authorized if "server_id:tool_name" appears in the authorized_steps list. Invocations of tools not in the list MUST be rejected. The crate makes this check in SkillGrant::authorizes_step.
Ordering modes
When strict_ordering is true (the default), each step MUST execute at index equal to the number of previously completed steps. A step submitted out of order MUST be rejected with StepOutOfOrder.
When strict_ordering is false (relaxed mode), steps may execute in any order. All steps MUST still be in the authorized_steps list. SkillGrant::is_step_in_order returns true for every index in relaxed mode.
SkillManifest
A SkillManifest is authored by the skill developer and declares the full execution plan for a skill.
Schema
| Field | Type | Required | Description |
|---|---|---|---|
schema | string | yes | MUST be "chio.skill-manifest.v1" |
skill_id | string | yes | Unique skill identifier |
version | string | yes | Semantic version |
name | string | yes | Human-readable name |
description | string | no | Human-readable description |
steps | SkillStep[] | yes | Ordered steps in the skill |
budget_envelope | MonetaryAmount | no | Budget for a single execution |
max_duration_secs | u64 | no | Maximum wall-clock seconds |
author | string | no | Author identifier |
SkillStep
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
index | usize | yes | none | Step index (0-based) |
server_id | string | yes | none | Tool server hosting this step's tool |
tool_name | string | yes | none | Tool to invoke |
label | string | no | null | Human-readable step label |
input_contract | IoContract | no | null | Input data contract |
output_contract | IoContract | no | null | Output data contract |
budget_limit | MonetaryAmount | no | null | Per-step budget limit |
retryable | bool | no | false | Whether this step can be retried |
max_retries | u32 | no | null | Maximum retries (only relevant when retryable is true) |
IoContract
The IoContract type describes data flow between steps. Each list field deserializes as empty when absent.
| Field | Type | Description |
|---|---|---|
required_fields | string[] | Field names required by the step (inputs) or guaranteed (outputs) |
produced_fields | string[] | Field names this step produces |
optional_fields | string[] | Optional field names |
json_schema | JSON | Optional JSON Schema for the data structure |
I/O contract validation
Implementations MUST validate that I/O contracts form a consistent data flow. SkillManifest::validate_io_contracts walks the steps in order and applies these rules:
- For each step after the first, every field in
input_contract.required_fieldsMUST appear in theoutput_contract.produced_fieldsof some preceding step. - The first step's input requirements come from the caller and are not validated against the manifest.
- A violation is reported with the step index, the tool name, and the missing field name.
Tool dependencies
The manifest's tool dependencies are the list of "server_id:tool_name" strings derived from each step, returned by SkillManifest::tool_dependencies. begin checks every manifest step against the grant before an execution starts.
WorkflowReceipt
A WorkflowReceipt records a skill execution in one signed receipt.
WorkflowReceiptBody
| Field | Type | Description |
|---|---|---|
id | string | Unique receipt ID; the execution id begin minted |
schema | string | MUST be "chio.workflow-receipt.v1" |
started_at | u64 | Unix timestamp when execution started |
completed_at | u64 | Unix timestamp when execution completed |
skill_id | string | Skill ID from the manifest |
skill_version | string | Skill version from the manifest |
agent_id | string | Agent that executed the workflow |
session_id | string | Session binding; omitted when null |
capability_id | string | Capability that authorized the workflow |
outcome | WorkflowOutcome | Overall outcome |
steps | StepRecord[] | Per-step execution records |
total_cost | MonetaryAmount | Total cost; omitted when null |
duration_ms | u64 | Wall-clock duration in milliseconds |
kernel_key | PublicKey | Kernel public key |
chio.workflow-receipt.v1 is the only workflow receipt schema a verifier accepts. Verifiers MUST reject unknown schema values fail-closed: WorkflowReceipt::verify returns false for any other schema value before it checks the signature.
Signed WorkflowReceipt
The signed WorkflowReceipt inlines all fields from WorkflowReceiptBody and adds a signature field (Ed25519 signature over canonical JSON of the body). The signature MUST be computed over canonical_json_bytes(body) using RFC 8785 canonical JSON; the crate signs through Keypair::sign_canonical.
The receipt MAY also carry vendor_signatures, a list of WorkflowVendorSignature entries with the camel-case fields vendorId, publicKey, and signature; the list is omitted when empty. Each vendor co-signature signs the same canonical workflow body as the kernel signature. WorkflowReceipt::verify_vendor_signatures takes the required vendor signers and rejects a missing vendor signature, a public key that differs from the required one, and a signature that fails verification, in that order per signer.
WorkflowOutcome
The enum serializes with a status tag in snake case; the variant fields sit beside it.
| Variant | Wire status | Fields | Description |
|---|---|---|---|
Completed | completed | none | All steps completed successfully |
Denied | denied | reason | Workflow denied before execution started |
StepFailed | step_failed | step_index, reason | A step failed, halting the workflow |
BudgetExceeded | budget_exceeded | limit_units, spent_units, currency | Budget envelope exceeded |
TimedOut | timed_out | limit_secs, elapsed_secs | Time limit exceeded |
Cancelled | cancelled | reason | Canceled by agent or operator |
StepRecord
| Field | Type | Description |
|---|---|---|
step_index | usize | Step index in the manifest |
server_id | string | Tool server |
tool_name | string | Tool name |
allowed | bool | Whether the step was authorized to execute; record_step sets it for a success or failed outcome |
tool_receipt_id | string | Receipt ID for the underlying governed call (nullable) |
outcome | StepOutcome | Step-level outcome |
duration_ms | u64 | Step duration in milliseconds |
cost | MonetaryAmount | Cost attributed to this step (nullable) |
output_hash | string | SHA-256 hash of step output (nullable) |
bilateral_dsse_sha256 | string | SHA-256 hash of the strict bilateral DSSE envelope for this step (optional) |
governance_receipt_id | string | Governance receipt id authorizing this step (optional) |
parent_receipt_sha256 | string | SHA-256 hash of the previous step receipt in the workflow chain (optional) |
consistency_anchor | string | Per-step consistency anchor value (optional) |
destructive | bool | Destructive-action marker (optional) |
Every optional field is omitted from the JSON when absent. record_step fills tool_receipt_id, cost, and output_hash from its input and leaves the remaining optional fields unset. The specification requires that a destructive step be backed by a live capability lease and a matching governance receipt when the verifier's action-class policy marks the step as receipt-backed.
StepOutcome
| Value | Description |
|---|---|
success | Step completed successfully |
denied | Step denied by policy |
failed | Step failed during execution |
skipped | Step skipped (workflow aborted before reaching it) |
Signature verification
WorkflowReceipt::verify reconstructs the WorkflowReceiptBody from the receipt fields and verifies the Ed25519 signature over its canonical JSON serialization using the embedded kernel_key. A tampered receipt (any field modified after signing) MUST fail verification.
WorkflowAuthority lifecycle
The WorkflowAuthority manages the lifecycle of skill executions. It holds the kernel signing key, counts the executions it has started, and keeps a per-capability reservation table for grants that carry max_executions. The signatures below are quoted from the specification; the numbered behavior is the crate's.
begin
begin(manifest, grant, agent_id, capability_id, session_id)
-> Result<WorkflowExecution, WorkflowError>Preconditions, checked in this order:
grant.skill_id == manifest.skill_idandgrant.skill_version == manifest.version. Fail:UnauthorizedSkill.- Every step in the manifest MUST be authorized by the grant:
grant.authorized_stepscontains"step.server_id:step.tool_name"for each step. Fail:UnauthorizedStep. - If
grant.max_executionsis set, the authority reserves one execution for thecapability_id; completed plus in-flight executions for that capability MUST be below the limit. Fail:ExecutionLimitReached.
On success the authority increments its execution count and returns a WorkflowExecution with:
- Budget limit from
grant.budget_envelopeormanifest.budget_envelope(grant takes precedence). - Time limit from
grant.max_duration_secsormanifest.max_duration_secs(grant takes precedence). - An
idof the formwf-{started_at}-{sequence}-{agent_id}, where the sequence is a per-authority counter so two executions begun in the same second get distinct ids. activeset totrue.- Empty
step_recordsand zerobudget_spent.
validate_step
validate_step(execution, step, grant) -> Result<(), WorkflowError>Preconditions, checked in order:
execution.activeMUST betrue. Fail:InvalidState.- The step MUST be authorized by the grant. Fail:
UnauthorizedStep. - If strict ordering is enabled,
step.index == execution.completed_steps(), wherecompleted_stepscounts the records whose outcome issuccess. Fail:StepOutOfOrder. - If a time limit is set, elapsed time MUST be less than the limit. Fail:
TimeLimitExceeded.
record_step
record_step(execution, step, outcome, duration_ms, cost, tool_receipt_id, output_hash)
-> Result<(), WorkflowError>The crate passes the arguments after step as one StepExecutionRecordInput value. Behavior:
- If
execution.activeisfalse, returnInvalidState. - Evaluate, without returning yet: whether the time limit has elapsed; whether
cost.currencydiffers from the currency ofstep.budget_limitor of the execution budget; and whethercost.unitsexceedsstep.budget_limit.units. - Unless the currency mismatched, add
cost.units(if present) toexecution.budget_spentusing saturating addition. - Append a
StepRecordtoexecution.step_records. The record is always appended, even when a check below fails, so the finalized receipt carries the offending step. - Return the first failure that applies, each of which sets
activetofalse: a currency mismatch returnsBudgetCurrencyMismatchand records aDeniedoutcome; a per-step limit breach returnsBudgetExceeded;budget_spent > budget_limit.unitsreturnsBudgetExceeded; an elapsed time limit returnsTimeLimitExceededand records aTimedOutoutcome. - If
outcomeisfailedordenied, setactivetofalse.
finalize
finalize(execution) -> Result<WorkflowReceipt, WorkflowError>Behavior, in the order the specification numbers:
- Set
execution.activetofalse. - Determine the
WorkflowOutcome, in this precedence:- If any step record has outcome
failedordenied, the outcome isStepFailedwith that step's index. - Otherwise a terminal outcome that
record_steprecorded (Denied,BudgetExceeded, orTimedOut) stands; a time limit that elapsed by finalize time also yieldsTimedOut. - Otherwise, if
budget_spent > budget_limit.units, the outcome isBudgetExceeded. - Otherwise the outcome is
Completed.
- If any step record has outcome
- Construct
WorkflowReceiptBodywith all execution data:completed_atandduration_msfrom the clock,total_costasbudget_spentin the envelope's currency when spend is positive and an envelope exists, andkernel_keyas the authority's public key. - Sign the body:
keypair.sign_canonical(body). Fail:SigningFailed. - For a grant with
max_executions, move the capability's reservation from in-flight to completed. The authority's execution count was already incremented bybegin. - Return the signed
WorkflowReceipt.
WorkflowError
| Error | Fields | Description |
|---|---|---|
UnauthorizedSkill | skill_id, version | Grant does not authorize the requested skill |
UnauthorizedStep | step_index, server, tool | Step not in the grant's authorized list |
StepOutOfOrder | step_index, expected | Step submitted out of sequence |
BudgetExceeded | limit_units, spent_units, currency | Budget envelope or per-step limit exceeded |
BudgetCurrencyMismatch | expected_currency, actual_currency | A step reported cost in a currency that does not match the declared budget |
TimeLimitExceeded | elapsed_secs, limit_secs | Time limit exceeded |
ExecutionLimitReached | limit | Maximum executions reached |
InvalidState | message | Workflow is not in the correct state |
SigningFailed | message | Receipt signing error |
Example
Section 7 of spec/WORKFLOW.md gives this manifest for a two-step search-and-summarize skill:
# Skill Manifest
schema: chio.skill-manifest.v1
skill_id: search-and-summarize
version: "1.0.0"
name: Search and Summarize
steps:
- index: 0
server_id: search-srv
tool_name: search
label: Search
output_contract:
produced_fields: [results]
- index: 1
server_id: llm-srv
tool_name: summarize
label: Summarize
input_contract:
required_fields: [results]
output_contract:
produced_fields: [summary]
budget_envelope:
units: 1000
currency: USDThe same section pairs it with this grant, which authorizes both steps under one envelope:
# Skill Grant
schema: chio.skill-grant.v1
skill_id: search-and-summarize
skill_version: "1.0.0"
authorized_steps:
- search-srv:search
- llm-srv:summarize
budget_envelope:
units: 1000
currency: USD
max_executions: 10
strict_ordering: trueExecution flow, in the order the specification lists it:
authority.begin(manifest, grant, agent, capability, session): validates that the grant matches the manifest and creates the execution.authority.validate_step(execution, step_0, grant): checks authorization, ordering, and time.- Invoke
search-srv:searchand collect the result. authority.record_step(execution, step_0, Success, 100ms, $0.50): records the cost and checks the budget.authority.validate_step(execution, step_1, grant): checks authorization, ordering, and time.- Invoke
llm-srv:summarizeand collect the result. authority.record_step(execution, step_1, Success, 200ms, $1.00): records the cost and checks the budget.authority.finalize(execution): signs the receipt and completes the execution reservation.
Schema registration
The specification defines chio.skill-grant.v1, chio.skill-manifest.v1, chio.workflow-receipt.v1, and spec/schemas/registry.json does not list them. The crate carries each id as a constant in the module that defines the object.
The chio-workflow family the registry does carry holds the preflight plan the CLI reads and the report it prints:
| Schema id | Kind | File |
|---|---|---|
chio.workflow.preflight-plan.v1 | workflow_preflight_plan | spec/schemas/chio-workflow/v1/preflight-plan.schema.json |
chio.workflow.preflight-report.v1 | workflow_preflight_report | spec/schemas/chio-workflow/v1/preflight-report.schema.json |
CLI command
The help text for chio workflow reads: Validate read-only workflow planning evidence before dispatch. It is a preflight and validation command. It does not run skills, spend budget, or emit signed workflow receipts.
An accepted plan prints a chio.workflow.preflight-report.v1 report with verdict accepted and evidence class planning, and the command exits 0:
$ chio workflow preflight --plan ./valid-plan.json | jq .{
"schema": "chio.workflow.preflight-report.v1",
"id": "workflow-preflight-report-workflow-preflight-valid-child-scope",
"issued_at": "2026-06-10T00:00:00Z",
"plan_id": "workflow-preflight-valid-child-scope",
"verdict": "accepted",
"evidence_class": "planning",
"verified_claims": [
"claim.workflow.preflight_child_scope_bounded",
"claim.workflow.preflight_planning_only"
],
"rejected_checks": [],
"live_authority_claims": []
}A plan whose child task requests an action outside the parent scope is rejected: the command names the rejected check on stderr and exits 1:
$ chio workflow preflight --plan ./broader-plan.jsonerror [urn:chio:error:cli:other]: workflow preflight rejected: child task task-child-payment requested action payment.capture outside parent scope
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.| Subcommand | Help text | Required option |
|---|---|---|
chio workflow preflight | Validate a read-only workflow preflight plan | --plan <PATH>: Path to a chio.workflow.preflight-plan.v1 JSON artifact |
The full option list is on the CLI reference.
Related
- Protocol: the capability and receipt contract a single governed call follows.
- Receipt format: the per-call receipt that
tool_receipt_idreferences. - Metering spec: the
MonetaryAmountunits the budget envelope counts. - Guard pipelines and the Guard trait: what each step passes through before it is recorded.
- CLI reference: every
chio workflowoption.