You have an OpenAPI description, and somewhere a script that calls a few of its operations in the right order. The script works until the API changes. Then it breaks quietly, and nobody notices until something downstream does.

An Arazzo document describes the same sequence as data: which operations to call, in what order, and what to pass from one to the next. A document can be checked before it runs. UseArazzo is the toolkit that checks it and runs it.

This guide follows one small workflow through the whole toolkit. Here is where it ends, a workflow run against a live API with every step accounted for:

✓ findPets   1389 ms
✓ getPet      357 ms

completed in 1750 ms
{ petId: 99, name: 'fido' }

By the end you will have:

  • seen two mistakes reported with their exact line and column, and fixed them,
  • a validate command that fails a CI job when the workflow regresses,
  • a script that runs the workflow against a live API and prints what happened at each step,
  • the same script refusing to run a workflow that does not validate.

Step 1: Get the sample workflow

Make an empty directory and save this file in it as find-a-pet.arazzo.yaml, or download it.

arazzo: 1.0.1
info:
  title: Find a pet
  summary: Find a pet by status, then fetch its details
  description: A two-step workflow against the Swagger Petstore.
  version: 1.0.0
sourceDescriptions:
  - name: petstore
    url: https://petstore3.swagger.io/api/v3/openapi.json
    type: openapi
workflows:
  - workflowId: findAPet
    summary: Find a pet by status, then fetch its details
    description: Lists the pets with the given status and fetches the first one.
    inputs:
      type: object
      properties:
        status:
          type: string
      required:
        - status
    steps:
      - stepId: findPets
        description: List the pets that have the given status
        operationId: findPetsByStatus
        parameters:
          - name: status
            in: query
            value: $inputs.status
        successCriteria:
          - condition: $statusCode == 200
        outputs:
          petId: $response.body#/0/id
      - stepId: getPet
        description: Fetch the first pet from the list
        operationId: getPetById
        parameters:
          - name: petId
            in: paths
            value: $steps.findPets.outputs.petId
        successCriteria:
          - condition: $statusCode == 200
        outputs:
          name: $response.body#name
    outputs:
      petId: $steps.findPets.outputs.petId
      name: $steps.getPet.outputs.name

What is happening here? The workflow has two steps. findPets asks the public Swagger Petstore for every pet with a given status and keeps the id of the first one. getPet fetches that pet and keeps its name. The Petstore’s own OpenAPI description, listed under sourceDescriptions, says what findPetsByStatus and getPetById are.

The file has two deliberate mistakes. Both are one character long. Don’t look for them yet.

Step 2: Validate it from the terminal

Let’s ask the CLI what it thinks. You need Node.js 20.10 or newer. npx fetches the CLI on first use, so there is nothing else to install:

npx @usearazzo/cli validate find-a-pet.arazzo.yaml
find-a-pet.arazzo.yaml
  39:17-39:22  error  9060201  in must be one of: 'path', 'query', 'header', 'cookie'
  44:11-44:36  error  9050804  Step output values must be valid Arazzo Runtime Expressions.

✖ 2 problems (2 errors)

Two problems, each with the line and column where it starts and ends. Lines and columns count from 1, so you can jump straight to them in your editor. The number in the third column is the rule’s code. The Validator reference lists every rule.

Because it found errors, the command exits with 1. Keep that in mind for step 4.

Notice that nothing is wrong with the YAML. Any YAML loader reads this file without a complaint. The mistakes are in the Arazzo, and that is what validate checks.

Step 3: Fix the two mistakes

Open the file at the two places the report names:

  • Line 39: in: paths should be in: path. A parameter lives in the path, the query, a header, or a cookie.
  • Line 44: $response.body#name should be $response.body#/name. What follows the # is a JSON Pointer, and a JSON Pointer starts with a slash.

Save the file and run the same command again:

npx @usearazzo/cli validate find-a-pet.arazzo.yaml
No problems found

This time the command exits with 0.

Step 4: Gate CI

The terminal catches mistakes while you write. CI catches the ones that get committed anyway.

The broken file made the command exit with 1, the fixed one with 0. That exit code is the whole CI story. If the workflow lives in a GitHub repository, save this next to it as .github/workflows/arazzo.yml:

name: Arazzo
on: [push, pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
      - name: Validate Arazzo workflows
        run: npx @usearazzo/cli validate find-a-pet.arazzo.yaml

The path in the last line is relative to the root of the repository. By default only errors fail the run. Add --fail-severity warning and warnings fail it too. The CLI reference covers every option.

Step 5: Run it against a live API

A valid document is a good start. Now let’s make the calls it describes.

The Runner is a library. It runs in Node.js and in the browser, and so does the Validator. This guide stays in Node.js. Install the Runner in the same directory:

npm install @usearazzo/runner

Save this script there as run.mjs:

import { ArazzoRunner } from '@usearazzo/runner';

const uri = 'find-a-pet.arazzo.yaml';

const runner = await ArazzoRunner.load(uri);

const result = await runner.execute('findAPet', {
  inputs: { status: 'available' },
});

for (const step of result.steps) {
  const mark = step.successful ? '✓' : '✗';
  const time = `${Math.round(step.durationMs)} ms`;
  console.log(`${mark} ${step.stepId.padEnd(10)}${time.padStart(8)}`);
}
console.log(`\n${result.status} in ${Math.round(result.durationMs)} ms`);
console.log(result.outputs);

process.exitCode = result.status === 'completed' ? 0 : 1;

load reads the document and the OpenAPI description behind it. execute runs one workflow by its workflowId, with the inputs the workflow asks for. The last line makes the script exit with 1 when the workflow does not complete. Run it:

node run.mjs
✓ findPets   1389 ms
✓ getPet      357 ms

completed in 1750 ms
{ petId: 99, name: 'fido' }

Two real API calls went to the Petstore. Each step was judged by its own successCriteria, and the id that findPets kept was handed to getPet. The last line is the workflow’s outputs.

The Petstore is a public server that anyone can write to, so your pet and your timings will differ. If a step shows ✗, or name is undefined, someone changed the data under you. Run it again, or try status: 'sold'.

Step 6: Check before you run

What happens if you run a workflow that does not validate? Put the first mistake back for a moment: change in: path on line 39 to in: paths, and run the script again:

node run.mjs

Node prints a stack trace. The line that matters is this one:

ResolverError: Parameter "petId" has location "paths", which is not one of path, query, header, cookie, querystring

The Runner stops with an error, and that is right. But look at where it stops: at the second step. The first API call had already been made. Here that was a harmless read. In a workflow that places an order first, it would not be.

So let’s check the document before a single API call goes out. The CLI’s validate command is a thin wrapper around one function, validateURI. Your code can call it directly. Install the Validator:

npm install @usearazzo/validator

Then put the check at the top of run.mjs. This is the finished script, which you can also download:

import { ArazzoRunner } from '@usearazzo/runner';
import { validateURI, DiagnosticSeverity } from '@usearazzo/validator';

const uri = 'find-a-pet.arazzo.yaml';

const diagnostics = await validateURI(uri);
const errors = diagnostics.filter((d) => d.severity === DiagnosticSeverity.Error);

if (errors.length > 0) {
  for (const { range, message } of errors) {
    console.error(`${uri}:${range.start.line + 1}:${range.start.character + 1}  ${message}`);
  }
  process.exit(1);
}

const runner = await ArazzoRunner.load(uri);

const result = await runner.execute('findAPet', {
  inputs: { status: 'available' },
});

for (const step of result.steps) {
  const mark = step.successful ? '✓' : '✗';
  const time = `${Math.round(step.durationMs)} ms`;
  console.log(`${mark} ${step.stepId.padEnd(10)}${time.padStart(8)}`);
}
console.log(`\n${result.status} in ${Math.round(result.durationMs)} ms`);
console.log(result.outputs);

process.exitCode = result.status === 'completed' ? 0 : 1;

Run it while the mistake is still in the file:

node run.mjs
find-a-pet.arazzo.yaml:39:17  in must be one of: 'path', 'query', 'header', 'cookie'

The script exits with 1 and no API call is made. Diagnostics follow the Language Server Protocol, which counts lines and characters from 0. That is why the script adds one to each.

Now change paths back to path and run it one more time. The two ticks are back.

So there it is. One file went from two errors to a validated workflow, a CI gate, and a run with a result.

Next steps