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.

yaml
arazzo: 1.1.0
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.

Note

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.

yaml
# Excerpt from openapi.yaml — only the operationIds matter here
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.

yaml
arazzo: 1.1.0
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.

yaml
steps:
  - 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.

yaml
  - 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

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.

yaml
  - 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

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.

yaml
sourceDescriptions:
  - 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.

yaml
  - workflowId: placeAndTrackOrder
    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.

  1. Validation against the specification. An Arazzo validator checks structure, required fields, and whether every reference resolves, including whether each operationId exists in the referenced OpenAPI file. This check belongs in the CI pipeline as a job right next to the OpenAPI linting.
  2. 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.
  3. 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 info block. 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.
Observation from the field

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.

yaml
# CI job, schematic: validate first, then run against staging
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.

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.

yaml
arazzo: 1.1.0
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
Tip

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.