Arazzo for Analysts: Multi-Step API Workflows as Testable YAML
Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.
Key takeaways
- Arazzo is the OpenAPI Initiative's specification for describing multi-step API workflows: which operations run in which order, what data flows from one step to the next, and what counts as success at each step.
- An Arazzo document has four parts that matter to an analyst: sourceDescriptions point to the OpenAPI files, workflows hold ordered steps, successCriteria say what a correct response looks like, and outputs pass values such as a payment id to later steps through runtime expressions like $steps.createIntent.outputs.intentId.
- Arazzo 1.1.0, dated 17 May 2026 in its revision history, adds AsyncAPI steps that send or receive messages, a correlationId and timeout per step, step-level dependsOn, Selector Objects, and $self; tools such as Redocly Respect still document support for Arazzo 1.0.1 only.
- The sequence that lives in a Confluence page and a Bruno collection becomes one machine-readable file that a linter can check, a runner can execute against a sandbox, and a reviewer can diff in a pull request.
- Polling an asynchronous status is expressed with an onFailure action of type retry, with retryAfter, retryLimit, and criteria that match the in-flight status, so the wait is part of the specification rather than hidden in a script.
Arazzo is the OpenAPI Initiative’s specification for describing multi-step API workflows in YAML: which operations run in which order, what data passes from one step to the next, and what a successful response looks like at each step. For an analyst, it turns the payment sequence that lives in a Confluence table into a file a linter can check, a runner can execute against a sandbox, and a reviewer can diff. The current version is 1.1.0, dated 17 May 2026.
An OpenAPI contract tells you every operation an API offers and nothing about the order you must call them in. That order is the integration: create the payment before confirming it, wait until it settles before refunding it, carry the id across. Today it lives in a sequence diagram, a paragraph of prose, and the post-response scripts of a Bruno collection. This article is part of the advanced track of APIs for Analysts.
APIs for Analysts, advanced track. Builds on chaining API requests with JavaScript and how to document an API. Full learning path: APIs for Analysts.
What is Arazzo, and why should an analyst care?
Arazzo describes sequences of API calls and the dependencies between them. It was released as 1.0.0 in 2024, patched as 1.0.1 in January 2025, and extended as 1.1.0 in May 2026. The specification text is at spec.openapis.org/arazzo.
The analyst case is simple. In every payment integration I have worked on, the most important artifact was the end-to-end flow, and it was the one artifact nobody could execute. The sequence diagram drifted from the collection, the collection drifted from the API, and the Confluence page drifted from both. Arazzo collapses three of those into one file that is precise enough to run.
| Where the sequence lives today | What it cannot do | What Arazzo adds |
|---|---|---|
| A sequence diagram | Be executed or validated | The same order of calls, as a runnable workflow |
| A Confluence table of steps | Show what counts as success at each step | successCriteria per step |
| A Bruno or Postman collection | Be read without opening scripts | Data flow declared as outputs and runtime expressions |
| Prose in the API docs | Fail a build when it goes stale | A linter and a runner in CI |
How is an Arazzo document structured?
Five fields carry almost all the meaning.
| Field | What it holds | Analyst reading |
|---|---|---|
arazzo | The specification version, such as 1.0.1 or 1.1.0 | Which tools can read this file |
sourceDescriptions | Named links to OpenAPI (and, from 1.1, AsyncAPI or other Arazzo) documents | Which contracts this flow depends on |
workflows | A list of workflows, each with a workflowId, inputs (a JSON Schema), steps, and outputs | One business scenario each |
steps | An ordered list; each step references an operationId, an operationPath, or another workflowId | One call, its inputs, and its expected result |
components | Reusable parameters, successActions, failureActions, and inputs | Shared rules, such as one retry policy |
Inside a step, four fields do the work:
parametersandrequestBody: what to send. Values can be literals or runtime expressions.successCriteria: a list of conditions that must all pass, such as$statusCode == 200. Conditions can besimple,regex,jsonpath, orxpath.outputs: named values extracted from the response, such asintentId: $response.body#/id.onSuccessandonFailure: what happens next. A success action isendorgoto; a failure action isend,goto, orretrywithretryAfter(seconds) andretryLimit. With noonFailure, the default is to stop and return.
Runtime expressions are the glue. The ones you will use daily:
| Expression | Meaning |
|---|---|
$inputs.amount | A workflow input |
$statusCode | The HTTP status of this step’s response |
$response.body#/status | A field in the response body, by JSON Pointer |
$response.header.Request-Id | A response header |
$steps.createIntent.outputs.intentId | An output of an earlier step |
$sourceDescriptions.stripe.PostPaymentIntents | An operation in a named source description |
Bearer {$inputs.stripeKey} | An expression embedded in a string, in curly braces |
String literals in conditions use single quotes, and the specification requires string comparisons to be case-insensitive, which matters when you assert on status values.
What does a payment workflow look like in Arazzo?
The worked example uses Stripe’s public API in a sandbox, so you can run it. It is the same flow as the hand-scripted chain in chaining API requests with JavaScript: create a PaymentIntent, confirm it with a test card, wait until the status is final, refund it, and wait until the refund is final. The operationIds come from Stripe’s published OpenAPI file: PostPaymentIntents, PostPaymentIntentsIntentConfirm, GetPaymentIntentsIntent, PostRefunds, and GetRefundsRefund. Stripe’s request bodies are form-encoded, which is why the contentType is application/x-www-form-urlencoded.
arazzo: 1.0.1
info:
title: Card payment to refund (Stripe sandbox)
summary: Create, confirm, wait for a final status, refund, wait for the refund.
version: 1.0.0
sourceDescriptions:
- name: stripe
url: https://raw.githubusercontent.com/stripe/openapi/master/latest/openapi.spec3.yaml
type: openapi
workflows:
- workflowId: payAndRefund
summary: Happy path from a card payment to a completed refund
inputs:
type: object
required: [stripeKey, amount, currency, orderRef]
properties:
stripeKey: { type: string, description: "Sandbox secret key, sk_test_..." }
amount: { type: integer, description: "Minor units, 1250 = 12.50" }
currency: { type: string, examples: [eur] }
orderRef: { type: string, description: "Merchant reference, reused in idempotency keys" }
paymentMethod: { type: string, default: pm_card_visa }
parameters:
- name: Authorization
in: header
value: Bearer {$inputs.stripeKey}
steps:
- stepId: createIntent
description: Create the PaymentIntent. The idempotency key makes a retried create safe.
operationId: $sourceDescriptions.stripe.PostPaymentIntents
parameters:
- name: Idempotency-Key
in: header
value: pi-{$inputs.orderRef}
requestBody:
contentType: application/x-www-form-urlencoded
payload:
amount: $inputs.amount
currency: $inputs.currency
"automatic_payment_methods[enabled]": true
"automatic_payment_methods[allow_redirects]": never
successCriteria:
- condition: $statusCode == 200
- condition: $response.body#/status == 'requires_payment_method'
outputs:
intentId: $response.body#/id
- stepId: confirmIntent
operationId: $sourceDescriptions.stripe.PostPaymentIntentsIntentConfirm
parameters:
- name: intent
in: path
value: $steps.createIntent.outputs.intentId
requestBody:
contentType: application/x-www-form-urlencoded
payload:
payment_method: $inputs.paymentMethod
successCriteria:
- condition: $statusCode == 200
- stepId: waitForPayment
description: Poll until the PaymentIntent reaches succeeded.
operationId: $sourceDescriptions.stripe.GetPaymentIntentsIntent
parameters:
- name: intent
in: path
value: $steps.createIntent.outputs.intentId
successCriteria:
- condition: $statusCode == 200
- condition: $response.body#/status == 'succeeded'
onFailure:
- name: stillProcessing
type: retry
retryAfter: 2
retryLimit: 15
criteria:
- condition: $response.body#/status == 'processing'
outputs:
paymentStatus: $response.body#/status
- stepId: createRefund
operationId: $sourceDescriptions.stripe.PostRefunds
parameters:
- name: Idempotency-Key
in: header
value: re-{$inputs.orderRef}
requestBody:
contentType: application/x-www-form-urlencoded
payload:
payment_intent: $steps.createIntent.outputs.intentId
successCriteria:
- condition: $statusCode == 200
outputs:
refundId: $response.body#/id
- stepId: waitForRefund
description: Refund status is not guaranteed final at response time.
operationId: $sourceDescriptions.stripe.GetRefundsRefund
parameters:
- name: refund
in: path
value: $steps.createRefund.outputs.refundId
successCriteria:
- condition: $statusCode == 200
- condition: $response.body#/status == 'succeeded'
onFailure:
- name: refundPending
type: retry
retryAfter: 2
retryLimit: 15
criteria:
- condition: $response.body#/status == 'pending'
outputs:
refundStatus: $response.body#/status
outputs:
paymentIntentId: $steps.createIntent.outputs.intentId
refundId: $steps.createRefund.outputs.refundId
refundStatus: $steps.waitForRefund.outputs.refundStatus
Read it as a reviewer and four analyst decisions are visible that a collection usually hides.
- The polling rule is in the contract.
waitForPaymentsucceeds only onsucceeded, retries every 2 seconds while the status isprocessing, and gives up after 15 retries. Any other status (requires_action,canceled) fails the step immediately. In a Bruno collection, that rule is inside a JavaScript loop nobody reviews. - Idempotency is explicit. Both writes carry an
Idempotency-Keyderived from the merchant’sorderRef, so a rerun after a timeout does not create a second payment or a second refund. That is the property you test in idempotency testing, now declared. - The data flow is declared.
intentIdis produced once bycreateIntentand consumed by three later steps. The specification tells runners to treat$steps.createIntent.outputs.intentIdas a dependency, and recommends an error when a step references an output of a step that comes later. - The expected state at creation is asserted. Checking
requires_payment_methodafter create catches the day someone addsconfirm=trueto the create call and the flow silently changes shape.
Why is the file pinned to Arazzo 1.0.1?
Because the tools that run it today document 1.0.x. Redocly’s respect command documentation states it supports Arazzo 1.0.1 descriptions only, Spectral’s Arazzo ruleset is written against 1.0, and Speakeasy’s arazzo validate command links to 1.0.1. The workflow above uses no 1.1-only field, so pinning to 1.0.1 costs nothing.
How do you write the negative path?
A second workflow in the same file covers the decline. The trick is that successCriteria describe what the test expects, so for a negative test the expected result is the error.
- workflowId: declinedCard
summary: A declined card is rejected with card_declined
inputs:
type: object
required: [stripeKey, orderRef]
properties:
stripeKey: { type: string }
orderRef: { type: string }
parameters:
- name: Authorization
in: header
value: Bearer {$inputs.stripeKey}
steps:
- stepId: createAndConfirm
operationId: $sourceDescriptions.stripe.PostPaymentIntents
requestBody:
contentType: application/x-www-form-urlencoded
payload:
amount: 1250
currency: eur
confirm: true
payment_method: pm_card_visa_chargeDeclined
"automatic_payment_methods[enabled]": true
"automatic_payment_methods[allow_redirects]": never
successCriteria:
- condition: $statusCode == 402
- condition: $response.body#/error/code == 'card_declined'
One workflow per scenario, and the file reads like the scenario list in your test plan, which is exactly what it is. Deriving that scenario list is covered in how to write API test cases.
How does Arazzo relate to request chaining in Bruno and Postman?
It is the same idea moved from imperative scripts into declarative YAML.
| Bruno or Postman | Arazzo |
|---|---|
| Request order in the collection | Order of steps |
| Environment variables | Workflow inputs |
bru.setVar("intentId", res.body.id) or pm.collectionVariables.set(...) | outputs: { intentId: $response.body#/id } |
{{intentId}} in the next request | $steps.createIntent.outputs.intentId |
assert block or expect(...) in tests | successCriteria |
| A polling loop in a post-response script | onFailure with type: retry, retryAfter, retryLimit |
bru.runner.setNextRequest() or pm.execution.setNextRequest() | onSuccess or onFailure with type: goto |
What you lose is flexibility: Arazzo cannot compute an HMAC signature or generate a UUID, which is why the example derives idempotency keys from an input. What you gain is a sequence that any compliant tool can read without executing your JavaScript. My practical split: keep the Bruno collection for exploratory and regression work, and write the Arazzo file for the five or six business-critical flows that must stay documented and tested together. Generating either one with an assistant follows the same rules as in AI-built API collections and scripts: give it the contract, demand real assertions, and run the result.
The habit of writing every flow with its success conditions and failure branches, so consumers and testers read the same thing, is what API Documentation from Scratch teaches, and API Testing and QA Mastery for BAs covers the scenario design behind the workflows.
Which tools can lint and run Arazzo today?
Current as of October 2026, from each project’s own documentation:
| Tool | What it does with Arazzo | Command |
|---|---|---|
| Spectral (Stoplight) | Lints with the built-in spectral:arazzo ruleset: arazzo-document-schema, arazzo-workflowId-unique, arazzo-workflow-stepId-unique, arazzo-workflow-output-validation, and more | spectral lint payment-refund.arazzo.yaml |
Redocly CLI, respect | Executes workflows as tests against a live API; documents support for Arazzo 1.0.1 only | npx @redocly/cli@latest respect payment-refund.arazzo.yaml |
Speakeasy openapi CLI | Validates structure, step references, expressions, and actions | openapi arazzo validate ./payment-refund.arazzo.yaml |
| Speakeasy SDK testing | Generates SDK contract tests from .speakeasy/tests.arazzo.yaml | Configured in the SDK repo |
| Jentic Arazzo Runner | Python execution engine for Arazzo workflows | uvx arazzo-runner execute-workflow ... |
A minimal local loop:
# Lint: structure, unique ids, valid output expressions
echo 'extends: ["spectral:arazzo"]' > .spectral.yaml
npx @stoplight/spectral-cli lint payment-refund.arazzo.yaml
# Run against the Stripe sandbox; the key comes from your shell, not the file
npx @redocly/cli@latest respect payment-refund.arazzo.yaml \
--workflow payAndRefund \
--input stripeKey=$STRIPE_SECRET_KEY \
--input amount=1250 --input currency=eur --input orderRef=ord-20261009-001 \
--verbose
Put the same two commands in the pipeline and the flow becomes a release control, the same move described in API tests in CI.
What did Arazzo 1.1 add?
Arazzo 1.1.0 is dated 17 May 2026 in the specification’s revision history, and the OpenAPI Initiative announced it on 19 May 2026. The additions an analyst should know:
- AsyncAPI steps. A source description can now be of type
asyncapi. A step cansendorreceivea message (theactionfield), reference a channel withchannelPath, match replies withcorrelationId, and wait up to atimeoutin milliseconds. Payload values are read with$message.payload. A flow can now say “POST the payment, then wait for thepayment.settledevent”, which is how most real payment platforms work. The event side is covered in AsyncAPI for analysts. dependsOnon steps, to declare that a step must wait for asynchronous work to finish.- Selector Objects with
jsonpath,xpath, orjsonpointer, for precise extraction. querystringparameters, aligned with OpenAPI 3.2. The OpenAPI side of that change is in OpenAPI 3.1 and 3.2 for analysts.$self, a root URI for unambiguous reference resolution, plus a complete ABNF grammar for runtime expressions and explicit truthy and falsy rules for conditions.
Watch the tool tables before adopting 1.1 features: as of October 2026, the runner documentation I could verify still names 1.0.1.
Can AI agents use Arazzo workflows?
This is where a lot of the current interest comes from, so it is worth being precise. An agent given only an OpenAPI file sees two hundred operations and has to guess the order, the data dependencies, and when a status is final. An Arazzo workflow gives it the order, the inputs, the success conditions, and the retry rules, written down by someone who knows the domain. Jentic, for example, describes its open-source Arazzo Runner as an engine that “powers agents and automation by orchestrating multi-step API flows”. The Arazzo 1.1 announcement lists actor-in-the-loop support (human or agent) as a roadmap item, not a released feature.
For an analyst, the practical point is the same either way: the workflow you write for a human tester is the workflow an agent will execute, so the success criteria and failure branches need the same rigour. AI agents for analysts covers how to specify what an agent may and may not do around those calls.
The takeaway
Arazzo describes the order of API calls, the data passed between them, and the expected result at each step, in a YAML file that references operations in your OpenAPI contracts. For an analyst, that turns the end-to-end flow from a diagram nobody executes into an artifact you can lint with Spectral, run with Redocly Respect, and review in a pull request. Write one workflow per business scenario, put the polling and idempotency rules in the file rather than in scripts, pin the version to what your runner supports, and keep collections for the exploratory work Arazzo is not built for.
Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.
Tags: Arazzo, OpenAPI, API Workflows, API Testing, Systems Analysis, Payments
About the author
Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.
Related articles
- Chaining API Requests With JavaScript in Bruno and Postman: The Scripts Analysts Need Chain API requests in Bruno and Postman: capture values, pre-request and post-response scripts, token refresh, polling, branching, and a Stripe sandbox flow.
- How to Document an API: What Analysts Write So Developers Integrate Without a Call How to document an API as an analyst: the seven sections consumers need, an OpenAPI endpoint example, an error catalogue, flow guides, and docs you can test.
- Sequence Diagrams for Business Analysts: Draw the Flow, Find the Gaps How business analysts use sequence diagrams to map a flow across services, expose integration gaps, and write better requirements. With a payments example.
- AI-Built API Collections and Scripts: Postman, Bruno, and the Checks You Repeat Turn an OpenAPI contract into a Bruno or Postman collection with real assertions, generate the chaining scripts, and put the whole suite behind a CI gate.
Go deeper on this
Not ready to buy? The free downloads are a no-cost place to start, and every article here stays free.
Free account
Practice on the Labs, keep your progress
A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.
Your email is used to sign you in. Nothing else, unless you ask. Privacy.