Steps, inputs, and outputs

By Phil Sturgeon
Last update on January 31, 2025

If workflows are the recipes, then steps are the individual cooking instructions. Each step does one thing: call an API endpoint, run another workflow, or make a decision. Steps get their data from inputs and output from other steps, do their work, then save interesting bits of the response for steps that come after.

What is a step? #

A step is a single action in your workflow. Most often that means calling an API endpoint, but steps can also invoke other workflows or handle conditional branching.

Here’s an example step that operates within a workflow to search for train trips:

steps:
  - stepId: searchTrips
    description: Search for available train trips
    operationId: $sourceDescriptions.trainApi.searchTrips
    parameters:
      - name: origin
        in: query
        value: $inputs.origin
    successCriteria:
      - condition: $statusCode == 200
    outputs:
      firstTripId: $response.body#/trips/0/id

Required fields #

stepId - The unique name for this step within the workflow.

steps:
  - stepId: search
  - stepId: createBooking
  - stepId: processPayment

Stick to descriptive names in camelCase or kebab-case. This ID is how other steps reference this step’s outputs, and how control flow (like goto) targets specific steps.

Every step needs to specify what it’s going to do, and that will be a single operation or a whole other workflow. Operations can be picked with the operationId if the OpenAPI has defined operationIds (best practice, and most linters will pester you to do this), or operationPath if operationIds are missing. Kicking off a whole workflow can be done with workflowId.

operationId - References an operation from your source APIs.

- stepId: getBooking
  operationId: $sourceDescriptions.trainApi.getBookingById

operationPath - Reference by HTTP method and path. Requires a bit more implementation detail and will break when a path or parameter changes.

- stepId: getBooking
  operationPath: /bookings/{bookingId}
  method: get  # Will also need to specify method

workflowId - Execute another workflow.

- stepId: cancelBooking
  workflowId: $sourceDescriptions.trainApi.cancelBookingWorkflow

Optional fields #

description - Explain what the step does in a way that is useful for human-readable documentation.

- stepId: search
  description: Search for train trips between two stations on a specific date
  operationId: $sourceDescriptions.trainApi.searchTrips

parameters - Override or add parameters to the operation.

- stepId: search
  operationId: $sourceDescriptions.trainApi.searchTrips
  parameters:
    - name: origin
      in: query
      value: $inputs.origin
    - name: destination
      in: query
      value: $inputs.destination

requestBody - Define the request body for POST/PUT/PATCH/QUERY operations.

- stepId: createBooking
  operationId: $sourceDescriptions.trainApi.createBooking
  requestBody:
    contentType: application/json
    payload:
      tripId: $steps.search.outputs.tripId
      passengers: $inputs.passengers

successCriteria - Define what counts as a successful step execution.

- stepId: search
  operationId: $sourceDescriptions.trainApi.searchTrips
  successCriteria:
    - condition: $statusCode == 200
    - context: $response.body
      condition: $[?count(@.trips) > 0]
      type: jsonpath

onSuccess / onFailure - Actions to take based on outcome.

- stepId: checkAvailability
  operationId: $sourceDescriptions.api.checkStock
  successCriteria:
    - condition: $response.body#/available == true
  onFailure:
    - name: soldOut
      type: end

outputs - Extract data from the response.

- stepId: createBooking
  operationId: $sourceDescriptions.api.createBooking
  outputs:
    bookingId: $response.body#/id
    status: $response.body#/status
    totalPrice: $response.body#/pricing/total

Step execution order #

By default, steps run in order from top to bottom. Step 1 finishes, then step 2 runs, then step 3, and so on. Simple and predictable:

steps:
  - stepId: step1  # Executes first
    # ...
  
  - stepId: step2  # Executes second (after step1 completes)
    # ...
  
  - stepId: step3  # Executes third (after step2 completes)
    # ...

Controlling flow with actions #

But sometimes you need to jump around. The onSuccess and onFailure fields let you break out of that linear flow:

steps:
  - stepId: checkInventory
    operationId: $sourceDescriptions.api.checkStock
    onSuccess:
      - name: proceed
        type: goto
        stepId: createBooking
    onFailure:
      - name: unavailable
        type: goto
        stepId: notifyUser
  
  - stepId: createBooking
    # Runs if inventory check succeeded
  
  - stepId: notifyUser
    # Runs if inventory check failed

Working with inputs #

Steps need data to do their work, and that data comes from three places. Let’s look at each:

1. Workflow inputs #

Data passed to the entire workflow gets accessed with $inputs:

workflows:
  - workflowId: search-trips
    inputs:
      type: object
      properties:
        origin:
          type: string
        destination:
          type: string
    
    steps:
      - stepId: search
        operationId: $sourceDescriptions.api.searchTrips
        parameters:
          # Reference workflow inputs
          - name: origin
            in: query
            value: $inputs.origin
          - name: destination
            in: query
            value: $inputs.destination

2. Previous step outputs #

Access data from earlier steps.

steps:
  - stepId: search
    operationId: $sourceDescriptions.trainApi.searchTrips
    outputs:
      selectedTripId: $response.body#/trips/0/id
  
  - stepId: createBooking
    operationId: $sourceDescriptions.api.createBooking
    requestBody:
      payload:
        # Reference output from previous step
        tripId: $steps.search.outputs.selectedTripId

3. Global parameters #

Inherit parameters defined at the workflow level:

workflows:
  - workflowId: authenticated-flow
    parameters:
      - name: Authorization
        in: header
        value: Bearer $inputs.token
    
    steps:
      - stepId: getBookingDetails
        operationId: $sourceDescriptions.trainApi.getBooking
        # Automatically includes Authorization header

Parameters #

Parameters are how you customize API calls. They can go in the URL (query or path), in headers, or in cookies. Each parameter overrides or supplements what’s defined in the OpenAPI operation:

Query parameters #

- stepId: search
  operationId: $sourceDescriptions.api.searchTrips
  parameters:
    - name: origin
      in: query
      value: $inputs.origin
    - name: destination
      in: query
      value: $inputs.destination
    - name: date
      in: query
      value: $inputs.departureDate
    - name: passengers
      in: query
      value: $inputs.passengerCount

Path parameters #

- stepId: getBooking
  operationId: $sourceDescriptions.api.getBookingById
  parameters:
    - name: bookingId
      in: path
      value: $steps.createBooking.outputs.bookingId

Header parameters #

- stepId: authenticatedRequest
  operationId: $sourceDescriptions.api.getProtectedResource
  parameters:
    - name: Authorization
      in: header
      value: Bearer $steps.login.outputs.accessToken
    - name: X-Request-ID
      in: header
      value: $inputs.requestId
- stepId: sessionRequest
  operationId: $sourceDescriptions.api.getSessionData
  parameters:
    - name: sessionId
      in: cookie
      value: $steps.auth.outputs.sessionId

Request bodies #

For POST, PUT, PATCH, and QUERY operations, you’ll usually need to send a request body. Arazzo makes this straightforward:

Simple request body #

- stepId: addPassenger
  operationId: $sourceDescriptions.trainApi.addPassengerToBooking
  requestBody:
    contentType: application/json
    payload:
      name: $inputs.passengerName
      email: $inputs.passengerEmail

Complex request body #

- stepId: createBooking
  operationId: $sourceDescriptions.api.createBooking
  requestBody:
    contentType: application/json
    payload:
      tripId: $steps.search.outputs.tripId
      passengers:
        - name: $inputs.passengers[0].name
          age: $inputs.passengers[0].age
          seatPreference: $inputs.passengers[0].seat
        - name: $inputs.passengers[1].name
          age: $inputs.passengers[1].age
          seatPreference: $inputs.passengers[1].seat
      contactInfo:
        email: $inputs.email
        phone: $inputs.phone
      specialRequests: $inputs.specialRequests

Using previous step data #

- stepId: updateBooking
  operationId: $sourceDescriptions.api.updateBooking
  parameters:
    - name: bookingId
      in: path
      value: $steps.createBooking.outputs.id
  requestBody:
    contentType: application/json
    payload:
      # Merge previous booking data with updates
      status: confirmed
      paymentId: $steps.processPayment.outputs.id
      confirmationCode: $steps.generateCode.outputs.code

Different content types #

# JSON
- stepId: jsonRequest
  requestBody:
    contentType: application/json
    payload:
      key: value

# Form data
- stepId: updateTravelPreferences
  requestBody:
    contentType: application/x-www-form-urlencoded
    payload:
      seatPreference: $inputs.seatPreference
      mealOption: $inputs.mealOption

# Multipart (file upload)
- stepId: uploadTicket
  requestBody:
    contentType: multipart/form-data
    payload:
      file: $inputs.ticketScan
      bookingId: $inputs.bookingId

Outputs #

Outputs are how you pluck useful data from responses and make it available to later steps. Don’t extract everything, just grab what you’ll actually need.

Basic outputs #

- stepId: createBooking
  operationId: $sourceDescriptions.api.createBooking
  outputs:
    bookingId: $response.body#/id
    status: $response.body#/status

JSON pointer notation #

Most real-world APIs using JSON are going to have nested data, with objects and arrays inside each other. Selecting out the exact piece of data needed for an output value can be done with JSON Pointer notation.

- stepId: getBookingDetails
  operationId: $sourceDescriptions.trainApi.getBooking
  outputs:
    bookingId: $response.body#/id
    customerName: $response.body#/customer/name
    customerEmail: $response.body#/customer/email
    tripOrigin: $response.body#/trip/origin
    tripDestination: $response.body#/trip/destination
    totalPrice: $response.body#/pricing/total

When dealing with arrays, you can access a specific record in an array by its index using JSON Pointer notation.

- stepId: search
  operationId: $sourceDescriptions.api.searchTrips
  outputs:
    firstTripPrice: $response.body#/trips/0/price
    secondTripPrice: $response.body#/trips/1/price

Headers and status #

Besides response bodies, you can also extract HTTP status codes and headers. This is useful for capturing rate limits, pagination tokens, or debugging information:

- stepId: makeRequest
  operationId: $sourceDescriptions.api.someOperation
  outputs:
    # Response body
    responseData: $response.body
    
    # Status code
    statusCode: $statusCode
    
    # Headers
    contentType: $response.header.content-type
    rateLimit: $response.header.x-rate-limit-remaining

Using outputs in subsequent steps #

Outputs are referenced using the $steps runtime expression:

steps:
  # Step 1: Create a booking
  - stepId: createBooking
    operationId: $sourceDescriptions.api.createBooking
    outputs:
      bookingId: $response.body#/id
      amount: $response.body#/totalPrice
  
  # Step 2: Process payment (uses booking outputs)
  - stepId: processPayment
    operationId: $sourceDescriptions.paymentApi.createCharge
    requestBody:
      payload:
        bookingReference: $steps.createBooking.outputs.bookingId
        amount: $steps.createBooking.outputs.amount
        currency: USD
    outputs:
      paymentId: $response.body#/id
      status: $response.body#/status
  
  # Step 3: Confirm booking (uses both previous outputs)
  - stepId: confirmBooking
    operationId: $sourceDescriptions.api.confirmBooking
    parameters:
      - name: bookingId
        in: path
        value: $steps.createBooking.outputs.bookingId
    requestBody:
      payload:
        paymentId: $steps.processPayment.outputs.paymentId
        paymentStatus: $steps.processPayment.outputs.status

Best practices #

A few tips that’ll make your workflows easier to work with:

Descriptive step IDs #

Make step IDs self-documenting. Six months from now, searchAvailableTrips will be much clearer than step1:

# Good
- stepId: searchAvailableTrips
- stepId: selectFirstTrip
- stepId: createBookingForTrip
- stepId: processPaymentForBooking

# Avoid
- stepId: step1
- stepId: doSomething
- stepId: api_call

Extract useful outputs #

Be intentional about outputs. Only extract data you know you’ll need in a later step:

# Good - outputs are used in subsequent steps
- stepId: search
  outputs:
    tripId: $response.body#/trips/0/id  # Used in next step
    price: $response.body#/trips/0/price  # Used for display

# Avoid - extracting everything "just in case"
- stepId: search
  outputs:
    entireResponse: $response.body
    # Too broad - unclear what will actually be used

# Avoid - vague names
outputs:
  id: $response.body#/trips/0/id
  value: $response.body#/customer/email
  amount: $response.body#/pricing/total

Clear descriptions #

- stepId: validatePayment
  description: |
    Validates payment details before processing.
    Checks card number format, expiry date, and CVV.
    Returns validation errors if any field is invalid.
  operationId: $sourceDescriptions.paymentApi.validateCard

Wrapping up #

Steps are where workflows get real work done. Each step calls an operation, transforms data, and passes results to the next step.

With steps down, you’re ready to handle the inevitable: things going wrong. The next section covers success and failure conditions.

Success and failure