Workflow
Bind a multi-step skill to one grant, check each step against it, and verify the signed receipt that records the trace.
On this page
A skill is an ordered sequence of tool calls that runs under one grant and ends in one signed receipt.
The skill developer declares the steps, and the data each one hands to the next, in a SkillManifest. A SkillGrant bounds which steps may run, in what order, how many times, for how much, and for how long. The host asks the WorkflowAuthority to check each step before it runs the tool through the kernel, reports the outcome and cost afterward, and receives a WorkflowReceipt that signs the whole trace.
Synopsis
A skill grant names one skill version, lists the "server_id:tool_name" steps it authorizes in declared order, and sets the envelope every execution runs inside. This grant, from section 7 of the specification, authorizes the two steps of a search-and-summarize skill for at most ten executions under a 1,000-unit USD 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: trueSkill model
Four types carry a skill from declaration to evidence. Each tool call still passes the kernel and leaves its own receipt; the workflow receipt binds those calls into one signed trace.
finalize signs the trace the host recorded
begin checks the grant against the manifest. Before each step, validate_step checks scope, ordering, and elapsed time; the host runs the tool through the kernel and passes record_step the outcome, cost, and tool receipt id. finalize signs one WorkflowReceipt whose outcome is completed. WorkflowAuthority lifecycle.
search-and-summarize skill from the specification’s example: six authority calls, two governed tool calls, and the signed workflow receipt, with the example’s step costs. Durations, the budget envelope, and vendor co-signatures are left out.- SkillGrant. Extends the capability model to ordered tool sequences, with a budget envelope and execution limits.
- SkillManifest. Declares the tool dependencies, the I/O contracts between steps, and the budget requirements.
- WorkflowReceipt. Records the execution trace in one signed receipt.
- WorkflowAuthority. Checks scope, ordering, and elapsed time before a step, then accounts for the step's reported cost and outcome afterward.
Try strict_ordering, StepOutOfOrder, vendor_signatures, or finalize. Each entry has its own link.
Workflow objects
The schema id and top-level shape of each object. The Registry column reads spec/schemas/registry.json.
| Object | Schema id | Top-level shape | Registry |
|---|---|---|---|
SkillGrant | chio. | schema, skill_id, skill_version, authorized_ | not listed |
SkillManifest | chio. | schema, skill_id, version, name, description?, steps: SkillStep[], budget_envelope?, max_ | not listed |
WorkflowReceipt | chio. | the Workflow fields, then signature and vendor_ | not listed |
Workflow | none | begin, validate_step, record_step, finalize | none |
SkillGrant
A SkillGrant authorizes an agent to execute a named skill. Where a tool grant authorizes one call, a skill grant binds the whole tool sequence under one authorization and one shared budget envelope.
Schema
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
schema | string | yes | none | MUST be "chio. |
skill_id | string | yes | none | Unique skill identifier |
skill_version | string | yes | none | Version of the skill manifest this grant authorizes |
authorized_ | 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_ | 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 when "server_id:tool_name" appears in the authorized_steps list. Invocations of tools not in the list MUST be rejected. SkillGrant::authorizes_step makes this check.
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_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_ | 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, read when retryable is true |
IoContract
An IoContract describes the data that flows between steps. Each list field deserializes as empty when absent.
| Field | Type | Description |
|---|---|---|
required_fields | string[] | Field names the step requires (inputs) or guarantees (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; validation leaves them unchecked.
- A violation reports the step index, the tool name, and the missing field name.
Tool dependencies
The manifest's tool dependencies are the "server_id:tool_name" strings derived from its steps, returned by SkillManifest::tool_dependencies. begin checks every manifest step against the grant before an execution starts.
WorkflowReceipt
A WorkflowReceipt records one 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. |
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 every field of WorkflowReceiptBody and adds a signature field, an Ed25519 signature over the 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, per signer and in this order, rejects a missing vendor signature, a public key that differs from the required one, and a signature that fails verification.
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_ | string | SHA-256 hash of the strict bilateral DSSE envelope for this step (optional) |
governance_ | string | Governance receipt id authorizing this step (optional) |
parent_ | string | SHA-256 hash of the previous step receipt in the workflow chain (optional) |
consistency_ | string | Per-step consistency anchor value (optional) |
destructive | bool | Destructive- |
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 a destructive step to 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
StepOutcome serializes as one of four snake-case values:
success: the step completed successfully.denied: policy denied the step.failed: the step failed during execution.skipped: the workflow aborted before reaching the step.
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 host drives the lifecycle: begin once, validate before each tool invocation, record each outcome, and finalize the trace. The authority checks and records. Dispatching tools, authenticating the enclosing capability token, and enforcing a step's I/O schema belong to the host's integration and the tool's kernel execution path.
The WorkflowAuthority holds the kernel signing key, counts the executions it has started, and keeps a per-capability reservation table for grants that carry max_executions. These counters are local to the authority instance and are lost when it is recreated. 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.
This check does not reserve money for the upcoming call. Budget checks run when the host records the step’s cost. For a spending ceiling that must prevent dispatch, also use the kernel’s budget admission.
record_step
record_step(execution, step, StepExecutionRecordInput {
outcome, duration_ms, cost, tool_receipt_id, output_hash
})
-> Result<(), WorkflowError>The host supplies a StepExecutionRecordInput. The authority records the reported tool receipt id and output hash without checking them against the tool receipt. 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>Finalization signs the records supplied to this authority. A Completed outcome means the recorded trace has no failure selected by the rules below. The authority keeps no copy of the manifest's step count, so to judge completeness, compare the recorded steps with the manifest and verify each linked tool receipt.
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.beginalready incremented the authority's execution count. - Return the signed
WorkflowReceipt.
WorkflowError
| Error | Fields | Description |
|---|---|---|
Unauthorized | skill_id, version | The grant names a different skill or version |
Unauthorized | step_index, server, tool | Step missing from 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 |
Budget | expected_ | A step reported cost in a currency other than the declared budget's |
Time | elapsed_secs, limit_secs | Time limit exceeded |
Execution | limit | Maximum executions reached |
InvalidState | message | Workflow is in the wrong state for the call |
SigningFailed | message | Receipt signing error |
Example
Section 7 of spec/WORKFLOW.md pairs the grant in the synopsis with this manifest for the two-step search-and-summarize skill. The second step requires the results field the first step produces, so the manifest passes I/O contract validation:
# 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: USDExecution 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 crate carries chio.skill-grant.v1, chio.skill-manifest.v1, chio.workflow-receipt.v1 as constants in the module that defines each object, outside spec/schemas/registry.json.
The registry's chio-workflow family holds the preflight plan the CLI reads and the report it prints:
| Schema id | Kind | File |
|---|---|---|
chio. | workflow_ | spec/ |
chio. | workflow_ | spec/ |
CLI command
The help text for chio workflow reads: Validate read-only workflow planning evidence before dispatch. The command plans and validates: it runs no skill, spends no budget, and signs no workflow receipt.
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[CHIO-KERNEL-OUT-OF-SCOPE-TOOL]: workflow preflight rejected the plan: child task task-child-payment requests payment.capture, which its parent's scope does not grant help: narrow task-child-payment to actions its parent grants, or widen the parent's scope in the plan code: urn:chio:error:capability:scope-exceeded docs: https://chio.computer/docs/reference/errors#error-capability-scope-exceeded
| Subcommand | Help text | Required option |
|---|---|---|
chio workflow preflight | Validate a read-only workflow preflight plan | --plan <PATH>: Path to a chio. |
The full option list is in the CLI reference.
Source
The normative text is spec/WORKFLOW.md in the Chio repository, version 1.0, dated 2026-04-14, with status Normative. 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, each entry follows the crate and names the function.