The fastest way to understand Arazzo is to write a spec yourself. This tutorial therefore walks through a complete workflow step by step, from login through placing an order to checking its status. The skeleton fits in twenty lines.
info:
title: Order with status tracking
version: 1.0.0
sourceDescriptions:
- name: shopApi
url: ./openapi.yaml
type: openapi
workflows:
- workflowId: placeAndTrackOrder
steps:
- stepId: login
operationId: createSession
- stepId: createOrder
operationId: createOrder
- stepId: checkStatus
operationId: getOrderStatus
Three steps across one API, still without data flows and failure handling. The rest of the tutorial grows this exact skeleton into a complete, verifiable description. If you first want to know what Arazzo is and what it is meant for, the article What is Arazzo has the background. This guide is about doing.
An Arazzo spec describes a multi-step API workflow as a YAML or JSON file. It references one or more OpenAPI descriptions via sourceDescriptions, defines the steps of a flow under workflows, and uses outputs, parameters, and successCriteria to pin down how data flows from step to step and when a step counts as successful. The finished file can be validated, versioned, and executed by tools such as test runners or AI agents.
The use case and the starting point
The example workflow models a transaction nearly every commerce API knows. A consumer logs in, places an order, and then checks its status. Three endpoints, three dependencies. The login returns a token the order needs. The order returns an ID the status check needs. Those handoffs are precisely the part an OpenAPI spec alone cannot express and Arazzo does.
The starting point is an existing OpenAPI file, openapi.yaml, with the three operations. For this tutorial, only their operationId values matter, because they are how Arazzo wires its steps to the operations.
paths:
/sessions:
post:
operationId: createSession
/orders:
post:
operationId: createOrder
/orders/{orderId}/status:
get:
operationId: getOrderStatus
A clean OpenAPI spec with descriptive operationId values is half the Arazzo work done. If yours holds cryptic or generated IDs, clean them up before the first workflow, because every Arazzo file references them verbatim.
Referencing the sources
Every Arazzo file opens with three pieces of information. The version line states which Arazzo version the file is written against. The info block names the workflow for humans. And sourceDescriptions connects the file to the underlying API descriptions.
info:
title: Order with status tracking
version: 1.0.0
description: Log in, place an order, track its status
sourceDescriptions:
- name: shopApi
url: ./openapi.yaml
type: openapi
The source’s name is more than decoration. Once several sources are in play, a second API for payment, say, the steps address their operations through that name. For getting started, one source does the job. In this tutorial the reference points to a local file. In practice it points to the published spec of the respective API.
The login step and its outputs
The first step authenticates the consumer. The interesting part is the ending, where the token for the following steps gets extracted.
- stepId: login
operationId: createSession
requestBody:
payload:
apiKey: $inputs.apiKey
successCriteria:
- condition: $statusCode == 201
outputs:
token: $response.body#/accessToken
Three mechanisms work together here. The expression $inputs.apiKey reaches into the workflow’s inputs, which get defined later. The successCriteria states that only a 201 counts as success, and anything else fails the step. And the outputs extract the token from the response and publish it under the name token. From here on, any later step can reach it.
The syntax behind $response.body#/accessToken is a JSON pointer behind a runtime expression. The part before the hash names the source, the part after it the path into the document. For deeply nested structures or XML responses, Arazzo 1.1.0 adds the Selector Object with jsonpath or xpath, one of the additions that What’s new in Arazzo 1.1 introduces.
The order and the data flow
The second step uses the token and produces the order ID in turn. It shows the pattern that carries every Arazzo workflow, the chain of output and access.
operationId: createOrder
parameters:
- name: Authorization
in: header
value: Bearer $steps.login.outputs.token
requestBody:
payload:
items: $inputs.items
successCriteria:
- condition: $statusCode == 201
outputs:
orderId: $response.body#/id
The expression $steps.login.outputs.token reads left to right as a path through the workflow. From the step login, take the output field token, used here as a bearer header. These references are explicit and checkable, and that is exactly what separates the spec from a prose description that merely claims the handoff. A validator catches a typo in a step name immediately. A wiki page forgives it for years.
The dual role of parameters deserves a mention. They set header, query, or path parameters and can draw values from inputs, earlier steps, or constants. That covers practically every spot where an API call gets parameterized from the outside.
The status check with retries
The third step queries the order status, and a requirement joins in here that real systems run into often. The status rarely sits at the target value right away, because processing happens in the background. So the step must not give up on the first try.
operationId: getOrderStatus
parameters:
- name: orderId
in: path
value: $steps.createOrder.outputs.orderId
- name: Authorization
in: header
value: Bearer $steps.login.outputs.token
successCriteria:
- condition: $response.body#/status == "confirmed"
onFailure:
- name: retryStatus
type: retry
retryAfter: 2
retryLimit: 10
outputs:
finalStatus: $response.body#/status
The onFailure with type retry repeats the step up to ten times, two seconds apart, until the success criterion holds. The familiar polling pattern is now described declaratively, limits included. After the tenth attempt, the step counts as failed for good, and the workflow ends in a controlled way rather than waiting forever.
Besides retry, onFailure knows the types goto, for jumping to another step, and end, for an immediate stop. To describe compensation steps, canceling the order after a finally failed follow-up step, say, you combine goto with a dedicated cleanup step. The conceptual side of those failure paths, from idempotency to the saga pattern, is covered in the overview of API orchestration.
A second API joins
Real flows rarely stay inside one API, and that is exactly why sourceDescriptions is a list. Suppose payment runs through a dedicated payment API with its own OpenAPI file. The head of the spec then grows by one source, and the steps address their operations with the source name as prefix.
- name: shopApi
url: ./openapi.yaml
type: openapi
- name: paymentApi
url: ./payment-openapi.yaml
type: openapi
# In the workflow: address the operation unambiguously through its source
- stepId: authorizePayment
operationId: paymentApi.authorizePayment
parameters:
- name: Authorization
in: header
value: Bearer $steps.login.outputs.token
successCriteria:
- condition: $statusCode == 200
outputs:
paymentId: $response.body#/id
The prefix is only required when an operationId appears in several sources or could be confused, yet it does no harm elsewhere and makes each operation’s origin readable at a glance. This is also where the spec’s real value shows. A flow across two APIs normally has no shared home anymore, because each API documents only itself. The Arazzo file is that home.
With the second source, the ownership question comes up fresh, since the flow now belongs to neither API team alone. What works in practice is placing the file with the team that owns the business process, with that team looping in both API teams on changes. By this point at the latest, it becomes clear why the workflow spec needs a repository of its own. It is an asset in its own right, with its own versioning and review processes, and it belongs in none of the participating API repositories.
The workflow’s inputs and outputs
So far the steps have reached into $inputs without those being defined. The workflow declares its inputs as a JSON schema and its outputs as named expressions, which makes it look like a function from the outside.
inputs:
type: object
required: [apiKey, items]
properties:
apiKey:
type: string
items:
type: array
items:
type: object
outputs:
orderId: $steps.createOrder.outputs.orderId
finalStatus: $steps.checkStatus.outputs.finalStatus
This interface pays off twice. A test runner knows which data to supply and which results to check. And since Arazzo 1.1.0, another workflow can call this one as a building block and pass values through exactly these inputs, letting larger flows grow out of smaller ones.
Validate, run, check in
An Arazzo file earns its keep once it gets checked and executed. Three stations have proven themselves for that.
- Validation against the specification. An Arazzo validator checks structure, required fields, and whether every reference resolves, including whether each
operationIdexists in the referenced OpenAPI file. This check belongs in the CI pipeline as a job right next to the OpenAPI linting. - Execution against a test environment. A test runner works through the steps against a live API, fills the inputs with test data, and reports which step and which criterion a run failed on. The documentation turns into an end-to-end test.
- Maintenance under version control. The Arazzo file belongs in a repository of its own, with its own reviews, tags, and releases, and its version in the
infoblock. It is an asset in its own right with its own lifecycle, and that versioning only stays clean as long as it does not blend into the release history of any single API.
At one team that introduced this three-part routine, the nightly workflow run failed unexpectedly at the status step a few weeks later. The backend colleagues had renamed the status value from "confirmed" to "completed" and considered the change cosmetic. The run made the break visible the next morning, long before any consumer noticed. The wiki documentation of the same flow would likely have claimed the old value for months.
For the pipeline integration, a single additional job covers both checks in the simplest case.
arazzo-check:
stage: test
script:
- arazzo validate workflows/place-and-track-order.arazzo.yaml
- arazzo run workflows/place-and-track-order.arazzo.yaml
--server https://staging.example.com
--input apiKey=$STAGING_API_KEY
--input items=@testdata/items.json
The tooling landscape around Arazzo is young, and validation and execution already have several options, from open-source runners to editors with built-in checks. Which combination fits depends mainly on whether the run happens in the pipeline or at the developer’s desk. More important than the tool choice is the rule behind it. A workflow file that never gets executed ages just as fast as the wiki it was meant to replace.
Typical snags on the first workflow
A few snags come up so reliably on first contact that a heads-up saves real time.
- Mixed-up expression contexts.
$response.bodyonly works inside the current step’soutputsandsuccessCriteria, while later steps go through$steps.name.outputs.field. Reaching directly into an earlier step’s response is the most common beginner mistake, and the validator’s message for it is rarely clear. - Missing quotes around comparison values. The condition
$response.body#/status == confirmedfails silently, because the comparison value gets read as an expression rather than a string. With== "confirmed", the intent is unambiguous. - Overly strict success criteria. Check for exact response times or complete body structures and you build a test that breaks on every harmless API extension. Criteria on the status code plus the few business-critical fields hold up best, nothing more.
- Workflows as a copy of the OpenAPI description. An Arazzo file that merely calls each endpoint once, with no data flowing, documents nothing the OpenAPI spec does not already say. The value comes from dependencies and failure paths.
- Secrets in the spec. API keys and tokens belong in the inputs and arrive at runtime, as in the CI example through variables. A workflow file with an embedded key otherwise lands in the repository, secret included.
None of these is a reason to postpone the start. Each costs about half an hour of searching on the first workflow, and afterward they are routine.
Keeping the workflow readable
An Arazzo file gets read by more people than it gets written by, including consumers, reviewers, and, increasingly, AI agents that use it as a blueprint. A few conventions keep it understandable for all three.
The stepId values deserve the same care as function names in code. A step called checkStatus explains itself, one called step3 explains nothing, and because later steps reference outputs through these names, unclear names travel through the whole file. The same goes for output names, which act as the interface between steps.
Every step also accepts a description, and it pays to use it for the why rather than the what. That the step queries the status, anyone can see from the operation. Why it retries up to ten times, and why only "confirmed" counts as success, is written down nowhere else. For agents executing the workflow, this layer of reasoning is valuable context the bare structure cannot carry. How APIs as a whole become readable for machine consumers is the subject of AI-ready APIs.
As the number of workflows grows, one file per workflow has proven itself, named after the workflowId, rather than a collection file with ten flows inside. Individual files get versioned, reviewed, and listed in the catalog one by one, and chaining through workflow calls works across file boundaries, since sourceDescriptions accepts Arazzo documents as well.
Where the finished spec goes
With the file running, the visibility question follows, and it decides whether the work pays off beyond your own team. In its repository, the workflow is in good hands for everyone working on the flows. API consumers rarely look there, though. They search the developer portal, and what they usually find there are the endpoints, without the flows.
The consistent next step is therefore to publish the workflow description where the API documentation lives, in the portal’s catalog. A consumer who discovers the order API then sees the described flow right next to it and no longer has to guess the call order from the endpoint list. There, an Arazzo file can be managed exactly like an OpenAPI spec, with ownership, versions, and an audit trail that stands up to regulated requirements. What that means organizationally, and how workflows work as catalog content in their own right, is covered in Arazzo workflows in the API catalog.
The complete file at a glance
To wrap up, here is the full spec as it sits in the repository after all the steps. It is deliberately compact and free of special cases, and it works as a template for your first own workflow.
info:
title: Order with status tracking
version: 1.0.0
sourceDescriptions:
- name: shopApi
url: ./openapi.yaml
type: openapi
workflows:
- workflowId: placeAndTrackOrder
inputs:
type: object
required: [apiKey, items]
properties:
apiKey: { type: string }
items: { type: array }
steps:
- stepId: login
operationId: createSession
requestBody:
payload:
apiKey: $inputs.apiKey
successCriteria:
- condition: $statusCode == 201
outputs:
token: $response.body#/accessToken
- stepId: createOrder
operationId: createOrder
parameters:
- name: Authorization
in: header
value: Bearer $steps.login.outputs.token
requestBody:
payload:
items: $inputs.items
successCriteria:
- condition: $statusCode == 201
outputs:
orderId: $response.body#/id
- stepId: checkStatus
operationId: getOrderStatus
parameters:
- name: orderId
in: path
value: $steps.createOrder.outputs.orderId
- name: Authorization
in: header
value: Bearer $steps.login.outputs.token
successCriteria:
- condition: $response.body#/status == "confirmed"
onFailure:
- name: retryStatus
type: retry
retryAfter: 2
retryLimit: 10
outputs:
finalStatus: $response.body#/status
outputs:
orderId: $steps.createOrder.outputs.orderId
finalStatus: $steps.checkStatus.outputs.finalStatus
The best candidate for your first Arazzo spec is a flow that already exists as a prose guide in your wiki and gets asked about by consumers. The translation into a spec usually takes less than a day, and the mismatches between guide and actual API behavior that surface along the way tend to be worth the effort on their own.
Where to take the workflow next
With the first runnable spec in place, the follow-up questions arrive quickly. Multiple sources for flows across API boundaries work through additional entries in sourceDescriptions. Event steps that wait for a message come with the AsyncAPI additions in Arazzo 1.1.0. And once several workflows exist, it pays to carve recurring sub-flows into workflows of their own and call them.
As your next step, describe a second workflow from your backlog, this time one with a real compensation case, a cancellation, say. That is exactly where it shows whether your APIs' failure paths are as well defined as the documentation claims.