Arazzo 1.1: what's new, and what runs today
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:
| Change | What it does |
|---|---|
type: asyncapi sources | A source description can now be an AsyncAPI document, next to openapi and arazzo. |
channelPath, action, correlationId, timeout | Steps that send or receive messages. action is send or receive, correlationId matches a reply to its request, and timeout is a limit in milliseconds. |
dependsOn | A step can list other steps that must finish before it starts. Listing a step doesn't run it. |
parameters on actions | A success or failure action that calls another workflow can pass it inputs, for example to refresh a token and then retry. |
| Selector Object | Pulls a value out of a response or input with JSONPath, XPath or JSON Pointer. Works in outputs, parameter values and request bodies. |
| Expression Type Object | Pins 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: querystring | Sends a parameter as the entire query string, matching the same addition in OpenAPI 3.2. |
$self | A 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.
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.productIdDownload 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:
- products.arazzo.yaml
- testingapis-products.openapi.json (our live OpenAPI document, cut down to the products and echo endpoints, with a
serversentry so the runner knows where to send requests)
$ npx @redocly/cli@2.62.1 respect products.arazzo.yamlRunning 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: 922msA few parts of the file are doing more than they look:
$response.body#/data/idis 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
readProductuses 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:
✗ 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 == 404The 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:
- 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.ratingredocly lint passes this file. The echo endpoint should receive {"rating": 4.3}. Here is what it got:
{
"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:
outputs:
rating: $response.body#/rating/rateParameters 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.
- 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 foundThe 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:
"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 feature | Lint | Run |
|---|---|---|
arazzo: 1.1.0 | Accepted | Runs |
$self | Accepted | Accepted; nothing to observe at run time |
| Expression Type Object | Accepted | Runs, and fails when it should |
dependsOn | Accepted | Accepted; steps run in order anyway |
| Selector Object | Accepted | Not applied: the object is sent as data |
parameters on actions | Accepted | Workflow runs, inputs are empty |
| AsyncAPI steps | Not tested | Not 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.0and the features that run: Expression Type Objects,$selfanddependsOn. - 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
parameterson 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.