Skip to content

Repository files navigation

arazzo-cli

CI Rust License: MIT

Execute multi-step API workflows from a YAML spec — no code generation, no glue scripts.

arazzo-cli is a standalone executor for Arazzo, the OpenAPI Initiative's spec for describing sequences of API calls as declarative workflows. Define your steps, parameters, success criteria, and control flow in YAML, then run them directly from the command line or step through them in VS Code.

Why?

Testing a sequence of API calls today means writing imperative scripts, maintaining collections, or building custom test harnesses. The Arazzo spec (part of the OpenAPI ecosystem) lets you describe these sequences declaratively — but without a runtime, the spec is just documentation.

arazzo-cli makes Arazzo specs executable: validate them, run them, trace them, and debug them interactively.

Demo

asciicast

Quick Start

git clone https://github.com/strefethen/arazzo-cli.git
cd arazzo-cli
cargo run -p arazzo-cli -- validate examples/httpbin-get.arazzo.yaml
cargo run -p arazzo-cli -- run examples/httpbin-get.arazzo.yaml get-origin

Or install it:

cargo install --path ./crates/arazzo-cli --locked
arazzo-cli validate examples/httpbin-get.arazzo.yaml
arazzo-cli run examples/httpbin-get.arazzo.yaml get-origin

Features

Feature What it does
Run workflows Execute HTTP steps, resolve expressions, evaluate success criteria, route control flow
Run single steps Execute one step with automatic dependency resolution (--step)
Generate workflows Scaffold CRUD workflows from OpenAPI 3.x specs (generate)
Validate specs Parse and structurally validate Arazzo YAML before running
Parallel execution Run independent steps concurrently with DAG-based scheduling (--parallel)
Dry-run mode Resolve all requests without sending them (--dry-run)
Input validation Require, type-check, and enforce top-level property enums for workflow inputs, with strict mode (--strict-inputs)
Execution traces Write detailed trace.v1 JSON artifacts with automatic sensitive value redaction
Deterministic replay Re-execute trace artifacts offline with response injection and drift checks (replay)
Sub-workflows Call workflows from workflows with input/output passing (up to 10 levels deep)
VS Code debugger Set breakpoints, step through workflows, inspect variables, evaluate expressions
JSON output --json on every command for scripting and CI integration
Expression language $inputs, $steps, $response, XPath, JSON Pointer, interpolation
Arazzo 1.1 selectors Typed RFC 9535 JSONPath, JSON Pointer, and XPath Selector Objects across values and outputs
Success criteria Simple expressions, regex, XPath, and RFC 9535 JSONPath criterion types
Control flow onSuccess/onFailure actions with goto, retry (with backoff), and end
Multiple API sources Route steps to different APIs via sourceDescriptions
SOAP support Execute SOAP workflows with XPath-based success criteria
Rate limiting Built-in token-bucket rate limiter (10 req/sec default, configurable burst)
Reusable components $ref to shared parameters, inputs, and action handlers via components
MCP server Expose workflows as tools for AI agents via Model Context Protocol (serve), plus authoring tools for OpenAPI inspection and workflow generation

Some of the above (bare XPath outputs and the operationPath/sourceDescriptions[].url routing idiom) are arazzo-cli extensions, not part of the Arazzo specification — see Specification Conformance: Extensions and Gaps.

Contents

CLI Commands

arazzo-cli run <spec> <workflow-id>        Execute a workflow
arazzo-cli replay <trace.json>             Replay a trace.v1 artifact with injected responses
arazzo-cli validate <spec>                 Parse and validate a spec
arazzo-cli list <spec>                     List workflows in a spec
arazzo-cli steps <spec> <workflow-id>      List steps within a workflow
arazzo-cli catalog <dir>                   Discover specs across a directory tree
arazzo-cli show <workflow-id> --dir <dir>  Display workflow details (inputs, outputs, steps)
arazzo-cli generate --spec <openapi>       Generate Arazzo workflows from an OpenAPI spec
arazzo-cli schema [command]                Print JSON Schema for a command's --json output
arazzo-cli serve [specs...] [--dir <dir>]  Start an MCP server for AI agent integration

Global flags:

  • --json — structured JSON output (all commands)
  • --verbose — step-by-step execution details

run flags:

  • --input key=value — workflow input (repeatable)
  • --input-json key=<json> — JSON-typed input (repeatable)
  • --header Name=value — HTTP header applied to all requests (repeatable)
  • --step <step-id> — execute a single step (auto-resolves upstream dependencies)
  • --no-deps — skip dependency resolution when using --step (isolated execution)
  • --strict-inputs — make input validation errors fatal (missing required fields, type mismatches, property enum assertions)
  • --http-timeout <duration> — per-request timeout (default 30s)
  • --execution-timeout <duration> — overall workflow deadline (default 5m)
  • --max-response-size <bytes> — response body size limit (default 10485760 = 10 MiB)
  • --parallel — execute independent steps concurrently
  • --dry-run — resolve requests without sending
  • --openapi <path> — operationId source spec (repeatable)
  • --expr-diagnostics <off|warn|error> — expression warning level (default off)
  • --trace <path> — write a trace.v1 execution artifact
  • --trace-max-body-bytes <n> — max body size in trace (default 2048)

replay flags:

  • <trace.json> — trace.v1 file to replay
  • --spec <path> — override run.specPath recorded in the trace
  • --workflow-id <id> — override run.workflowId recorded in the trace
  • --execution-timeout <duration> — replay timeout (default 5m)
  • --openapi <path> — operationId source spec (repeatable)

generate flags:

  • --spec <path> — path to an OpenAPI 3.x spec (YAML or JSON; required)
  • --scenario <name> — generation scenario (default crud)
  • -o/--output <path> — write generated YAML to file instead of stdout

serve flags:

  • [specs...] — Arazzo spec files to load (positional, repeatable)
  • --dir <path> — directory containing .arazzo.yaml files to discover and load

Examples

Runnable specs live in examples/. See examples/README.md for the catalog: what each spec demonstrates, its workflow IDs, and a dry-run command per scenario.

Try them:

# Validate
arazzo-cli validate examples/httpbin-auth.arazzo.yaml

# Run with inputs
arazzo-cli run examples/httpbin-get.arazzo.yaml status-check --input code=200

# Dry-run (no network calls)
arazzo-cli run examples/httpbin-get.arazzo.yaml status-check --dry-run --input code=429

# Verbose output with step details
arazzo-cli run examples/httpbin-parallel.arazzo.yaml independent-steps --parallel --verbose

# Run a single step (auto-resolves its dependencies)
arazzo-cli run examples/httpbin-data-flow.arazzo.yaml chained-outputs --step echo-origin

# Generate CRUD workflows from an OpenAPI spec
arazzo-cli generate --spec petstore.yaml -o petstore-crud.arazzo.yaml

# Write a trace file
arazzo-cli run examples/httpbin-get.arazzo.yaml status-check --input code=429 --trace ./trace.json

# Replay a trace offline
arazzo-cli replay ./trace.json

Execution Traces

run --trace <path> writes a trace.v1 JSON artifact capturing every step's request, response, criteria evaluation, and routing decision.

Sensitive values are automatically redacted:

  • Headers: Authorization, Cookie, X-API-Key
  • URL query params: token, password, session
  • JSON body fields with sensitive keys
{
  "schemaVersion": "trace.v1",
  "tool": { "name": "arazzo", "version": "0.1.0" },
  "run": {
    "workflowId": "status-check",
    "status": "success",
    "durationMs": 12
  },
  "steps": [
    {
      "seq": 1,
      "workflowId": "status-check",
      "stepId": "check-status",
      "decision": { "path": "next" }
    }
  ]
}

Schema reference: docs/trace-schema-v1.md | docs/schemas/trace-v1.schema.json

Deterministic Replay

The replay command re-executes a workflow using the recorded responses from a trace file instead of making live HTTP requests. This enables offline testing, regression detection, and CI validation without network access.

# Record a trace
arazzo-cli run spec.yaml my-workflow --trace ./trace.json

# Replay it later — no network calls
arazzo-cli replay ./trace.json

How It Works

Replay operates at the HTTP transport layer. The engine resolves expressions, evaluates criteria, and routes control flow exactly as it would during a live run — but instead of sending HTTP requests, it serves the recorded responses from the trace file.

Before injecting each recorded response, the engine compares the replayed request against the original:

Field Checked? What a mismatch means
HTTP method Yes Step target changed
URL Yes Parameters or base URL changed
Headers Yes Authentication or header logic changed
Request body Yes (JSON-aware) Payload construction changed

If any field differs, the engine reports a RUNTIME_REPLAY_REQUEST_MISMATCH error with the specific drift. This catches regressions where spec or expression changes silently alter what gets sent to the API.

Use Cases

  • CI regression tests — record a golden trace, replay it on every push to verify that expression resolution and request construction haven't changed
  • Offline development — work on workflow logic without access to the target API
  • Spec refactoring — rename steps, restructure parameters, and verify the same requests are produced
  • Review artifacts — trace files are plain JSON, easy to diff and inspect in code review

Overrides

Replay supports overriding the spec path and workflow ID stored in the trace:

arazzo-cli replay ./trace.json --spec ./updated-spec.yaml --workflow-id new-workflow

This is useful when replaying a trace against a modified spec to detect drift.

VS Code Debugger

The project includes a full Debug Adapter Protocol (DAP) implementation with a VS Code extension for interactive workflow debugging.

VS Code Debugger

Capabilities: breakpoints on steps/criteria/actions, conditional breakpoints, Step Over / Step In / Step Out / Continue / Pause, variable inspection (Locals, Request, Response, Inputs, Steps scopes), watch expressions, call stack with sub-workflow depth tracking.

Setup

  1. Build the debug adapter and extension:
    cargo build --release -p arazzo-debug-adapter
    cd vscode-arazzo-debug && npm install && npm run build && node scripts/copy-binary.js
  2. In VS Code, press F5 to launch the Extension Development Host
  3. Create .vscode/launch.json in the new window:
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "arazzo",
      "request": "launch",
      "name": "Debug Workflow",
      "spec": "${workspaceFolder}/examples/httpbin-get.arazzo.yaml",
      "workflowId": "get-origin",
      "stopOnEntry": true
    }
  ]
}
  1. Open the Arazzo YAML file, set breakpoints on step lines, and press F5

Launch Configuration

Property Type Required Description
spec string yes Path to the .arazzo.yaml workflow spec
workflowId string yes Workflow to execute
inputs object no Key-value map passed as workflow inputs
stopOnEntry boolean no Pause before the first step (default: false)
runtimeExecutable string no Command to launch the debug adapter (default: cargo)
runtimeArgs string[] no Arguments for adapter launch
runtimeCwd string no Working directory for adapter launch

Breakpoints

Set breakpoints on any meaningful line in a YAML spec. The debugger maps source lines to internal checkpoints:

  • Step lines (- stepId: fetch-data) — pause before execution
  • Success criteria (- condition: $statusCode == 200) — pause at evaluation
  • onSuccess / onFailure actions — pause at dispatch
  • Output lines (title: //item[1]/title) — pause at extraction

Conditional breakpoints are supported — right-click a breakpoint and add an expression:

$statusCode == 429
$steps.fetch-auth.outputs.token != null

Stepping

Control Behavior
Continue (F5) Run to next breakpoint or end
Step Over (F10) Next checkpoint at current workflow depth
Step In (F11) Descend into sub-workflow calls
Step Out (Shift+F11) Run until returning to parent workflow
Pause (F6) Pause at next checkpoint

Variable Inspection

When paused, the Variables panel shows:

  • LocalsworkflowId, stepId, checkpoint
  • Requestmethod, url, headers, body
  • ResponsestatusCode, contentType, headers, bodyPreview
  • Inputs — workflow input parameters
  • Steps — completed step outputs as a nested tree

Watch Expressions

Add expressions to the Watch panel or hover in the editor:

  • $inputs.name — workflow input
  • $steps.fetch-data.outputs.token — step output
  • $statusCode — HTTP status code
  • $response.header.Content-Type — response header
  • $response.body.data.origin — JSON body path
  • //item[1]/title — XPath query

Debugger Architecture

Three-thread coordinator design prevents deadlocks during slow HTTP requests:

stdin ──> [Reader Thread] ──cmd_tx──> [Coordinator] ──> stdout
                                           ^
          [Engine Monitor] ──event_tx──────┘
                |
          [Runtime Engine]

Neither channel blocks the other. A slow HTTP request does not prevent processing VS Code commands (pause, disconnect, etc.).

Expression Language

Expression Resolves to
$inputs.name Workflow input parameter
$steps.<id>.outputs.<name> Previous step output
$outputs.name Workflow outputs map (inside workflow.outputs)
$workflows.<id>.inputs.<name> / .outputs.<name> Another workflow's inputs/outputs
$statusCode HTTP response status code
$method HTTP method (GET, POST, etc.)
$url Fully constructed request URL
$self The current Arazzo Description's $self URI (null, with a warning, when the document declares no $self)
$response.header.Name Response header (case-insensitive)
$response.body.path JSON dot-path body extraction
$response.body#/pointer RFC 6901 JSON Pointer body access
$request.header.Name Request header
$request.query.Name Request query parameter
$request.path.Name Request path parameter
$request.body Request body (dot-path or JSON Pointer)
$sourceDescriptions.<name>.url Source description URL
reference: $components.parameters.<name> on a Parameter Named parameter component, via a Reusable Object's reference field
reference: $components.successActions|failureActions.<name> on an action Resolves the named action component (the specification's Reusable Object form); value is ignored for actions
name: $components.successActions|failureActions.<name> on an action (arazzo-cli extension) Resolves the named action component; retained for compatibility and overridden by reference when both are present
//xpath/expression (arazzo-cli extension, retained legacy form) XML/HTML extraction — prefer the Selector Object form (type: {type: xpath, version: xpath-10}) for new workflows

Not implemented:

  • $response.query.<name> and $response.path.<name> are listed by the specification but not resolved by arazzo-cli — they evaluate to null. Only $response.header.<name> and $response.body... are supported on $response.
  • $message.header.<name> and $message.payload... are modeled in the expression evaluator but nothing in the runtime populates them for a real request — arazzo-cli does not execute asynchronous/message-style transports. Against an actual HTTP response, both evaluate to null, not the response's own header/body (use $response.* for that).

String interpolation: {$expr} embeds any expression in a string value (e.g., "Bearer {$steps.auth.outputs.token}")

Multi-source routing (arazzo-cli extension): {sourceName}./path — e.g. operationPath: "{petstore}./pets", or with a method prefix, operationPath: "GET {petstore}./pets" — selects a source description's base URL for a step. This, and the bare-path form used elsewhere in this README (e.g. operationPath: /protected in Sub-Workflows), are not the specification's operationPath syntax — see Specification Conformance: Extensions and Gaps.

Condition operators: ==, !=, >, <, >=, <=, &&, ||, contains, matches, in

Typed JSONPath (RFC 9535): type: jsonpath success criteria, Selector Objects, and targetSelectorType: jsonpath payload replacement targets all run on one RFC 9535 query engine. That is the full query language: child and descendant segments ($.items[0].name, $..sku), wildcards ($.items[*].id, $.*), array slices ($.items[0:2]), negative indices, unions, filter expressions ($.items[?@.price > 10 && @.active]) with existence tests and comparisons, and the standard function extensions length(), count(), value(), match() and search() — the latter two taking a literal pattern or a pattern drawn from the queried document ($.items[?search(@.description, @.tag)]).

A criterion is decided by nodelist cardinality alone: one or more selected nodes pass, zero nodes fail. A single node holding false, 0, "" or null therefore passes, and a null or undefined context fails. Selector reads normalize the same nodelist to null (zero matches, with a warning), the selected value (one match), or an array in query order (many matches, repeated occurrences kept). A replacement target applies only when it resolves to exactly one location.

JSONPath versions, limits, and what is not claimed: JSONPath Criterion and Selector expressions execute under RFC 9535 semantics when the version is omitted or rfc9535; any other declared JSONPath version and any syntactically invalid expression are rejected before evaluation (a criterion fails with an error, a selector resolves null with one warning). A rejected replacement target or value leaves the body unchanged with a warning. In particular draft-goessner-dispatch-jsonpath-00 is a permanent capability limit: the draft has expired, arazzo-cli will not implement it, and there is no compatibility engine, silent alias, or fallback. Unlike the XPath version rejection above, validate carries no JSONPath version advisory — a Goessner declaration validates as document metadata (§5.8.12.1 allows the token) and is rejected only at run time.

Three conservative admission budgets are applied before the parser or the evaluator runs: at most 16,384 UTF-8 bytes per query, at most 128 combined occurrences of the raw bytes . [ ( ! & | in a query (counted in quoted and escaped literals too), and at most 128 nested containers in the queried context. Exceeding one names the resource and the limit. match() and search() delegate to an I-Regexp matcher and inherit its own pattern, repetition, nesting, and compiled-matcher limits; an invalid pattern yields logical false, while a resource or backend failure invalidates the whole query — even under negation — rather than quietly reading as false. These are explicit resource budgets, not a universal CPU, heap, or result-size quota for every query, and arazzo-cli does not claim 100% RFC 9535 Compliance Test Suite conformance. The engine is serde_json_path 0.7.2 with one eight-line recursive numeric-equality repair vendored under vendor/serde_json_path_core, which records the provenance, the checksum, and the criterion for dropping the patch.

This typed surface is distinct from the legacy dot-path traversal that $response.body... runtime expressions use — see JSONPath under Success Criteria.

Arazzo 1.1 Selector Objects

Step/workflow outputs, parameter values, request-body values, and replacement values accept structured Selector Objects in addition to existing literal and runtime-expression forms:

outputs:
  enabledIds:
    context: $response.body
    selector: $.items[?(@.enabled == true)].id
    type:
      type: jsonpath
      version: rfc9535

type may be the string jsonpath, jsonpointer, or xpath, or an object with an explicit version. Supported schema combinations are JSONPath rfc9535 / draft-goessner-dispatch-jsonpath-00, JSON Pointer rfc6901, and XPath xpath-10 / xpath-20 / xpath-30 / xpath-31. What a document may declare and what this runtime executes are different questions, and both version families answer it the same way: an unexecutable version validates as document metadata and is rejected before evaluation.

Runtime JSONPath execution accepts an omitted version or version: rfc9535; draft-goessner-dispatch-jsonpath-00 is rejected permanently — see Typed JSONPath (RFC 9535). Runtime XPath execution requires an explicit version: xpath-10: every other declared version — and the bare type: xpath string form, whose omitted version the specification defaults to xpath-31 — is rejected the same way. A rejected criterion fails its step with an error; a rejected selector resolves to null with one warning; a rejected replacement leaves the body unchanged with one warning. For XPath, validate flags each such declaration ahead of execution with a warning naming the rejected version and the xpath-10 remedy, and --strict promotes it to an error; there is no equivalent JSONPath version advisory, so a Goessner declaration passes validate cleanly and is rejected only at run time.

All selector callers use the same selection engine — RFC 9535 for jsonpath. Zero matches resolve to null, one match resolves to the value, and multiple matches resolve to an array in query/document order. Invalid syntax, unsupported runtime versions, admission-limit rejections, and zero matches produce trace or dry-run warnings; only the last of those is a legitimate selection, and a JSONPath failure is never written into a request body as null. A mapping is treated as a Selector Object only when it satisfies the complete context + selector + type contract, so ordinary literal mappings retain their existing recursive expression behavior.

For XPath outputs specifically, prefer the Selector Object form with an explicit version (type: {type: xpath, version: xpath-10}) over the bare //xpath/expression value in the table above — the Selector Object is the specification-conformant 1.1 form; the bare form is a retained arazzo-cli extension kept for compatibility with older workflows.

Specification Conformance: Extensions and Gaps

arazzo-cli implements the Arazzo Specification v1.1.0, plus a small set of constructs the specification does not define. Policy: extensions are permitted, must be labeled arazzo-cli extension everywhere they are documented, and must never silently change the meaning of an otherwise-conformant document. A workflow that uses a labeled construct runs correctly here but is not guaranteed to validate or run on another Arazzo tool.

Extensions beyond the specification:

Construct Specification-conformant alternative
Bare XPath output, e.g. outputs: { title: //item[1]/title } Selector Object with type: {type: xpath, version: xpath-10}
operationPath as "{sourceName}.<path>", a bare path, or a "METHOD "-prefixed form, e.g. "GET {petstore}./pets" None implemented yet — the specification's form is listed under "Not implemented" below
sourceDescriptions[].url read as an absolute request base URL Identity-based referencing: provide the document the url names and the reference binds to it, resolving requests against that document's own servers. An absolute url binds to a provided document whose $self matches it; a relative url binds to the file it resolves to. The base-URL reading applies only when nothing provided answers to the url. See "Not implemented" below
name: $components.successActions|failureActions.<name> resolving a Success/Failure Action Object to its named component reference: $components.successActions|failureActions.<name> is the specification's Reusable Object form; name is retained as an arazzo-cli extension for compatibility

The operationPath idiom and the sourceDescriptions[].url meaning are the same open decision: this tool's form ("[METHOD ]{source}.<path>" plus url-as-base-URL) is internally consistent and is what every example in this repository uses, but it is not the specification's form. The specification's form — a Runtime Expression pointing at a Source Description Object plus a JSON Pointer to an operation, e.g. {$sourceDescriptions.petstore.url}#/paths/~1pets/get — is not resolved by this runtime; using it now produces a validate warning and a clear run error rather than a silently wrong URL. generate was updated in ac-91284 to emit a document-pointing, relative sourceDescriptions[].url for newly generated workflows. An absolute url now resolves by document identity first — a provided document whose identity matches it wins — and the extension reading of that url as a base URL applies only when no provided document answers to it. Tracked in the conformance audit's Recommendation section.

Specification features not implemented:

  • $response.query.<name> and $response.path.<name> — listed by the specification's Runtime Expressions grammar; arazzo-cli resolves only $response.header.<name> and $response.body..., so both evaluate to null rather than the request-matched query/path value.

  • $message.header.<name> and $message.payload... — modeled in the expression evaluator, but no code path in the runtime populates a message context for a real request (arazzo-cli does not execute asynchronous/message-style transports), so both evaluate to null against a real response rather than erroring or falling back to $response.*.

  • The specification's operationPath form (source reference + JSON Pointer), described above.

  • Fetching a remote document from sourceDescriptions[].url over the network. This is a deliberate decision, not a gap: arazzo-cli never makes a network request to a url value except as the extension base-URL reading described above. The specification's own answer for a url pointing at a hosted document is identity-based referencing (§9.6) — provide the document and the reference binds to it without a request. Vendor a local copy, give it a $self matching the url, and hand it over:

    { "$self": "https://example.com/v1/openapi/spec.json", "openapi": "3.1.0", "...": "..." }
    arazzo-cli run spec.arazzo.yaml my-workflow --openapi ./vendored.openapi.json

    The $self is what binds it, and the request then resolves against that document's own servers base — the conformant result. --openapi alone is not enough for an absolute url: a provided document with no $self answers only to the path it was read from, which is what binds a relative url. A url nothing provided answers to falls back to the base-URL reading in the table above.

How It Works

arazzo-cli is a pure interpreter — it reads your Arazzo YAML spec, resolves every expression at runtime, executes real HTTP requests, and routes control flow based on the results. There is no code generation step and no intermediate representation.

Execution Pipeline

YAML spec ──> Parse & Validate ──> Build Engine ──> Execute Steps ──> Collect Results
                                       │
                           ┌───────────┼───────────────┐
                           │           │               │
                      Resolve      Send HTTP      Evaluate
                    expressions    request(s)     criteria
                           │           │               │
                           └───────────┼───────────────┘
                                       │
                                 Route control flow
                              (next / goto / retry / end)

For each step, the engine:

  1. Resolves parameters and request body — evaluates literals, runtime expressions, interpolation, and typed Selector Objects through one value-selection path
  2. Sends the HTTP request — through a rate-limited client with configurable timeout
  3. Evaluates success criteria — checks conditions like $statusCode == 200, regex patterns, XPath queries, or JSONPath filters against the response
  4. Extracts outputs — pulls values through runtime expressions or typed JSONPath/JSON Pointer/XPath selectors into the step's output map for downstream steps to consume
  5. Routes control flow — matches onSuccess or onFailure action criteria to decide: advance to the next step, jump to another step (goto), retry with a delay, or end the workflow

The engine streams events as it runs. CLI output, traces, verbose logging, and the VS Code debugger all consume the same event stream — there is no special path for any consumer.

.env File Support

On startup, arazzo-cli loads a .env file from the current directory (if one exists) into the process environment. Values from the file overwrite variables that already exist in the environment, so treat .env as authoritative for every name it defines.

The $env.VAR_NAME expression namespace that used to expose those values inside workflow text was removed in 0.4.0. It was an arazzo-cli extension the Arazzo specification does not define, and it handed workflow text read access to the entire process environment. $env.* now resolves like any other unknown namespace — null, with a warning — and the variable's value never appears in diagnostics. To get a secret or environment-specific value into a workflow, declare a workflow input and pass it at invocation:

# In your Arazzo spec
parameters:
  - name: Authorization
    in: header
    value: "Bearer {$inputs.apiKey}"
arazzo-cli run workflow.arazzo.yaml my-workflow -i apiKey="$API_KEY"

Inputs keep secrets out of spec files, are declared per workflow rather than ambient, and are portable to other Arazzo tools.

Parallel Execution

When you pass --parallel, the engine analyzes step dependencies and executes independent steps concurrently.

How the DAG Scheduler Works

The scheduler uses a variant of Kahn's algorithm for topological sorting:

  1. Scan expressions — for each step, find all $steps.<id>.outputs.* references to identify upstream dependencies
  2. Build a directed acyclic graph (DAG) — if step B references $steps.A.outputs.token, add an edge A → B
  3. Compute execution levels — Level 0 contains all steps with no dependencies. Level N+1 contains steps whose dependencies are all satisfied by levels 0..N
  4. Execute level by level — within each level, steps run concurrently via async tasks. The engine waits for all steps in a level to complete before starting the next level
  5. Detect cycles — if any steps remain with unresolvable dependencies, the engine reports a RUNTIME_DEPENDENCY_CYCLE error
Level 0:  [fetch-token]  [fetch-config]     ← run concurrently
              │                │
Level 1:  [create-user] ──────┘              ← waits for both
              │
Level 2:  [verify-user]                     ← sequential

Determinism guarantee: even with concurrent execution, event sequence numbers are assigned per-level in stable step order, so identical inputs always produce identical traces.

Parallel mode is automatically disabled if any step uses onSuccess/onFailure actions or calls a sub-workflow, since these require sequential control flow.

Success Criteria

Each step can define success criteria — conditions that must pass for the step to be considered successful. The engine supports four criterion types:

Simple (default)

Boolean expressions evaluated against the runtime context:

successCriteria:
  - condition: $statusCode == 200
  - condition: $response.body.status == "active"
  - condition: $response.body.items.length > 0

Supports all comparison and logical operators: ==, !=, >, <, >=, <=, &&, ||, contains, matches, in.

Regex

Pattern matching against a context value:

successCriteria:
  - condition: "^2[0-9]{2}$"
    context: $statusCode
    type: regex

Compiled regexes are cached for performance — repeated evaluation of the same pattern across steps or retries avoids recompilation (100–500x speedup).

XPath

XPath 1.0 queries for XML/SOAP responses:

successCriteria:
  - condition: //customer/id
    context: $response.body
    type:
      type: xpath
      version: xpath-10

XPath criteria, Selector Objects, and payload replacements evaluate only with an explicit version: xpath-10. Every other §5.8.12.1 version token — and the omitted form, which the specification defaults to xpath-31 — is rejected before evaluation, because this runtime implements XPath 1.0 and nothing else: a criterion fails with an error, a selector yields null with one warning, and a replacement leaves the body unchanged with one warning.

Unprefixed XPath name tests match on local names, so //customer/id matches <ns:customer><ns:id> without namespace qualification — the response body is never rewritten. Prefixed expressions (//ns:customer) resolve against the document's root-scope namespace declarations, so two prefixes bound to different URIs are distinguishable; a prefix not declared on the document element falls back to matching the prefix text literally. A document that uses an entirely undeclared prefix is rejected as invalid XML.

JSONPath

type: jsonpath criteria are RFC 9535 queries decided by nodelist cardinality — one or more selected nodes pass, zero fail:

successCriteria:
  - condition: $.users[?search(@.email, @.domainPattern)].name
    context: $response.body
    type:
      type: jsonpath
      version: rfc9535

The grammar, the version rule, the admission budgets, and the regex-function behavior are documented once under Typed JSONPath (RFC 9535); the same engine serves Selector Objects and payload replacement targets.

Legacy dot-path traversal (arazzo-cli extension)

A criterion with no type is a simple criterion, and its $response.body... expression uses the runtime-expression dot-path traversal instead — a separate, older code path that is not RFC 9535 and is not going to become it:

successCriteria:
  - condition: $response.body.users[?(@.role=="admin")].name
    context: $response.body

Beyond the specification's plain . de-reference, that traversal accepts array indexing ([0]), wildcards ([*]), array length (.#), a JSONPath-style bracket filter predicate ([?(@.field=="value")]), and a GJSON-style dot-form filter predicate (.#(field==value), or .#(field==value)# to keep all matches instead of the first) — all of it an arazzo-cli extension (see Specification Conformance: Extensions and Gaps). The bracket-wrapped GJSON form [#(field==value)] is not supported — it is parsed as a literal (and normally nonexistent) field name, so it silently resolves to null instead of erroring or matching; use [?(@.field=="value")] or .#(field==value) instead. Verified by running each form through arazzo-cli run --json against a local test server and comparing outputs.

The two surfaces do not share syntax. Every GJSON form is rejected on a typed JSONPath criterion, Selector Object, or replacement target with an invalid JSONPath syntax diagnostic; this extension applies only to runtime-expression dot-path traversal. New workflows should prefer the typed form.

Control Flow

After evaluating a step's success criteria, the engine routes control flow through onSuccess and onFailure action lists. Each action has optional guard criteria — the first action whose criteria all pass is selected.

Action Types

Type Behavior
end Terminate the workflow (success or failure depending on branch)
goto Jump to another step by stepId, or invoke a sub-workflow by workflowId
retry Re-execute the current step after retryAfter seconds (up to retryLimit)

Example: Retry with Fallback

onFailure:
  - name: retry-on-429
    type: retry
    retryAfter: 2
    retryLimit: 3
    criteria:
      - condition: $statusCode == 429
  - name: fail-hard
    type: end

This retries on 429 Too Many Requests up to 3 times with a 2-second delay, then fails on any other error. An omitted retryLimit performs one retry; explicit limits are exact, and bounded execution uses the declared effective retry budgets with 10 levels of sub-workflow nesting to prevent runaway execution.

Goto with Criteria Guards

onSuccess:
  - name: handle-created
    type: goto
    stepId: verify-created
    criteria:
      - condition: $statusCode == 201
  - name: handle-existing
    type: goto
    stepId: update-existing
    criteria:
      - condition: $statusCode == 200

Actions are evaluated in order — the first match wins. An action with no criteria acts as a catch-all.

Input Validation

Workflow inputs are validated before execution begins:

  1. Default injection — missing inputs are populated from schema defaults
  2. Required check — required inputs that are missing or null produce an error
  3. Type check — values are validated against their declared JSON Schema type (string, integer, boolean, number)
  4. Top-level property enum check — present values, including injected defaults, must match a declared top-level property enum member
  5. Undeclared input warning — inputs not defined in the workflow schema are flagged

By default, validation issues are reported as warnings and execution continues. With --strict-inputs, all validation errors are fatal:

# Warns about missing 'name' input but continues
arazzo-cli run spec.yaml my-workflow

# Fails immediately if 'name' is missing
arazzo-cli run spec.yaml my-workflow --strict-inputs

Sub-Workflows

A step can invoke another workflow defined in the same spec by setting workflowId instead of an HTTP operation:

workflows:
  - workflowId: parent
    steps:
      - stepId: authenticate
        workflowId: auth-flow
        parameters:
          - name: username
            value: $inputs.user
      - stepId: use-token
        operationPath: GET /protected
        parameters:
          - name: Authorization
            in: header
            value: "Bearer {$steps.authenticate.outputs.token}"

The child workflow executes with its own input context, and its outputs are available to the parent as $steps.<stepId>.outputs.<name>. Sub-workflows can nest up to 10 levels deep. Circular calls are prevented by depth tracking.

Generating Workflows

The generate command scaffolds Arazzo workflows from existing OpenAPI 3.x specs:

arazzo-cli generate --spec petstore.yaml -o petstore-crud.arazzo.yaml

The crud scenario (currently the only supported scenario) analyzes your OpenAPI spec and produces:

  • A workflow per resource with Create → Read → Update → Delete steps
  • Chained outputs (the id from Create feeds into subsequent steps)
  • Realistic request bodies derived from schema examples and property types
  • Authentication detection (bearer, basic, API key) applied to all steps
  • Source descriptions pointing back to the original OpenAPI spec

This gives you a runnable starting point that you can customize — add assertions, error handling, conditional logic, or compose into larger workflows.

MCP Server

arazzo-cli includes a built-in Model Context Protocol (MCP) server that exposes Arazzo workflows as tools for AI agents. Any MCP-compatible client — Claude Desktop, Cursor, or custom agents — can discover and execute your workflows.

Starting the server

# Standalone binary
arazzo-mcp examples/httpbin-get.arazzo.yaml

# Or via the CLI subcommand
arazzo-cli serve examples/httpbin-get.arazzo.yaml

# Load all specs from a directory
arazzo-cli serve --dir examples/

The server communicates over stdio using Content-Length framed JSON-RPC 2.0 (the same transport as LSP and DAP). Newline-delimited JSON framing is also supported for simpler integrations.

Security

By default, file-accepting tools (validate_spec, generate_workflow, describe_openapi) can read any path the process has access to. Use --allowed-dir to restrict file access:

arazzo-cli serve --allowed-dir /home/user/specs examples/*.arazzo.yaml

The flag is repeatable — pass it multiple times to allow several directories.

Available tools

Workflow execution

Tool Description
list_workflows Discover all workflows across loaded specs (IDs, summaries, inputs, outputs)
describe_workflow Full input schema, output names, step summaries, and source descriptions for a workflow
run_workflow Execute a workflow with inputs and return structured outputs. Supports dry_run, parallel, and timeout options.
validate_spec Validate an Arazzo spec YAML file and return any errors

Authoring assistance

Tool Description
describe_openapi Inspect an OpenAPI spec — returns endpoints, schemas, and auth schemes
generate_workflow Generate Arazzo CRUD workflows from an OpenAPI spec with chained steps, auth, and realistic request bodies
generate_example Generate a realistic example value from a JSON Schema using name/format/enum heuristics

Claude Desktop configuration

Add to your claude_desktop_config.json:

{
  "mcpServers": {
    "arazzo": {
      "command": "arazzo-mcp",
      "args": ["path/to/your/workflows.arazzo.yaml"]
    }
  }
}

Example: executing a workflow

An AI agent discovers and executes a workflow:

  1. Agent calls list_workflows → sees get-origin workflow
  2. Agent calls describe_workflow(workflow_id: "get-origin") → learns it takes no inputs, outputs origin and url
  3. Agent calls run_workflow(workflow_id: "get-origin") → gets {"kind":"success","outputs":{"origin":"1.2.3.4","url":"https://httpbin.org/get"}}

The agent never constructs raw HTTP requests — all multi-step orchestration, expression evaluation, retry logic, and error handling is managed by the Arazzo spec.

Example: generating a workflow from an OpenAPI spec

An AI agent creates a new Arazzo workflow from scratch:

  1. Agent calls describe_openapi(file_path: "petstore.openapi.yaml") → sees 5 endpoints, Pet/PetInput schemas, ApiKeyAuth
  2. Agent calls generate_workflow(file_path: "petstore.openapi.yaml") → gets a complete Arazzo YAML with chained CRUD steps and realistic payloads
  3. Agent calls generate_example(schema: {"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"}}}) → gets {"name":"Jane Doe","email":"user@example.com"}

Safety and Correctness

arazzo-cli is designed to be reliable and predictable:

  • No unsafe code#![forbid(unsafe_code)] is enforced across the entire workspace. All concurrency uses safe abstractions (Arc, Mutex, tokio::sync, CancellationToken)
  • No .unwrap() or .expect() — Clippy's unwrap_used lint is set to deny. All error paths are handled explicitly
  • Deterministic traces — identical inputs always produce identical event sequences, even under parallel execution. Sequence numbers are assigned by level and step order, not by thread timing
  • Bounded execution — declared effective retry budgets, 10 sub-workflow nesting levels, configurable per-request and overall timeouts, and a 10 MiB default response-body cap prevent runaway workflows
  • Automatic redaction — trace files redact 18 sensitive key patterns (authorization, token, password, secret, cookie, api-key, etc.) by default
  • Rate limiting — a built-in token-bucket rate limiter (10 requests/sec, burst of 20) prevents accidental API abuse

Programmatic API

The arazzo-runtime crate exposes a Rust API for embedding the engine in other tools. The engine uses an async streaming architecture — execution produces a channel of events that consumers can process in real-time.

EngineBuilder

use arazzo_runtime::{EngineBuilder, EngineEvent};

let engine = EngineBuilder::new(spec)
    .client_config(config)          // Custom HTTP settings
    .parallel(true)                 // DAG-based parallel execution
    .dry_run(false)                 // Actually send requests
    .trace(true)                    // Record trace events
    .strict_inputs(true)            // Fatal input validation
    .max_response_bytes(5_000_000)  // 5 MiB response limit
    .observer(my_observer)          // Rich event callbacks
    .build()?;

let handle = engine.execute("my-workflow", inputs);

// Collect all events and await the final result
let result = handle.collect().await;
for event in &result.events {
    match event {
        EngineEvent::Observer(obs) => { /* fine-grained lifecycle events */ }
        EngineEvent::TraceStep(step) => { /* per-step trace records */ }
        _ => {}
    }
}
let outputs = result.outputs?; // BTreeMap<String, Value>

ExecutionObserver

Implement the ExecutionObserver trait to receive fine-grained lifecycle events without polling:

use arazzo_runtime::{ExecutionObserver, ObserverEvent};

struct MyObserver;

impl ExecutionObserver for MyObserver {
    fn on_event(&self, event: &ObserverEvent) {
        match event {
            ObserverEvent::StepStarted { workflow_id, step_id, .. } => { }
            ObserverEvent::RequestPrepared { method, url, .. } => { }
            ObserverEvent::CriterionEvaluated { condition, passed, .. } => { }
            ObserverEvent::StepCompleted { duration, outputs, .. } => { }
            ObserverEvent::WorkflowCompleted { duration, outputs, .. } => { }
            _ => {}
        }
    }
}

Observer events include: StepStarted, RequestPrepared, RequestSent, CriterionEvaluated, RetryScheduled, StepCompleted, SubWorkflowStarted, and WorkflowCompleted.

The internal API types (EngineEvent, ExecutionHandle, RuntimeError, TraceStepRecord, etc.) are versioned as api_v1 with a documented stability contract — backward-compatible additions are allowed, but type shape changes require a version bump.

Repository Layout

crates/
  arazzo-spec              Arazzo domain model types
  arazzo-validate          YAML parser + structural validation
  arazzo-expr              Expression parser/evaluator
  arazzo-runtime           Execution engine + debug controller
  arazzo-cli               CLI binary
  arazzo-mcp               MCP server (Model Context Protocol) for AI agents
  arazzo-debug-adapter     DAP server (Debug Adapter Protocol)
vscode-arazzo-debug/       VS Code debugger extension (TypeScript)
examples/                  Runnable workflow specs (see examples/README.md)
testdata/                  Test fixtures
docs/schemas/              JSON Schemas for --json output formats

Building from Source

Prerequisites: rustup. The repository pins Rust 1.98.1 for development and release builds via rust-toolchain.toml; the minimum supported Rust version (MSRV) for source builds remains 1.88.

git clone https://github.com/strefethen/arazzo-cli.git
cd arazzo-cli
cargo build --workspace --locked
cargo test --workspace --locked

Quality gates (run by CI on every push):

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --locked

CI also runs cargo audit, locked MSRV verification (Rust 1.88), and locked Rust 1.98.1 cross-platform builds (Linux, macOS, Windows).

Rust 1.88 is a supported compatibility boundary, not the compiler used to cut releases. Any future MSRV increase should be intentional, tested, and called out in the release notes.

Contributing

Issues, bug reports, and feature requests are welcome.

This project accepts PRs to demonstrate a fix or approach, though the maintainer may independently implement changes after review. See CONTRIBUTING.md for details.

Acknowledgments

Built with these open-source crates:

License

MIT

About

Standalone Arazzo workflow executor, CI contract tester, MCP server, and VS Code debugger

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages