TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Arazzo 1.1: what's new, and what runs today

Share

On 19 May 2026 the OpenAPI Initiative released Arazzo 1.1.0. Arazzo is the OpenAPI Initiative's format for describing a sequence of API calls: log in, then create an order, then pay for it, with each step passing values to the next. OpenAPI says what one endpoint accepts and returns. Arazzo says which endpoints to call, in what order, and how to tell whether each call worked.

Version 1.1 is the first feature release since 1.0 in 2024. We wrote a 1.1 workflow against our own API and ran it with Redocly CLI 2.62.1, released on 9 October. The runner accepts 1.1 documents, and a plain create, read, update, delete workflow passes. Two of the new features pass the linter but are ignored when the workflow actually runs. This post covers what changed, a workflow you can download and run, and the two features to avoid for now.

What an Arazzo file does

An Arazzo document points at one or more API descriptions, usually OpenAPI files, and lists workflows. Each workflow is a list of steps. A step names an operation by its operationId, fills in parameters and a request body, says what counts as success, and saves values from the response for later steps.

That one file is useful in three places:

  • Tests.A runner executes the steps against a real server and fails if a success criterion doesn't hold. This is how we use it below.
  • Documentation.A reader sees the order of calls a task needs, which an OpenAPI file can't express.
  • AI agents. An agent that reads OpenAPI knows what each endpoint does, but has to guess how to combine them. An Arazzo workflow tells it.

What changed in Arazzo 1.1

The headline is AsyncAPI support, so a workflow can mix HTTP calls with message channels. Most of the other changes make HTTP workflows easier to write. All of them are listed in the release announcement:

ChangeWhat it does
type: asyncapi sourcesA source description can now be an AsyncAPI document, next to openapi and arazzo.
channelPath, action, correlationId, timeoutSteps that send or receive messages. action is send or receive, correlationId matches a reply to its request, and timeout is a limit in milliseconds.
dependsOnA step can list other steps that must finish before it starts. Listing a step doesn't run it.
parameters on actionsA success or failure action that calls another workflow can pass it inputs, for example to refresh a token and then retry.
Selector ObjectPulls a value out of a response or input with JSONPath, XPath or JSON Pointer. Works in outputs, parameter values and request bodies.
Expression Type ObjectPins the version of a query language, such as JSONPath as defined by RFC 9535 or XPath 3.1. Replaces the 1.0 Criterion Expression Type Object.
in: querystringSends a parameter as the entire query string, matching the same addition in OpenAPI 3.2.
$selfA URI that identifies the document, so references to it don't depend on where the file happens to be stored.

The spec also gained a full grammar for runtime expressions such as $steps.login.outputs.token, a defined order for resolving source descriptions, and exact rules for when a condition counts as true. These don't add fields, but they settle cases where two runners could disagree. Support for gRPC, GraphQL, MCP and loops is on the roadmap for later versions and isn't in 1.1.

A workflow you can run against our API

This workflow creates a product on api.testingapis.com, reads it back, changes its price, deletes it and checks that it's gone. It uses only free endpoints, so it runs without an account or an API key.

products.arazzo.yaml
arazzo: 1.1.0
$self: https://testingapis.com/downloads/arazzo/products.arazzo.yaml
info:
  title: Product lifecycle on testingapis.com
  summary: Create a product, read it back, change its price, delete it and check that it's gone.
  version: 1.0.0
sourceDescriptions:
  - name: testingapis
    url: ./testingapis-products.openapi.json
    type: openapi
workflows:
  - workflowId: product-lifecycle
    inputs:
      type: object
      properties:
        title: { type: string, default: Arazzo test product }
        price: { type: number, default: 12.5 }
        newPrice: { type: number, default: 9.99 }
    steps:
      - stepId: createProduct
        operationId: Products_Create
        requestBody:
          contentType: application/json
          payload:
            title: $inputs.title
            price: $inputs.price
            category: electronics
        successCriteria:
          - condition: $statusCode == 201
        outputs:
          productId: $response.body#/data/id

      - stepId: readProduct
        operationId: Products_GetById
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        successCriteria:
          - condition: $statusCode == 200
          - context: $response.body
            condition: $[?@.category == 'electronics' && @.inStock == false]
            type:
              type: jsonpath
              version: rfc9535
        onFailure:
          - name: cleanUp
            type: goto
            stepId: deleteProduct

      - stepId: changePrice
        operationId: Products_Update
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        requestBody:
          contentType: application/json
          payload:
            title: $inputs.title
            price: $inputs.newPrice
            category: electronics
        successCriteria:
          - condition: $statusCode == 200
          - condition: $response.body#/data/price == $inputs.newPrice
        onFailure:
          - name: cleanUp
            type: goto
            stepId: deleteProduct

      - stepId: deleteProduct
        operationId: Products_Delete
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        successCriteria:
          - condition: $statusCode == 200

      - stepId: confirmGone
        operationId: Products_GetById
        parameters:
          - name: id
            in: path
            value: $steps.createProduct.outputs.productId
        successCriteria:
          - condition: $statusCode == 404
    outputs:
      productId: $steps.createProduct.outputs.productId

Download it with the OpenAPI file it points at, put both in one folder and run it. Node 22.12 or later is enough, as npx fetches the runner:

Terminal
$ npx @redocly/cli@2.62.1 respect products.arazzo.yaml
Output (13 October 2026)
Running workflow products.arazzo.yaml / product-lifecycle

✓ POST /api/Products - step createProduct
  ✓ success criteria check - $statusCode == 201
  ✓ status code check - $statusCode in [200, 201, 400, 500]

✓ GET /api/Products/{id} - step readProduct
  ✓ success criteria check - $statusCode == 200
  ✓ success criteria check - $[?@.category == 'electronics' && @.inStock == fal...
  ✓ status code check - $statusCode in [200, 404, 500]

✓ PUT /api/Products/{id} - step changePrice
  ✓ success criteria check - $statusCode == 200
  ✓ success criteria check - $response.body#/data/price == $inputs.newPrice
  ✓ status code check - $statusCode in [200, 404, 500]

✓ DELETE /api/Products/{id} - step deleteProduct
  ✓ success criteria check - $statusCode == 200
  ✓ status code check - $statusCode in [200, 404, 500]

✓ GET /api/Products/{id} - step confirmGone
  ✓ success criteria check - $statusCode == 404
  ✓ status code check - $statusCode in [200, 404, 500]

Workflows: 1 passed, 1 total
Steps: 5 passed, 5 total
Checks: 12 passed, 12 total
Time: 922ms

A few parts of the file are doing more than they look:

  • $response.body#/data/id is a runtime expression with a JSON Pointer after the #. Our create endpoint wraps the new product in { message, data }, so the ID is at /data/id. Every later step reads it as $steps.createProduct.outputs.productId.
  • The criterion in readProduct uses the new Expression Type Object to say the query is RFC 9535 JSONPath. A JSONPath condition passes when the filter matches at least one node, so $[?@.category == 'electronics' && @.inStock == false] checks two fields at once. We also flipped the expected values to make sure the check fails when it should, and it did.
  • The third check on each step, status code check, isn't in the Arazzo file. Redocly adds it from the responses the OpenAPI file lists, so a status code the API doesn't document fails the step even if your own criteria pass.

The workflow cleans up when a step fails

Products created on our API are shared: anyone listing products sees yours until it's deleted. A test that stops halfway would leave junk behind, so readProduct and changePrice have an onFailure action that jumps to deleteProduct. To check it, we changed the price check so it couldn't pass:

Output with a failing step
✗ PUT /api/Products/{id} - step changePrice
  ✓ success criteria check - $statusCode == 200
  ✗ success criteria check - $response.body#/data/price == $inputs.price
  ✓ status code check - $statusCode in [200, 404, 500]

Running failure action cleanUp for the step changePrice

✓ DELETE /api/Products/{id} - step deleteProduct
  ✓ success criteria check - $statusCode == 200

✓ GET /api/Products/{id} - step confirmGone
  ✓ success criteria check - $statusCode == 404

The workflow still reports a failure, but the product is deleted first. Do the same in any workflow that creates data on a shared environment.

Selector Objects are linted but not run

A Selector Object is the new way to pull a value out of a response. We used one to read a product's rating with a JSON Pointer and send it to our echo endpoint, which returns whatever it receives:

selector.arazzo.yaml (steps)
- stepId: getProduct
  operationId: Products_GetById
  parameters:
    - { name: id, in: path, value: 1 }
  outputs:
    rating:
      context: $response.body
      selector: /rating/rate
      type: jsonpointer
- stepId: echoIt
  operationId: Echo_Echo_POST
  requestBody:
    contentType: application/json
    payload:
      rating: $steps.getProduct.outputs.rating

redocly lint passes this file. The echo endpoint should receive {"rating": 4.3}. Here is what it got:

Request body the runner sent
{
  "rating": {
    "context": {
      "id": 1,
      "title": "Wireless Noise-Cancelling Headphones",
      "price": 249.99,
      ...
      "rating": { "rate": 4.3, "count": 127 },
      "inStock": true,
      "createdAt": "2025-06-12T10:30:00Z"
    },
    "selector": "/rating/rate",
    "type": "jsonpointer"
  }
}

The runner resolved context to the whole response body, then sent the Selector Object itself instead of applying the selector. Nothing in the output warns about it. The step fails only because we checked what the server received; a workflow that just checks for a 200 would pass while sending the wrong data.

Until runners catch up, use the 1.0 form. For a JSON Pointer it's shorter anyway:

Works in 1.0 and 1.1
outputs:
  rating: $response.body#/rating/rate

Parameters passed to a workflow are dropped

The example the announcement uses for parametersis a failure action that calls a token refresh workflow and retries the step. Our free endpoints don't issue tokens, so we tested the same mechanism with a missing product: the failure action calls a report workflow that posts its reason input to the echo endpoint.

params.arazzo.yaml (step)
- stepId: getMissing
  operationId: Products_GetById
  parameters:
    - { name: id, in: path, value: 99999 }
  successCriteria:
    - condition: $statusCode == 200
  onFailure:
    - name: reportAndRetry
      type: retry
      retryLimit: 1
      workflowId: report
      parameters:
        - name: reason
          value: not found

The runner does call the report workflow and then retries the step, which is the order the spec describes. But the echo endpoint received an empty object:

What the echo endpoint received
"contentType": "application/json",
"contentLength": 2,
"body": "{}",
"json": {}

reason never arrived. Running report on its own with --input reason=... works, so the problem is in passing parameters from the action. For a token refresh, this means the refresh call goes out without a refresh token.

What ran in Redocly CLI 2.62.1

Arazzo 1.1 featureLintRun
arazzo: 1.1.0AcceptedRuns
$selfAcceptedAccepted; nothing to observe at run time
Expression Type ObjectAcceptedRuns, and fails when it should
dependsOnAcceptedAccepted; steps run in order anyway
Selector ObjectAcceptedNot applied: the object is sent as data
parameters on actionsAcceptedWorkflow runs, inputs are empty
AsyncAPI stepsNot testedNot tested

We couldn't find a Redocly changelog entry that claims Arazzo 1.1 support, so these may simply be features that haven't been built yet. The other tools listed in the Arazzo repository may differ, and we only tested Redocly.

Writing 1.1 workflows now

  • Use arazzo: 1.1.0 and the features that run: Expression Type Objects, $self and dependsOn.
  • Read response values with runtime expressions like $response.body#/data/id, not Selector Objects.
  • Handle token refresh by logging in again in a step, not through parameters on a failure action.
  • Check what the server received, not only the status code. A passing lint and a 200didn't catch either problem in this post. Our echo endpoint did.
  • Pin the runner version, as we did with @redocly/cli@2.62.1, so a runner update can't change your results without a commit.

If your workflow tests a create, read, update, delete cycle, our CRUD testing lesson covers which checks each step needs. Reading an OpenAPI spec explains the operationId values a workflow refers to, and the products API docs list every field the workflow above touches.