API Workflows and How to Define Them

This article is written by Hunter Skrasek, Architect at Modernize Home Services and maintainer of arazzo-parser. All opinions expressed are his own.

Since 2011 OpenAPI (under the Swagger moniker up until 2016) has been leading the charge in empowering developers to quickly and easily document their RESTful APIs. A vast ecosystem has cropped up around the specification allowing for SDK generation, contract testing, API gateway configuration, and more.

But how to document using API endpoints in conjunction to accomplish a task or goal? That wasn't easily possible without using other tools, or a combination of Links and vendor extensions; That is, until 2024.

Arazzo Specification#

On September 25th, 2024 the OpenAPI Initiative published the 1.0.0 draft of a new specification: Arazzo. A new tool not for documenting the various endpoints of an API, but for documenting "... sequences of calls and their dependencies to be woven together and expressed in the context of delivering a particular outcome or set of outcomes when dealing with API descriptions." [1]

Now developers have the capability to document the various interactions between endpoints, even across multiple API specifications, to accomplish a goal; Purchasing train tickets, opening a bank account, creating an SMS conversation. Furthermore, you can nest or reference other Arazzo specifications when defining your sequences of actions. Allowing you to document more and more complex sequences depending on the needs of your service.

Example#

Before we dive in to the library I've created to help kick-start creating new tools, let's take a look at an example Arazzo document that bump.sh created for their own article about the new specification.

arazzo: 1.0.0
info:
  title: Train Travel API - Book & Pay
  version: 1.0.0
  description: >-
    This API allows you to book and pay for train travel. It is a simple API
    that allows you to search for trains, book a ticket, and pay for it, and
    this workflow documentation shows how each step interacts with the others.
sourceDescriptions:
  - name: train-travel
    url: ./openapi.yaml
    type: openapi
workflows:
  - workflowId: book-a-trip
    summary: Find train trips to book between origin and destination stations.
    description: >-
      This is how you can book a train ticket and pay for it, once you've found
      the stations to travel between and trip schedules.
    inputs:
      $ref: "#/components/inputs/book_a_trip_input"
    steps:
      - stepId: find-origin-station
        description: Find the origin station for the trip.
        operationId: get-stations
        parameters:
          - name: coordinates
            in: query
            value: $inputs.my_origin_coordinates
        successCriteria:
          - condition: $statusCode == 200
        outputs:
          station_id: $outputs.data[0].id
          # there is some implied selection here - get-station responds with a
          # list of stations, but we're only selecting the first one here.
      - stepId: find-destination-station
        operationId: get-stations
        description: Find the destination station for the trip.
        parameters:
          - name: search_term
            in: query
            value: $inputs.my_destination_search_term
        successCriteria:
          - condition: $statusCode == 200
        outputs:
          station_id: $outputs.data[0].id
          # there is some implied selection here - get-station responds with a
          # list of stations, but we're only selecting the first one here.
      - stepId: find-trip
        description: Find the trip between the origin and destination stations.
        operationId: get-trips
        parameters:
          - name: date
            in: query
            value: $inputs.my_trip_date
          - name: origin
            in: query
            value: $steps.find-origin-station.outputs.station_id
          - name: destination
            in: query
            value: $steps.find-destination-station.outputs.station_id
        successCriteria:
          - condition: $statusCode == 200
        outputs:
          trip_id: $response.body.data[0].id

      - stepId: book-trip
        description:
          Create a booking to reserve a ticket for that trip, pending payment.
        operationId: create-booking
        requestBody:
          contentType: application/json
          payload:
            trip_id: $steps.find-trip.outputs.trip_id
            passenger_name: "John Doe"
            has_bicycle: false
            has_dog: false
        successCriteria:
          - condition: $statusCode == 201
        outputs:
          booking_id: $response.body.id

components:
  inputs:
    book_a_trip_input:
      type: object
      properties:
        my_origin_coordinates:
          type: string
          description: The coordinates to use when searching for a station.
        my_destination_search_term:
          type: string
          description: The search term to use when searching for a station.
        my_trip_date:
          $ref: "#/components/inputs/trip_date"
    trip_date:
      type: string
      format: date-time

The examples provided include simplified assumptions and hardcoded values for illustrative purposes. Actual implementation typically involves dynamic inputs.

With this document you can know exactly how to locate two train stations, the next train between those stations, and booking a ticket on that train. A simple and straightforward example, but a good starting point for us to explore my newest contribution to open source development.

Bringing Arazzo to the PHP ecosystem#

The day I first learned about Arazzo, I was immediately struck with the potential it can bring. Better SDK generation around uses-cases instead of endpoints, automating functional tests against APIs, better documentation. So many opportunities and yet so little time, but before these tools can be built, you need additional tools to use as the foundation; it's tools all the way down.

To build tools for working with Arazzo documents in PHP, we first need to be able to parse those documents into something more useable within PHP. And that is where my newest package hskrasek/arazzo-parser comes in.

Getting Started#

To use my parser, you simply need to install it using Composer with the following command:

composer require hskrasek/arazzo-parser

Once installed, usage is relatively simple and straightforward. You can load an existing Arazzo document via locally or remotely hosted files, whether they are in JSON or YAML format, or from any string representation, and output an entirely "Plain Old PHP Object" version of the document and all of its components:

<?php

declare(strict_types=1);

use HSkrasek\Arazzo\Parser;

require __DIR__ . '/vendor/autoload.php';

$specification = Parser::parse(
    file_get_contents('https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/a97f549346f8cb44ec8d5e9d08cfe57b8b09cd6e/arazzo.yaml')
);

$info = $specification->info;

echo "Title: {$info->title}" . PHP_EOL;
echo "Version: {$info->version}" . PHP_EOL;
echo "Description: {$info->description}" . PHP_EOL;

The ultimate goal of my parser is to give myself, and hopefully other developers, a solid basis to build on. Concrete, type-hintable objects instead of arrays of arrays of arrays. Most of that mapping has been completed, but there is always room for improvement. Take for example, the representation of a workflow.

<?php

declare(strict_types=1);

namespace HSkrasek\Arazzo\Specification;

use HSkrasek\Arazzo\Specification\Actions\Failure;
use HSkrasek\Arazzo\Specification\Actions\Success;

final readonly class Workflow
{
    /**
     * @param  Step[]  $steps
     * @param  array<non-empty-string, string|array<array-key, string|array<array-key, mixed>>> $inputs
     * @param  Success[]  $successActions
     * @param  Failure[]  $failureActions
     * @param  array<array-key, string>  $outputs
     * @param  Parameter[]  $parameters
     * @param  list<string>  $dependsOn
     */
    public function __construct(
        public string $workflowId,
        public array $steps,
        public ?string $summary,
        public ?string $description,
        public array $inputs = [],
        public array $successActions = [],
        public array $failureActions = [],
        public array $outputs = [],
        public array $parameters = [],
        public array $dependsOn = [],
    ) {}
}

With the help of DocBlocks and PHPStan we have a much better idea of what can be expected within the $inputs or $outputs properties of a Workflow, but I can already foresee some difficulties in the next step of tooling I plan to build off of this package. But these cases will improve over time, not only as I start to build my own tools and run into "gotchas" or rough edges around the API, but hopefully as others start to use it as well.

Anticipated challenges include resolving complex workflow dependencies and managing nested workflow steps.

Whats next?#

I have a lot of ideas and plans for Arazzo, but I can't really commit to when I'll start producing more tooling. Hopefully by sharing my ideas it'll inspire others with more time to take the charge and build something great. I work in and live within the Laravel side of the PHP Ecosystem so a lot of my ideas will be related to it:

  • Arazzo to Blade components
  • Arazzo to Livewire components
  • Pest PHP plugin for automated test

And my most ambitious idea so far:

  • An API First Laravel starter kit, based on OpenAPI with an Arazzo module and more for the built in developer portal.

I'm looking forward to what the future holds as the OpenAPI Initiative expands outwards into defining more than just RESTful APIs, but how to use them to create whole new experiences be it new products, or LLM powered Agentic experiences. Contributions and collaboration on these tools are encouraged and greatly appreciated to accelerate development.

Questions? Let's chatOPEN DISCORD
0members online

Designed for Developers, Made for the Edge