diff --git a/src/_data/guides.yml b/src/_data/guides.yml index d43ae081..9554f9cb 100644 --- a/src/_data/guides.yml +++ b/src/_data/guides.yml @@ -16,6 +16,10 @@ categories: long_name: OpenAPI specification guides and tutorials description: Learn how to use OpenAPI to build HTTP APIs that humans and machines can interact with conveniently. + - name: Arazzo + long_name: Arazzo specification guides and tutorials + description: Learn how to use Arazzo to document, test, and execute API workflows that orchestrate multiple operations into meaningful tasks. + - name: AsyncAPI long_name: AsyncAPI specification guides and tutorials description: Learn how to use AsyncAPI to build event-driven APIs that humans and machines can interact with conveniently. diff --git a/src/_data/navbar.yml b/src/_data/navbar.yml index 47175162..050312c2 100644 --- a/src/_data/navbar.yml +++ b/src/_data/navbar.yml @@ -3,6 +3,7 @@ left_links: - {title: "Product Updates", link: "/product-updates"} - {title: "Guides", link: "/guides"} - {title: "OpenAPI Specification", link: "/guides/openapi/specification/v3.2/introduction/what-is-openapi"} + - {title: "Arazzo Specification", link: "/guides/arazzo/specification/v1.0/introduction/what-is-arazzo"} right_links: - {title: "API Reference", link: "https://developers.bump.sh"} - {title: "Bump.sh", link: "https://bump.sh"} diff --git a/src/_data/sidebars/guides/arazzo/v1-0.yml b/src/_data/sidebars/guides/arazzo/v1-0.yml new file mode 100644 index 00000000..296f9688 --- /dev/null +++ b/src/_data/sidebars/guides/arazzo/v1-0.yml @@ -0,0 +1,38 @@ +collection_name: guides +root: /guides/arazzo/specification/v1.0 +resources: + - type: category + label: Introduction to Arazzo + items: + - label: What is Arazzo? + link: /introduction/what-is-arazzo/ + - label: History and Evolution of Arazzo + link: /introduction/history/ + - label: Benefits of Using Arazzo + link: /introduction/benefits/ + + - type: category + label: Understanding Arazzo Structure + items: + - label: Basic Structure + link: /understanding-structure/basic-structure/ + - label: Defining Sources + link: /understanding-structure/defining-sources/ + - label: Workflows + link: /understanding-structure/workflows/ + - label: Steps, Inputs, and Outputs + link: /understanding-structure/steps-inputs-outputs/ + - label: Success and Failure + link: /understanding-structure/success-and-failure/ + - label: Components & References + link: /understanding-structure/components-and-references/ + + - type: category + label: Working with Arazzo + items: + - label: Runtime Expressions + link: /working-with-arazzo/runtime-expressions/ + - label: Workflow Documentation + link: /working-with-arazzo/workflow-documentation/ + - label: API Testing + link: /working-with-arazzo/api-testing/ diff --git a/src/_guides/arazzo/_defaults.yml b/src/_guides/arazzo/_defaults.yml new file mode 100644 index 00000000..3e3831df --- /dev/null +++ b/src/_guides/arazzo/_defaults.yml @@ -0,0 +1,9 @@ +layout: documentation +page_class: documentation +sidebar_title: Arazzo Specification +skip_listing: true +display_authors: true +display_pagination: true +display_cta: true +exclude_from_pagination: true +slug: arazzo-specification diff --git a/src/_guides/arazzo/specification/_defaults.yml b/src/_guides/arazzo/specification/_defaults.yml new file mode 100644 index 00000000..3e3831df --- /dev/null +++ b/src/_guides/arazzo/specification/_defaults.yml @@ -0,0 +1,9 @@ +layout: documentation +page_class: documentation +sidebar_title: Arazzo Specification +skip_listing: true +display_authors: true +display_pagination: true +display_cta: true +exclude_from_pagination: true +slug: arazzo-specification diff --git a/src/_guides/arazzo/specification/v1.0/_defaults.yml b/src/_guides/arazzo/specification/v1.0/_defaults.yml new file mode 100644 index 00000000..ebc4f578 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/_defaults.yml @@ -0,0 +1,2 @@ +sidebar_version: "v1.0" +sidebar_name: arazzo_v1-0 diff --git a/src/_guides/arazzo/specification/v1.0/introduction/benefits.md b/src/_guides/arazzo/specification/v1.0/introduction/benefits.md new file mode 100644 index 00000000..aa86c753 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/introduction/benefits.md @@ -0,0 +1,246 @@ +--- +title: Benefits of Using Arazzo +authors: phil +excerpt: Discover the advantages of adopting the Arazzo Specification for documenting, testing, and managing API workflows to benefit humans and agentic-AI. +date: 2025-01-26 +--- + +- TOC +{:toc} + +HTTP/REST APIs generally are not just data stores, they usually act more like state machines moving users/clients through various workflows. Despite this, most APIs are designed, documented, and tested as if every request is independent of everything else. + +REST APIs specifically attempted to solve this problem through HATEOAS (Hypermedia As The Engine Of Application State) being a core principle allowing clients to navigate through application states via links provided in responses. The trend never really caught on at large, but the problem of helping clients navigate through APIs has never been fully solved. This has lead to API clients generally stumbling around in the dark, hoping there is some manually maintained documentation explaining how to move from one state to another, with the occasional code sample thrown in to help. Some of this might even be up-to-date. + +In a world where agentic-AI is becoming more prevalent, this problem is only getting worse. Instead of human developers stumbling around in the dark, we now have AI agents fumbling around trying to figure out how to use APIs to accomplish tasks on behalf of users. These agents need clear, unambiguous instructions on how to navigate through API workflows, including handling errors and edge cases. + +Defining these workflows outside of the API itself helps provide clarity to this classic problem, avoiding confusing the client by shoving state controls into the runtime data models and headers. Instead they can be defined in a separate document that references the API definitions, describing how to use them together to accomplish real-world tasks. + +The Arazzo Specification provides a standardized way to define these API workflows, making them machine-readable and executable, and the way it extends OpenAPI means the schemas are right there for humans and agents to work with, with the schema and validation rules helping to cut down on hallucinations. + +Using Arazzo not only solves the documentation problem, but also drastically improves the process of design, governance, end to end testing, chaos testing, and all sorts of operational challenges like monitoring and health checks. + +## Documentation Benefits + +An API client needs to do a thing: book a ticket, onboard a user, move an order from "draft" to "paid", or recover when something fails halfway through. + +There probably is not a `POST /moveOrderFromDraftToPaid` endpoint (and if there is there shouldn't be). Instead there are a series of steps that need to be taken in order, with data flowing from one step to the next, and various failure modes that need to be handled along the way. + +Arazzo can be used to capture the journey step-by-step, with the same kind of clarity OpenAPI brought to individual operations. When you write a workflow with good descriptions and sensible outputs, you've effectively created a living "how to use this API" guide that tools can render, validate, and even execute. + +Here's what that kind of story looks like, at a high level: + +```mermaid +sequenceDiagram + autonumber + participant Client + participant API + + Client->>API: Search trips + API-->>Client: trips[] (includes tripId) + Client->>API: Create booking (tripId) + API-->>Client: bookingId + Client->>API: Pay for booking (bookingId) + API-->>Client: confirmation + ticket +``` + +And here's the same idea expressed as an Arazzo workflow snippet. Ignore the syntax of some of the runtime expressions for now, the key part is how the steps flow together with outputs from one step being used as inputs to the next: + +```yaml +workflows: + - workflowId: complete-booking-flow + summary: Test the full booking process + steps: + - stepId: search + operationId: search-trips + successCriteria: + - condition: $statusCode == 200 + - condition: $response.body.trips.length > 0 + outputs: + tripId: $response.body#/trips/0/id + + - stepId: book + operationId: create-booking + requestBody: + payload: + tripId: $steps.search.outputs.tripId + successCriteria: + - condition: $statusCode == 201 + - condition: $response.body.status == 'confirmed' +``` + +If you've ever maintained getting started documentation, you'll know the pain: they're the first thing users read and the first thing that goes stale. Arazzo helps because you can treat workflows as the canonical version of those journeys, then generate docs and examples from the same source. + +Common workflows could involve: + +- log in +- getting a user id number +- using that user id to find resources +- creating new resources +- knowing if that worked or failed + +It seems like it should be pretty standard stuff, but every API is different and these journeys are often surprisingly confusing to navigate without clear documentation that's definitely up-to-date. + +## Testing Benefits + +Once workflows are written down in a machine-readable format, testing stops being a separate project. The workflow itself becomes the test: run it against staging in CI, run it as a smoke test before deploy, or run it periodically as monitoring. + +Where Arazzo gets especially interesting is state. Many APIs behave like state machines, but they're documented as if every request is independent. Arazzo gives you a place to express state transitions as something deliberate and testable. + +Here's the idea in the abstract: + +```mermaid +stateDiagram-v2 + [*] --> draft + draft --> submitted: submit + submitted --> processing: pay + processing --> shipped: ship + shipped --> delivered: confirmDelivery + delivered --> [*] + + draft --> cancelled: cancel + submitted --> cancelled: cancel + processing --> cancelled: cancel +``` + +And here's what that looks like when you're checking the transitions with actual API calls: + +```yaml +workflows: + - workflowId: order-lifecycle + summary: Complete lifecycle of an order from draft to delivered + + steps: + - stepId: createDraft + description: Create order in draft state + operationId: createOrder + successCriteria: + - condition: $response.body.state == 'draft' + + - stepId: submitOrder + description: Transition from draft to submitted + operationId: submitOrder + parameters: + - name: orderId + value: $steps.createDraft.outputs.orderId + successCriteria: + - condition: $response.body.state == 'submitted' + + - stepId: processPayment + description: Process payment, moving to processing state + operationId: processPayment + successCriteria: + - condition: $response.body.state == 'processing' + - condition: $response.body.paymentStatus == 'paid' + + - stepId: ship + description: Ship the order + operationId: shipOrder + successCriteria: + - condition: $response.body.state == 'shipped' + + - stepId: confirmDelivery + description: Final state transition to delivered + operationId: confirmDelivery + successCriteria: + - condition: $response.body.state == 'delivered' +``` + +Instead of being locked away in some cloud testing environment or hidden QA repository for only a select few to see, the criteria for success and failure become easily visible and knowable to everyone. + +New team members can read the workflow and understand the valid transitions. QA can execute the same workflow as an end-to-end test. When a rule is changed (say you add a "refunded" state), you update one workflow and let tooling catch the places that no longer match. This can be done along with the code changes in a single pull request, making reviews and validation much simpler. + +You can also handle real-world edge cases without turning your docs into a wall of prose. For example: "cancel is allowed in three states, but if it's already shipped you need to start a refund flow instead". + +```yaml +- stepId: cancelOrder + description: Cancel order - allowed from draft, submitted, or processing states + operationId: cancelOrder + successCriteria: + - condition: $statusCode == 200 + - condition: $response.body.state == 'cancelled' + + # This step can be reached from multiple prior states + dependsOn: + - createDraft + - submitOrder + - processPayment + + # Only execute if certain conditions are met + onFailure: + - name: orderAlreadyShipped + type: goto + stepId: refundProcess + criteria: + - condition: $response.body.reason == 'ORDER_SHIPPED' +``` + +## Design, Governance, and Operations + +Once you've got a handful of core workflows, they naturally become a contract for how the API should be used. That helps during design reviews. Can a customer actually complete checkout with this API? Did a recent change break a critical journey? + +Operationally, workflows are useful as smoke tests and as synthetic monitoring. This is different from a classic "is the service up?" health check: you're not trying to prove the database is reachable, you're trying to prove the product still works. + +If the "signup → create resource → view resource" journey is broken, the API might still be returning 200s all day long, but users are stuck. Arazzo lets you encode that journey once and run it on a schedule (either on a testing environment, sandbox, or production if you're careful). + +Here's a compact example of a synthetic canary workflow you might run in CI or periodically in a monitoring job: + +```yaml +workflows: + - workflowId: canary-happy-path + summary: Validate a critical user journey end-to-end + steps: + - stepId: authenticate + operationId: getToken + - stepId: createResource + operationId: createItem + - stepId: readBack + operationId: getItem + - stepId: cleanup + operationId: deleteItem +``` + +Even if you never generate a fancy visualization as documentation, the day-to-day payoff is simple: onboarding is faster when people can run the workflow and watch it work; integration is smoother when the happy path is explicit; and breakages are easier to spot because failures show up as a specific step, with a specific condition that didn't match. + +## Cross-API Orchestration + +While OpenAPI describes a single API, Arazzo can orchestrate across multiple APIs by referencing multiple source descriptions. This is especially useful for documenting and testing business processes that span different APIs or services. + +```yaml +sourceDescriptions: + - name: paymentApi + url: ./payment-api.yaml + - name: inventoryApi + url: ./inventory-api.yaml + - name: shippingApi + url: ./shipping-api.yaml + +workflows: + - workflowId: complete-order + steps: + - stepId: reserveInventory + operationId: $sourceDescriptions.inventoryApi.reserve + + - stepId: processPayment + operationId: $sourceDescriptions.paymentApi.charge + + - stepId: scheduleShipping + operationId: $sourceDescriptions.shippingApi.schedule + onFailure: + - name: releaseInventory + type: goto + stepId: rollbackInventory +``` + +Notice the difference in `operationId` syntax. Instead of referencing just the `operationId` (e.g.; `reserve`), a longer runtime expression is used which references the API in the context of its source: `$sourceDescriptions.inventoryApi.reserve`. + +This is where Arazzo starts to feel more like CI/CD workflows, but for business journeys. Stitching together operations that live in different services, passing data between them, and defining what "success" means for the whole sequence in a single source of truth. + +## Business Value + +Beyond making sure an API ecosystem is actually functioning properly (with fewer partial sources of truth to disagree with each other) most of the business value is second-order effects. + +- Properly documented flows lead to fewer support requests from confused client developers struggling to integrate. +- Reduced chances of AI agents flailing around hallucinating incorrect API usage. +- More confidence when you ship changes because you can run the same workflows as regression tests. +- All of that saves time and money. diff --git a/src/_guides/arazzo/specification/v1.0/introduction/history.md b/src/_guides/arazzo/specification/v1.0/introduction/history.md new file mode 100644 index 00000000..2ea95117 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/introduction/history.md @@ -0,0 +1,57 @@ +--- +title: History and Evolution of Arazzo +authors: phil +excerpt: Learn about the origins of Arazzo, from early workflow experiments to becoming an official OpenAPI Initiative standard for API workflow descriptions. +date: 2025-01-24 +--- + +- TOC +{:toc} + +The Arazzo Specification represents years of evolution in API workflow documentation, emerging from real-world needs and community collaboration to become an official standard under the OpenAPI Initiative. + +## The Birth of Arazzo + +The first time Arazzo came up was on an OpenAPI community call somewhere in late 2021 where a bunch of the usual OpenAPI contributors were discussing the Special Interest Groups that could be formed under the OpenAPI Initiative. Along with groups for Overlays, Security, Travel, etc. one of the groups was Workflows. The idea was to explore how to standardize the way API workflows were defined. + +There had been some loose atempts to do this with OpenAPI's `Links` where one operation can point to next potential steps, but this was not being utilized much and seemed problematic for trying to document complex workflows across multiple APIs. + +Popular open-source tools like [Strest](https://github.com/eykrehbein/strest) immediately jumped to mind for many of us, and there was more inspiration pulled from all sorts of similar concepts: + +- GitHub Actions and other CI/CD workflow formats (steps, conditions, outputs). +- State machine concepts from computer science. +- Real-world API integration patterns observed in the field. + +Generally the folks involved were well used to working with these sorts of tools, building and maintaining these sorts of tools, and had seen the pain points of trying to document workflows in written guides or sample codebases that quickly became outdated, so everyone got some good ideas into the mix. + +In 2022 a formal proposal was drafted and presented to the OpenAPI Initiative, and a working group was formed to develop the specification further. This group included API designers, technical writers, tool developers, and other stakeholders who contributed their expertise to shape the emerging standard. + +The specification was initially called "OpenAPI Workflows" but was later renamed to **Arazzo** to give it a distinct identity while maintaining its connection to the OpenAPI ecosystem. The name "Arazzo" (Italian for "tapestry") reflects how workflows weave together multiple API calls into a cohesive pattern. + +### Version 1.0 Release (2024) + +After extensive community review, tooling experiments, and real-world testing, Arazzo 1.0.0 was officially released in 2024 as a standard under the OpenAPI Initiative. This first version includes: + +- **Core workflow structure** - Documents, workflows, and steps +- **Source descriptions** - Referencing OpenAPI and other API definitions +- **Runtime expressions** - Passing data between steps +- **Success and failure criteria** - Defining what constitutes success or failure +- **Parameters and request bodies** - Overriding or supplementing source definitions +- **Components and reusability** - Reducing duplication through references +- **Outputs** - Capturing and passing results between steps + +## Adoption and Ecosystem + +Arazzo has seen growing interest across the API community with most tooling vendors busily working on adding support, with the first two being Spectral and Speakeasy. The list of Arazzo-compatible tools continues to grow, and for the most up-to-date list check [OpenAPI.Tools: Arazzo](https://openapi.tools/collections/arazzo). + +## The Future + +Arazzo 1.1 is already underway with non-breaking improvements potentially including: + +- Support for JSONPath as well as JSON Pointer and XPath. +- Supporting AsyncAPI for event-driven workflows. +- Allow workflow inputs to be passed in success and failure actions. + +These iterative improvements aim to bring more flexibility and power to the specification while maintaining backward compatibility, whilst larger improvements are already being planned for a longer term Arazzo 2.0 release. + +If you're interested in contributing or learning more, check out the [Arazzo GitHub repository](https://github.com/OAI/Arazzo-Specification/). diff --git a/src/_guides/arazzo/specification/v1.0/introduction/what-is-arazzo.md b/src/_guides/arazzo/specification/v1.0/introduction/what-is-arazzo.md new file mode 100644 index 00000000..032bad39 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/introduction/what-is-arazzo.md @@ -0,0 +1,137 @@ +--- +title: What is Arazzo? +authors: phil +excerpt: Arazzo is the standard format for describing API workflows - sequences of API calls that work together to accomplish complex tasks, with built-in error handling and state management. +date: 2025-01-23 +--- + +- TOC +{:toc} + +Arazzo (the "Arazzo Specification") is a standard format for describing API workflows. Managed by the [OpenAPI Initiative](https://www.openapis.org/) (OAI), the folks that brought you OpenAPI. Arazzo provides a way to define sequences of API calls that work together to accomplish workflows. + +While OpenAPI describes what an API can do in any given interaction, Arazzo describes how those interactions can be chained together to achieve real-world goals, like finding the right train ticket, picking a seat, and booking it. + +## Previous Approaches to API Workflows + +For years OpenAPI has made "API reference documentation" considerably better, going beyond the hand-crafted written stuff that rarely had any useful details and was usually out of date. API clients are able to integrate with APIs much quicker, and it's helped at every stage of the API lifecycle from design, testing, monitoring, and back to designing new versions again. + +This API reference documentation provides detailed information about specific bits of the API's interface, like all the endpoints, the parameters, request & response bodies, status codes, content types, etc. That's really handy stuff, even more so if there are no SDKs built for an API, but it does not help anyone understand how to actually use these API endpoints together to accomplish a real task. + +Some HTTP APIs are treated like a data source, with CRUD operations being completely independent of each other, but REST APIs are most commonly used to define workflows. Using our [Train Travel API](https://bump.sh/bump-examples/doc/train-travel-api/) example, a typical user journey to book a ticket could involve a whole bunch of steps: + +1. Search for stations in a particular area. +2. See available trips between origin and destination stations, on a certain date, and with a dog! +3. Pick a seat on that train. +4. Create a booking for that trip and seat. +5. Pay for the booking. +6. Get a ticket issued for the paid booking. + +Each step depends on data from previous steps (like available trips depend on the chosen stations and date), and there are many ways things can go wrong (no trips found, seat already booked, payment failed, etc). + +Until Arazzo, most of the advice for teams maintaining API documentation was to document these workflows using one of the following approaches: + +- **Written documentation** - Technical writers will create written explanations with examples of workflows, but these explanations and examples can quickly become outdated, and they cannot be automatically tested. +- **Sample codebases** - These are language specific which can cause confusion for developers not familiar with them, and cannot be reused for other automated workflows. +- **Proprietary implementations** - Tools like Postman and Readme offer proprietary ways to handle this which can lead to vendor lock-in, limiting integration potential, and are limited in expressing complex logic. +- **Custom scripts** - Useful for testing but not standardized or portable. + +All of these approaches will partially solve the problem for some aspect of what various teams need, but just like OpenAPI solved the problem of API contracts being rewritten over and over in different formats for different documentation, testing, and governance tools, a solution was needed to standardize API workflow documentation and reduce the repetition that was so prone to mistakes and mismatches. + +## Introducing Arazzo + +Arazzo was designed to solve all these problems by creating a standardized, machine-readable, vendor-neutral description format to describe API workflows. Instead of picking a single problem sector (e.g. documentation) they brought in authors and contributors from a variety of backgrounds to create a specification that can be used for any part of the API lifecycle. + +Much like OpenAPI did for endpoint documentation, Arazzo standardizes how we describe the multi-step journeys so that tooling can parse them, validate them, and even visualize them. It also fits neatly into the ecosystem you already have, because Arazzo doesn’t replace OpenAPI: it extends it by using OpenAPI documents as sources for the API operations your workflows call. + +```yaml +arazzo: 1.0.0 +info: + title: Train Travel API - Book & Pay + version: 1.0.0 +sourceDescriptions: + - name: train-travel + url: ./openapi.yaml + type: openapi +workflows: + - ... +``` + +These workflow steps reference "operations" (API calls) from those sources. + +```yaml +workflows: + - workflowId: book-trip + summary: Find train trips to book between origin and destination stations. + inputs: + $ref: "#/components/inputs/book_trip_input" + steps: + - stepId: book-trip + operationId: create-booking +``` + +To turn this into brilliant documentation it needs some descriptions, which tools can use to generate user-friendly guides. Beyond that, the best part of this workflow documentation is visualizing the messiest part of real integrations: carrying values from one step to the next. + +Arazzo is designed to allow inputs and outputs from each step to be clearly described and passed between each other, then branching based on what happened, deciding success or failure based on more than "did I get a 2xx?", and handling retries or recovery paths when something goes wrong. + +Because the whole thing is structured, the workflows can be executed. That means the same file can power testing and automation, not just documentation. And once the documentation is executable there is no way for documentation and implementation to drift out of sync, because you can run the workflows as tests to verify everything still works as expected on every single pull request or commit to main branch. + +## What Arazzo Looks Like + +Here's a simple example of an Arazzo workflow for searching and booking a train: + +```yaml +arazzo: 1.0.1 +info: + version: 1.0.0 + +sourceDescriptions: + - name: train-travel + url: ./openapi.yaml + type: openapi + +workflows: + - workflowId: book-train-ticket + summary: Search for and book a train ticket + steps: + - stepId: search + operationId: search-trips + parameters: + - name: origin + in: query + value: $inputs.origin + - name: destination + in: query + value: $inputs.destination + successCriteria: + - condition: $statusCode == 200 + outputs: + tripId: $response.body#/trips/0/id + + - stepId: createBooking + operationId: create-booking + requestBody: + contentType: application/json + payload: + tripId: $steps.search.outputs.tripId + passengers: $inputs.passengers + successCriteria: + - condition: $statusCode == 201 + outputs: + bookingId: $response.body#/id +``` + +This workflow defines two steps: searching for trips and creating a booking. It specifies how to pass parameters and handle outputs between steps, so it's clear which bits come from where, and clearly defining what success looks like: e.g: this API uses a `201 Created` status code, where some poorly designed APIs might use `200 OK` for everything, or a `202 Accepted` for async operations. + +This clarity allows tools to generate accurate documentation, run tests, and even automate these workflows reliably. + +## Who Benefits from Arazzo? + +- **API Designers** - Document intended usage patterns during the design phase +- **Backend Developers** - Validate that implementations support the designed workflows +- **QA Engineers** - Use workflows as integration test suites +- **Technical Writers** - Generate accurate workflow documentation automatically +- **API Consumers** - Understand how to accomplish real tasks, not just individual calls +- **DevOps Teams** - Use workflows for smoke tests and monitoring + +Arazzo brings the same revolution to API workflow documentation that OpenAPI brought to API reference documentation: a single source of truth that serves multiple purposes throughout the API lifecycle. diff --git a/src/_guides/arazzo/specification/v1.0/understanding-structure/basic-structure.md b/src/_guides/arazzo/specification/v1.0/understanding-structure/basic-structure.md new file mode 100644 index 00000000..abe09bb1 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/understanding-structure/basic-structure.md @@ -0,0 +1,284 @@ +--- +title: Basic Structure +authors: phil +excerpt: Learn your way around an Arazzo document for describing HTTP workflows. +date: 2025-01-27 +--- + +- TOC +{:toc} + +The [Arazzo Specification](https://spec.openapis.org/arazzo/v1.0.1.html) defines how exactly an Arazzo document should be laid out, and understanding this structure is essential for creating effective HTTP workflow documentation. + +## The Arazzo Document + +An Arazzo document is written as YAML or JSON. In a simple world you would typically name it `arazzo.yaml` or `arazzo.json`, but you can use any name you prefer. + +Let's go through the main sections in an Arazzo document: + +1. **Arazzo Version** +2. **Info Object** +3. **Source Descriptions** +4. **Workflows** +5. **Components** + +Let's examine each of these in detail. + +## 1. Arazzo Version + +The root of every Arazzo document must specify the version of the Arazzo Specification being used: + +```yaml +arazzo: 1.0.1 +``` + +The `arazzo` field is required, and tells tooling which version of the specification to use when parsing the document. The latest version at time of writing is `1.0.1`, but there is no semantic difference between `1.0.0` and `1.0.1`. It is just good practice to use the latest patch version. + +## 2. Info Object + +The `info` object provides metadata about the workflow document: + +```yaml +info: + title: Train Travel Workflows + version: 1.0.0 + summary: Common workflows for the train ticket API + description: | + Workflows for working with the Train Travel API, covering + searching for trips, making bookings, and managing tickets. +``` + +**Required fields:** + +- `title` - A human-readable name for your workflow document +- `version` - The version of this workflow document (independent of API version or Arazzo spec version) + +**Optional fields:** + +- `summary` - A short summary of what these workflows accomplish +- `description` - A longer description, supports CommonMark (Markdown) + +The `info` object is similar to the OpenAPI `info` object, but describes the workflows rather than an API. + +## 3. Source Descriptions + +The `sourceDescriptions` array references the API definitions that workflows will use, which will often just be a single `openapi` document but could be multiple APIs or even other Arazzo documents. + +```yaml +sourceDescriptions: + - name: trainApi + url: https://api.example.com/openapi.yaml + type: openapi + + - name: paymentApi + url: ./payment-api.json + type: openapi +``` + +Each source description has the following fields: + +- `name` - An identifier used to reference APIs when multiple sources are used in a workflow. Some people prefer camelCase (e.g.; `trainApi`), because these will be used in runtime expressions later on. +- `url` - Location of the API description, could be a URL or relative file path. +- `type` - The type of API description (`openapi` to reference an API, or `arazzo` to reference another Arazzo document). + +**Learn more about [source descriptions](_guides/arazzo/specification/v1.0/understanding-structure/defining-sources.md).** + +## 4. Workflows + +The `workflows` array is where the magic really happens. Each workflow describes a sequence of steps to accomplish a given task: + +```yaml +workflows: + - workflowId: book-train-ticket + summary: Complete workflow for booking a train ticket + description: Searches for trips, selects one, and creates a booking + + inputs: + type: object + properties: + origin: + type: string + destination: + type: string + passengers: + type: array + + steps: + - stepId: search + description: Search for available trips + operationId: $sourceDescriptions.trainApi.searchTrips + parameters: + - name: origin + in: query + value: $inputs.origin + - name: destination + in: query + value: $inputs.destination + + successCriteria: + - condition: $statusCode == 200 + + outputs: + tripId: $response.body#/trips/0/id + + - stepId: createBooking + description: Create booking for the selected trip + operationId: $sourceDescriptions.trainApi.createBooking + requestBody: + contentType: application/json + payload: + tripId: $steps.search.outputs.tripId + passengers: $inputs.passengers + + successCriteria: + - condition: $statusCode == 201 + + outputs: + bookingId: $response.body#/id +``` + +Each workflow contains: + +**Workflow Metadata:** + +- `workflowId` (**required**) - Unique identifier for this workflow. +- `summary` - A shorter description (used as a title). +- `description` - Longer form description, with CommonMark (Markdown) support. +- `inputs` - JSON Schema defining what inputs the workflow accepts. +- `outputs` - What outputs the workflow produces. + +**Steps Array:** + +- `stepId` (**required**) - Unique identifier for this step within the workflow. +- `operationId` - Reference to an API operation to call. +- `parameters` - Parameters to pass to the operation. +- `requestBody` - Request body to send. +- `successCriteria` - Conditions that define success. +- `onSuccess` / `onFailure` - Actions to take based on results. +- `outputs` - Values to extract from the response. + +**Learn more about [Workflows](_guides/arazzo/specification/v1.0/understanding-structure/workflows.md).** + +## 5. Components + +In order to cut down on repetition - which is tedious and to keep up to date, and a vector for making mistakes - commonly used chunks of Arazzo can be defined in the `components` object. + +Components can define: + +- `inputs` - Reusable input schemas. +- `parameters` - Reusable parameters. +- `successActions` - Reusable success actions. +- `failureActions` - Reusable failure actions. + +```yaml +components: + inputs: + pagination: + type: object + properties: + page: + type: integer + default: 1 + pageSize: + type: integer + default: 20 + + parameters: + authHeader: + name: Authorization + in: header + value: Bearer $inputs.token + + successActions: + logSuccess: + type: end + name: logSuccess + + failureActions: + logFailure: + type: end + name: logFailure +``` + +**Learn more about [components and referencing](_guides/arazzo/specification/v1.0/understanding-structure/components-and-references.md).** + +## Putting It All Together + +To see how this structure all works together, here's a complete (but very minimal) Arazzo document showing the key bits, including a couple of reusable components: + +```yaml +arazzo: 1.0.1 + +info: + title: Train Travel Workflows + version: 1.0.0 + summary: Common workflows for the train ticket API + +sourceDescriptions: + - name: trainApi + url: https://api.example.com/openapi.yaml + type: openapi + +components: + inputs: + bookTrainTicketInput: + type: object + required: [origin, destination, passengers, token] + properties: + origin: + type: string + destination: + type: string + passengers: + type: array + token: + type: string + +workflows: + - workflowId: book-train-ticket + summary: Complete workflow for booking a train ticket + + inputs: + $ref: '#/components/inputs/bookTrainTicketInput' + + steps: + - stepId: search + description: Search for available trips + operationId: $sourceDescriptions.trainApi.searchTrips + parameters: + - name: origin + in: query + value: $inputs.origin + - name: destination + in: query + value: $inputs.destination + successCriteria: + - condition: $statusCode == 200 + + outputs: + tripId: $response.body#/trips/0/id + + - stepId: createBooking + description: Create booking for the selected trip + operationId: $sourceDescriptions.trainApi.createBooking + requestBody: + contentType: application/json + payload: + tripId: $steps.search.outputs.tripId + passengers: $inputs.passengers + successCriteria: + - condition: $statusCode == 201 + + outputs: + bookingId: $response.body#/id +``` + +## Next Steps + +Now that you understand the basic structure, if you'd like to learn more about any of the sections in particular you can take a look at these guides. + +- [Defining Sources](_guides/arazzo/specification/v1.0/understanding-structure/defining-sources.md) - How to reference API definitions +- [Workflows](_guides/arazzo/specification/v1.0/understanding-structure/workflows.md) - Creating workflow sequences +- [Steps, Inputs, and Outputs](_guides/arazzo/specification/v1.0/understanding-structure/steps-inputs-outputs.md) - Working with data flow +- [Success and Failure](_guides/arazzo/specification/v1.0/understanding-structure/success-and-failure.md) - Handling outcomes +- [Components & References](_guides/arazzo/specification/v1.0/understanding-structure/components-and-references.md) - Reusing definitions diff --git a/src/_guides/arazzo/specification/v1.0/understanding-structure/components-and-references.md b/src/_guides/arazzo/specification/v1.0/understanding-structure/components-and-references.md new file mode 100644 index 00000000..b1208d08 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/understanding-structure/components-and-references.md @@ -0,0 +1,339 @@ +--- +title: Components & References +authors: phil +excerpt: Learn how to use components and references in Arazzo to reduce repetition and build maintainable workflows. +date: 2025-01-23 +--- + +- TOC +{:toc} + +Arazzo helps teams and tools not waste time and effort writing and parsing the same stuff over and over again. By defining reusable components just once, they can be reused in various parts of the workflow. This not only makes it quicker to write them and cuts down on the maintenance burden, but far more importantly it drastically reduces the chance of human error that can cause inconsistencies. + +Reuse in Arazzo is built upon two concepts: the `components` object, and the `reference` keyword. + +## The Components Object + +The `components` object at the root of your Arazzo document contains reusable definitions: + +```yaml +arazzo: 1.0.1 +info: + title: My Workflows + version: 1.0.0 + +components: + inputs: + # Reusable input schemas + + parameters: + # Reusable parameters + + successActions: + # Reusable success actions + + failureActions: + # Reusable failure actions + +workflows: + # ... workflows can reference components +``` + +## Component Types + +Arazzo supports several component types, each designed to reduce repetition and make workflows easier to maintain. + +```yaml +components: + inputs: + pagination: + type: object + properties: + page: + type: integer + minimum: 1 + default: 1 + pageSize: + type: integer + minimum: 1 + maximum: 100 + default: 20 + + dateRange: + type: object + required: [startDate, endDate] + properties: + startDate: + type: string + format: date + endDate: + type: string + format: date + + authentication: + type: object + required: [username, password] + properties: + username: + type: string + password: + type: string + format: password +``` + +This example defines three input objects, named `pagination`, `dateRange`, and `authentication`. The latter two have required properties, whilst the former has only optional properties that can helpfully define defaults using the JSON Schema `default` keyword. + +This is a good place to focus on human descriptions explaining what each input is for, and any constraints not covered by the schema itself. + +### Parameters + +Parameters bring an input value and introduce it to the world of HTTP. They define how to send data to the source API, whether in the path, query string, headers, or cookies. + +- `path` - Part of the URL itself, using OpenAPI-style path templating. For example, in `/items/{itemId}`, the path parameter is `itemId`. +- `query` - Appended to the URL as a query string, like `/items?id=123`. +- `header` - Sent as an HTTP header. Header field names are case-insensitive (see [RFC 9110: Field Names](https://httpwg.org/specs/rfc9110.html#rfc.section.5.1)). +- `cookie` - Sent as a cookie value to the source API. + +A parameter is considered unique by the combination of its `name` and `in` fields, so give it a sensible name and don't have two parameters with the same name in the same location. + +```yaml +components: + parameters: + authHeader: + name: Authorization + in: header + value: Bearer $inputs.token + + apiVersion: + name: API-Version + in: header + value: "2024-01" + + requestId: + name: X-Request-ID + in: header + value: $inputs.requestId + + acceptJson: + name: Accept + in: header + value: application/json +``` + +### Success Actions + +Success actions define reusable actions to take when a step completes successfully. + +Successful actions have two possible outcomes, they can either end the workflow there, or they can go to another step. + +Whichever the outcome, defining criteria is an optional way to decide if that outcome should be taken. The criteria objects rely on runtime expressions, and can be paired with Regex, JSONPath, JSON Pointers, or XPath for even more advanced logic. + +```yaml +components: + successActions: + paymentPending: + name: paymentPending + type: goto + stepId: getBooking + criteria: + - condition: $response.body.status == 'pending' + + paymentSucceeded: + name: paymentSucceeded + type: end + criteria: + - context: $response.body + condition: $.status == 'succeeded' + type: jsonpath +``` + +Both of those conditions are actually the same but using different expression types. The first uses a simple expression, while the second uses JSONPath to extract the status from the response body. + +Learn more about success and failure actions in the [Success and Failure](_guides/arazzo/specification/v1.0/understanding-structure/success-and-failure.md) guide. + +### Failure Actions + +Failure actions define reusable actions to take when a step fails. + +```yaml +components: + failureActions: + retryOnServerError: + name: serverErrorRetry + type: retry + retryAfter: 5 + retryLimit: 3 + criteria: + - condition: $statusCode >= 500 + + handleRateLimit: + name: rateLimited + type: retry + retryAfter: 60 + retryLimit: 5 + criteria: + - condition: $statusCode == 429 + + logAndEnd: + name: logFailure + type: goto + stepId: logError +``` + +## Referencing Components + +Now that these components are defined, they can be referenced using the `reference` keyword. + +```yaml +components: + parameters: + authHeader: + name: Authorization + in: header + value: Bearer $inputs.token + +workflows: + - workflowId: myWorkflow + steps: + - stepId: authenticatedCall + operationId: $sourceDescriptions.api.getUser + parameters: + - reference: authHeader +``` + +Referenced parameters are pulled into the step at runtime, just as if they had been defined inline. + +You can even mix referenced and inline definitions. + +```yaml +steps: + - stepId: search + operationId: $sourceDescriptions.api.search + parameters: + - reference: authHeader + - name: query + in: query + value: $inputs.searchTerm + - name: limit + in: query + value: 20 +``` + +Here the `authHeader` parameter is reused as-is, while `query` and `limit` are defined inline for this one step. + +## Referencing Inputs + +Inputs are a little different than the rest of Arazzo in that they are defined entirely using JSON Schema. This means there is little space for the `reference` keyword, but that's no trouble as JSON Schema comes with its own `$ref` keyword. + +While JSON Schema composition keywords like `allOf` are valid, they are not always supported consistently by Arazzo tooling. In practice, the most widely supported pattern is to define full input schemas under `components.inputs`, then point `workflows[].inputs` at them with `$ref`. + +```yaml +components: + inputs: + pagination: + type: object + properties: + page: + type: integer + default: 1 + pageSize: + type: integer + default: 20 + +workflows: + - workflowId: listBookings + inputs: + type: object + properties: + pagination: + $ref: '#/components/inputs/pagination' + status: + type: string + enum: [pending, confirmed, cancelled] + + - workflowId: listUsers + inputs: + type: object + properties: + pagination: + $ref: '#/components/inputs/pagination' + role: + type: string + enum: [admin, user, guest] +``` + +Each input schema can define properties, and use `$ref` to pull in reusable input components mixed in with inline definitions for just that workflow. + +## Referencing Actions + +Actions are where `reference` really shines: define consistent behavior once, then attach it to any step using `onSuccess` or `onFailure`. + +### Success Actions + +Use a success action component when multiple steps should react the same way to success. + +For example, using the `paymentPending` and `paymentSucceeded` success actions defined earlier, you can attach both outcomes to a payment step. + +```yaml +workflows: + - workflowId: completeBooking + steps: + - stepId: payForBooking + operationId: $sourceDescriptions.api.createBookingPayment + onSuccess: + - reference: paymentPending + - reference: paymentSucceeded +``` + +Because each success action has criteria, you can attach both and let runtime data decide whether the workflow continues (`goto`) or completes (`end`). + +### Failure Actions + +Failure actions are often reused even more heavily, because retry logic and error routing tends to be consistent across many steps. + +```yaml +workflows: + - workflowId: completeBooking + steps: + - stepId: payForBooking + operationId: $sourceDescriptions.api.createBookingPayment + onFailure: + - reference: retryOnServerError + - reference: handleRateLimit + - reference: logAndEnd +``` + +This step reuses the failure actions from earlier: retry on transient server errors, retry more cautiously when rate limited, and then route to a logging step if it still fails. + +### Conditional Action Reuse + +Criteria let you attach multiple actions to the same step and still get different outcomes based on runtime data. + +```yaml +workflows: + - workflowId: completeBooking + steps: + - stepId: payForBooking + operationId: $sourceDescriptions.api.createBookingPayment + onSuccess: + - reference: paymentPending + - reference: paymentSucceeded + onFailure: + - reference: retryOnServerError + - reference: handleRateLimit +``` + +Because each action has criteria, you can attach them together without them all firing every time. + +## Summary + +Arazzo provides powerful reusability through: + +- **Components object** for shared definitions +- **reference** for Arazzo-style references (simple, readable) +- **External references** for cross-file reuse +- **Component types**: inputs, parameters, success actions, failure actions + +Inputs are JSON Schema, so you may see `$ref` in workflow inputs and inside input schemas. That's JSON Schema's own referencing mechanism and is separate from Arazzo's `reference` keyword. + +Effective use of components and references makes your Arazzo documents more maintainable, consistent, and easier to understand. Next, let's explore how workflows are executed at runtime. diff --git a/src/_guides/arazzo/specification/v1.0/understanding-structure/defining-sources.md b/src/_guides/arazzo/specification/v1.0/understanding-structure/defining-sources.md new file mode 100644 index 00000000..99c6ecec --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/understanding-structure/defining-sources.md @@ -0,0 +1,236 @@ +--- +title: Defining Sources +authors: phil +excerpt: Learn how to reference OpenAPI operations in Arazzo workflows using source descriptions. +date: 2025-01-30 +--- + +- TOC +{:toc} + +Source descriptions are how Arazzo gets a leg up pn understanding the APIs being worked with. To avoid repeating all the HTTP-level bits like endpoints, status codes, schema, etc. Arazzo just defines source description, then references operations by their `operationId`. Mostly these will be OpenAPI descriptions, but other Arazzo documents can also be used to reference workflows defined elsewhere, and soon AsyncAPI documents will be supported too. + +## The sourceDescriptions Array + +Source descriptions are described in the `sourceDescriptions` array at the root level, and there needs to be at least one: + +```yaml +arazzo: 1.0.1 +info: + title: My Workflows + version: 1.0.0 + +sourceDescriptions: + - name: trainApi + url: openapi.yaml + type: openapi + + - name: paymentGateway + url: https://api.example.com/payment-service.json + type: openapi + +workflows: + # ... workflows reference these sources +``` + +This array has the two APIs, one local OpenAPI document written in YAML and a payment service where the OpenAPI is hosted remotely, available as a JSON URl. That'll all work fine, and makes it easier to work across context boundaries where different teams might manage their own APIs and OpenAPI. + +## Source Description Fields + +Let's have a bit of a closer look at the fields that make up a source description. Each source description object has three required fields: + +```yaml +- name: myApi + url: ./path/to/api-description.yaml + type: openapi +``` + +### name (required) + +To avoid having to call up that API description by its full path or URL every time, or refer to it by name "Barry's Badly Named API v231.3-final" we give it a short name that can be reused more easily. These short names are used as variables when referencing operations from that source. + +```yaml +sourceDescriptions: + - name: bookingService + url: ./booking-api.yaml + type: openapi + +workflows: + - workflowId: book-ticket + steps: + - stepId: create + operationId: $sourceDescriptions.bookingService.createBooking +``` + +When there is only one source defined you can skip `$sourceDescriptions.bookingService.` and just use `createBooking` but when a second source is added the full reference is required to avoid ambiguity. + +### url (required) + +The location of the API description document. This can be a relative path: + +```yaml +url: ./openapi.yaml +url: ../apis/booking.yaml +url: ./specs/v2/openapi.json +``` + +Or it can be an absolute URL: + +```yaml +url: https://api.example.com/openapi.yaml +url: https://raw.githubusercontent.com/org/repo/main/openapi.json +``` + +### type (required) + +The type of API description format. Current valid values: + +- `openapi` - Supporting OpenAPI v3.x or v2.0 (formerly known as Swagger) +- `arazzo` - Reference other Arazzo documents. + +This can be used to mix and match different API description formats in the same Arazzo document: + +```yaml +sourceDescriptions: + - name: restApi + url: ./openapi.yaml + type: openapi + + - name: commonWorkflows + url: ./shared-workflows.yaml + type: arazzo +``` + +## Referencing Operations from Sources + +Once you've defined a source, you reference operations using the `operationId` with a runtime expression in the following format: `$sourceDescriptions.{name}.{operationId}`. + +```yaml +sourceDescriptions: + - name: trainApi + url: ./train-api.yaml + type: openapi + +workflows: + - workflowId: search-trains + steps: + - stepId: search + operationId: $sourceDescriptions.trainApi.searchTrips +``` + +The operation must exist in the referenced API definition with a matching `operationId`. + +### OpenAPI Operation IDs + +In your OpenAPI document: + +```yaml +# train-api.yaml +paths: + /trips: + get: + operationId: searchTrips # This is what Arazzo references + summary: Search for train trips + # ... +``` + +The `operationId` in OpenAPI should be unique across the entire API description. You'll also need to be cautious when renaming `operationId` or sunsetting old endpoints as it could break some workflows. + +## Multiple Sources + +Simple workflows might only need one API but when working on cross-API orchestration (e.g., microservices, third-party integrations) or writing end-to-end tests that cross multiple APIs/services, you'll likely need to define multiple sources: + +```yaml +sourceDescriptions: + - name: inventoryApi + url: https://inventory.example.com/openapi.yaml + type: openapi + + - name: paymentApi + url: https://payments.example.com/openapi.yaml + type: openapi + + - name: shippingApi + url: https://shipping.example.com/openapi.yaml + type: openapi + +workflows: + - workflowId: complete-order + summary: Orchestrate order across multiple services + steps: + - stepId: checkInventory + operationId: $sourceDescriptions.inventoryApi.checkStock + + - stepId: reserveItems + operationId: $sourceDescriptions.inventoryApi.reserveStock + + - stepId: processPayment + operationId: $sourceDescriptions.paymentApi.createCharge + + - stepId: scheduleShipping + operationId: $sourceDescriptions.shippingApi.createShipment + + - stepId: confirmOrder + operationId: $sourceDescriptions.inventoryApi.confirmReservation +``` + +This is powerful for orchestrating across multiple microservices, combining your API with external third-party integrations, and using multiple providers together in multi-vendor workflows. + +## Composing Arazzo Documents + +Arazzo documents can reference other Arazzo documents to compose workflows, enabling you to share common workflows across different teams or projects, or build complex workflows from simpler, reusable components. + +```yaml +# common-workflows.yaml +arazzo: 1.0.0 +info: + title: Common Workflows + version: 1.0.0 + +sourceDescriptions: + - name: authApi + url: ./auth-api.yaml + type: openapi + +workflows: + - workflowId: authenticate + summary: Standard authentication workflow + steps: + - stepId: getToken + operationId: $sourceDescriptions.authApi.createToken + outputs: + token: $response.body#/access_token +``` + +```yaml +# booking-workflows.yaml +arazzo: 1.0.0 +info: + title: Booking Workflows + version: 1.0.0 + +sourceDescriptions: + - name: bookingApi + url: ./booking-api.yaml + type: openapi + + - name: commonFlows + url: ./common-workflows.yaml + type: arazzo # Reference another Arazzo document + +workflows: + - workflowId: authenticated-booking + steps: + # Use a workflow from another Arazzo document + - stepId: auth + workflowId: $sourceDescriptions.commonFlows.authenticate + + - stepId: book + operationId: $sourceDescriptions.bookingApi.createBooking + parameters: + - name: Authorization + in: header + value: Bearer $steps.auth.outputs.token +``` + +This enables workflow reuse by sharing common workflows across documents, modularity by organizing workflows by domain or team, and composition by building complex workflows from simpler ones. diff --git a/src/_guides/arazzo/specification/v1.0/understanding-structure/steps-inputs-outputs.md b/src/_guides/arazzo/specification/v1.0/understanding-structure/steps-inputs-outputs.md new file mode 100644 index 00000000..38615064 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/understanding-structure/steps-inputs-outputs.md @@ -0,0 +1,581 @@ +--- +title: Steps, Inputs, and Outputs +authors: phil +excerpt: Master the building blocks of Arazzo workflows - steps that call operations, inputs that provide data, and outputs that capture results for subsequent steps. +date: 2025-01-31 +--- + +- TOC +{:toc} + +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: + +```yaml +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. + +```yaml +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. + +```yaml +- 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. + +```yaml +- stepId: getBooking + operationPath: /bookings/{bookingId} + method: get # Will also need to specify method +``` + +**workflowId** + +Execute another workflow. + +```yaml +- stepId: cancelBooking + workflowId: $sourceDescriptions.trainApi.cancelBookingWorkflow +``` + +### Optional Fields + +**description** +Explain what the step does in a way that is useful for human-readable documentation. + +```yaml +- 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. + +```yaml +- 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. + +```yaml +- 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. + +```yaml +- stepId: search + operationId: $sourceDescriptions.trainApi.searchTrips + successCriteria: + - condition: $statusCode == 200 + - condition: $response.body.trips.length > 0 +``` + +**onSuccess** / **onFailure** + +Actions to take based on outcome. + +```yaml +- stepId: checkAvailability + operationId: $sourceDescriptions.api.checkStock + successCriteria: + - condition: $response.body.available == true + onFailure: + - name: soldOut + type: end +``` + +**outputs** + +Extract data from the response. + +```yaml +- 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: + +```yaml +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: + +```yaml +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`: + +```yaml +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. + +```yaml +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: + +```yaml +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 + +```yaml +- 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 + +```yaml +- stepId: getBooking + operationId: $sourceDescriptions.api.getBookingById + parameters: + - name: bookingId + in: path + value: $steps.createBooking.outputs.bookingId +``` + +### Header Parameters + +```yaml +- 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 +``` + +### Cookie Parameters + +```yaml +- 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 + +```yaml +- stepId: addPassenger + operationId: $sourceDescriptions.trainApi.addPassengerToBooking + requestBody: + contentType: application/json + payload: + name: $inputs.passengerName + email: $inputs.passengerEmail +``` + +### Complex Request Body + +```yaml +- 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 + +```yaml +- 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 + +```yaml +# 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 + +```yaml +- 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](https://www.rfc-editor.org/rfc/rfc6901.html) notation. + +```yaml +- 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. + +```yaml +- 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: + +```yaml +- 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: + +```yaml +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`: + +```yaml +# 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: + +```yaml +# 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 +``` + +### Clear Descriptions + +```yaml +- 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 +``` + +### Extract Useful Outputs + +Be intentional about outputs. Only extract data you know you'll need in a later step: + +```yaml +# Good - clear purpose +outputs: + selectedTripId: $response.body#/trips/0/id + userEmailAddress: $response.body#/customer/email + totalPriceIncludingTax: $response.body#/pricing/total + +# Avoid - vague names +outputs: + id: $response.body#/trips/0/id + value: $response.body#/customer/email + amount: $response.body#/pricing/total +``` + +## 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. diff --git a/src/_guides/arazzo/specification/v1.0/understanding-structure/success-and-failure.md b/src/_guides/arazzo/specification/v1.0/understanding-structure/success-and-failure.md new file mode 100644 index 00000000..6bec0958 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/understanding-structure/success-and-failure.md @@ -0,0 +1,484 @@ +--- +title: Success and Failure in API Workflows +authors: phil +excerpt: Define precise success and failure criteria for each step in your Arazzo workflows, describing comprehensive error handling and branching logic. +date: 2025-01-30 +--- + +- TOC +{:toc} + +Some API workflow tools just check HTTP status codes for a 200 OK or a 500 server error and decide based on that! Real-world APIs are far more involved that. A 200 response might contain an empty result set. A 404 might be totally expected in a given scenario. Arazzo gives you fine-grained control over what "success" and "failure" actually mean for the use-case and work flow by allowing criteria to be defined for each step. + +```yaml +- stepId: search + operationId: $sourceDescriptions.api.searchTrips + successCriteria: + - condition: $statusCode == 200 + - type: jsonpath + context: $response.body + condition: $.trips[0] != null + failureCriteria: + - condition: $statusCode == 404 +``` + +Sometimes HTTP says might say "success", but the business logic says "nope!" so it's helpful to not rely entirely on status checks. Imagine a search API that returns 200 OK, but with zero results. Or an inventory check that returns 200 with `{"available": false}`. These need explicit criteria because it could be a success or a failure depending on the context of that workflow. + +Here are a few things we might want to check: + +- The response contains the data you expected. +- Specific fields have the right values. +- Arrays have at least one item (or exactly N items). +- Headers indicate proper caching or rate limiting. +- The response structure matches your schema. + +Both the success and failure criteria are arrays of checks that work in in the same way, using the same criteria objects. + +## Criteria Object + +Every one of these conditions must pass for the step to be considered successful, making it an `AND` operation, not an `OR`. + +```yaml +steps: +- stepId: exampleStep + operationId: $sourceDescriptions.api.exampleOperation + + successCriteria: + - condition: $statusCode == 200 + - condition: $response.body.results != null + + failureCriteria: + - context: $response.body + condition: $.errors[0] != null + type: jsonpath +``` + +**condition (required)** + +A boolean expression that must evaluate to true. + +```yaml +successCriteria: + - condition: $statusCode == 200 + - condition: $response.body.available == true +``` + +**type (optional)** + +Type of criterion (defaults to `simple`): + +- `simple` - Basic condition evaluation. +- `regex` - Regular expression matching. +- `jsonpath` - JSONPath query. + +**context (required when type == regex or jsonpath)** + +Which bit of the data are we evaluating. This could be `$response.body`, `$response.headers`, or any other valid [runtime expressions](_guides/arazzo/specification/v1.0/working-with-arazzo/runtime-expressions.md). + +```yaml +successCriteria: + - condition: $response.body.status == 'confirmed' +``` + +## Success and Failure Actions + +Once criteria determine whether a step has succeeded or failed, what happens next? By default, the workflow continues to the next sequential step. But we can also define `onSuccess` and `onFailure` actions to branch the workflow based on outcomes. + +Actions can be defined inline or referenced from [reusable components](_guides/arazzo/specification/v1.0/understanding-structure/components-and-references.md) to maintain consistency across workflows. + +### Action Types + +Both `onSuccess` and `onFailure` use the same action types. Each action has a `name`, a `type` (what to do), optional `criteria` which is a list of assertions to see if this action should be executed, and type-specific fields. + +**type: end** + +Stops the workflow immediately. + +```yaml +onFailure: + - name: criticalError + type: end + criteria: + - condition: $statusCode >= 500 +``` + +**type: goto** + +Jumps to another step (perfect for error handlers or alternative paths). + +```yaml +onFailure: + - name: tryAlternative + type: goto + stepId: alternativeBookingMethod + criteria: + - condition: $statusCode == 429 # Too Many Requests + +onSuccess: + - name: continueToPayment + type: goto + stepId: processPayment + criteria: + - condition: $response.body.requiresPayment == true +``` + +**type: retry** + +Tries the same step again (with optional delays and limits). + +```yaml +onFailure: + - name: retryOnTimeout + type: retry + retryAfter: 5 # seconds + retryLimit: 3 # attempts + criteria: + - condition: $statusCode == 408 # Request Timeout +``` + +### Invoking Workflows on Actions + +Sometimes a single step can't handle the recovery or next phase. The `workflowId` field lets actions invoke other workflows for complex scenarios: + +```yaml +onFailure: + # Expired token - refresh and retry + - name: refreshExpiredToken + type: retry + workflowId: refreshTokenWorkflow + retryAfter: 1 + retryLimit: 3 + criteria: + - condition: $statusCode == 401 + - condition: $response.body.errorCode == 'TOKEN_EXPIRED' + + # Primary API down - switch to backup + - name: useBackupApi + type: goto + workflowId: backupApiSearchWorkflow + stepId: searchWithBackup + criteria: + - condition: $statusCode >= 500 +``` + +This is particularly useful for: + +- **Token refresh flows** - When authentication expires, invoke a workflow to get new credentials and retry. +- **Fallback API chains** - Try alternative APIs or services when primary ones fail. +- **Data synchronization** - Trigger reconciliation workflows when data inconsistencies are detected. +- **Escalation procedures** - Invoke notification and logging workflows for critical errors. + +The workflow will runs completely when it's invoked, then returns to the current step to continue processing based on the result. + +## Examples + +Let's rattle through a few more complete scenarios to see how it all fits together. + +### Branching on Search Results + +A more advanced workflow has been created for the Train Travel API which allows folks to search for train trips, and based on various critera it will either look for better trips or go ahead and book. + +```yaml +workflows: + - workflowId: searchAndBookTrips + summary: Search for train trips and handle different results + inputs: + type: object + properties: + origin: + type: string + destination: + type: string + departureDate: + type: string + maxPrice: + type: number + + steps: + - stepId: searchTrips + operationId: $sourceDescriptions.trainApi.searchTrips + parameters: + - name: origin + in: query + value: $inputs.origin + - name: destination + in: query + value: $inputs.destination + - name: date + in: query + value: $inputs.departureDate + + successCriteria: + - condition: $statusCode == 200 + - type: jsonpath + context: $response.body + condition: $.trips[0] != null + + onSuccess: + # Found affordable trips - proceed to booking + - name: foundAffordableTrips + type: goto + stepId: selectTrip + criteria: + - type: jsonpath + context: $response.body + condition: $.trips[?(@.price <= $inputs.maxPrice)][0] != null + + # Only expensive trips - offer alternatives + - name: onlyExpensiveTrips + type: goto + stepId: suggestAlternativeDates + criteria: + - type: jsonpath + context: $response.body + condition: $.trips[?(@.price <= $inputs.maxPrice)][0] == null + + onFailure: + # No trips available - try different dates + - name: noTripsAvailable + type: goto + stepId: searchAlternativeDates + criteria: + - type: jsonpath + context: $response.body + condition: $.trips[0] == null + + # API error - retry + - name: apiError + type: retry + retryAfter: 5 + retryLimit: 3 + criteria: + - condition: $statusCode >= 500 + + - stepId: selectTrip + # ... trip selection logic + + - stepId: suggestAlternativeDates + # ... alternative date suggestions + + - stepId: searchAlternativeDates + # ... search with different dates +``` + +This workflow branches based on the search results: + +- **Affordable trips exist** - Go to trip selection. +- **Only expensive trips** - Suggest alternative dates. +- **No trips at all** - Also suggest alternative dates. +- **API failure** - Retry the search. + +## Example: Branching on Booking Status + +Once a trip is selected, creating the booking might succeed in different ways: + +```yaml +workflows: + - workflowId: createTripBooking + summary: Create booking with different confirmation flows + inputs: + type: object + properties: + passengers: + type: array + + steps: + - stepId: createBooking + operationId: $sourceDescriptions.trainApi.createBooking + requestBody: + payload: + tripId: $steps.selectTrip.outputs.selectedTripId + passengers: $inputs.passengers + + successCriteria: + - condition: $statusCode == 201 + - condition: $response.body.id != null + + onSuccess: + # Booking confirmed immediately - skip to payment + - name: instantConfirmation + type: goto + stepId: processPayment + criteria: + - condition: $response.body.status == 'confirmed' + + # Pending confirmation - wait for availability check + - name: pendingConfirmation + type: goto + stepId: pollBookingStatus + criteria: + - condition: $response.body.status == 'pending' + + # Free trip (promotional) - skip payment + - name: freeTrip + type: goto + stepId: sendConfirmationEmail + criteria: + - condition: $response.body.totalPrice == 0 + + onFailure: + # Seats sold out - offer alternative trips + - name: seatsUnavailable + type: goto + stepId: findAlternativeTrips + criteria: + - condition: $statusCode == 409 + - condition: $response.body.errorCode == 'SEATS_UNAVAILABLE' + + # Invalid passenger data - return to form + - name: invalidPassengerData + type: goto + stepId: notifyValidationError + criteria: + - condition: $statusCode == 400 + - condition: $response.body.errorCode == 'INVALID_PASSENGER_DATA' + + - stepId: processPayment + # ... payment processing + + - stepId: pollBookingStatus + # ... poll for booking confirmation + + - stepId: sendConfirmationEmail + # ... send confirmation + + - stepId: findAlternativeTrips + # ... find other available trips + + - stepId: notifyValidationError + # ... notify about validation issues +``` + +## Example: Multi-Passenger Validation + +When handling multiple passengers, validate all requirements before proceeding: + +```yaml +workflows: + - workflowId: validateTripPassengers + summary: Validate passenger data with different requirements + inputs: + type: object + properties: + passengers: + type: array + + steps: + - stepId: validatePassengers + operationId: $sourceDescriptions.trainApi.validatePassengerData + requestBody: + payload: + passengers: $inputs.passengers + tripId: $steps.selectTrip.outputs.selectedTripId + + successCriteria: + - condition: $statusCode == 200 + - type: jsonpath + context: $response.body + condition: $.passengers[?(@.valid == false)][0] == null + + onSuccess: + # All passengers valid - proceed + - name: allPassengersValid + type: goto + stepId: createBooking + + onFailure: + # Child without guardian - request guardian details + - name: childWithoutGuardian + type: goto + stepId: requestGuardianInfo + criteria: + - type: jsonpath + context: $response.body + condition: $.passengers[?(@.age < 16 && @.guardianId == null)][0] != null + + # Senior discount requires ID verification + - name: seniorRequiresVerification + type: goto + stepId: verifySeniorDiscount + criteria: + - type: jsonpath + context: $response.body + condition: $.passengers[?(@.age >= 65 && @.idVerified == false)][0] != null + + # Invalid passport for international trip + - name: invalidPassport + type: goto + stepId: requestValidPassport + criteria: + - condition: $response.body.tripType == 'international' + - type: jsonpath + context: $response.body + condition: $.passengers[?(@.passportValid == false)][0] != null + + - stepId: createBooking + # ... proceed with booking + + - stepId: requestGuardianInfo + # ... request guardian details for minors + + - stepId: verifySeniorDiscount + # ... verify senior citizen ID + + - stepId: requestValidPassport + # ... request valid passport for international travel +``` + + +### Validate Business Rules, Not Just HTTP + +The goal is to ensure the workflow meets business needs, not just technical success at the transportation level. Get business logic written down, and if the logic changes that's ok, the workflow can be updated to match. + +```yaml +# Good - checks business requirements +successCriteria: + - condition: $statusCode == 200 + - condition: $response.body.status == 'confirmed' + - condition: $response.body.seats != null + - condition: $response.body.totalPrice <= $inputs.maxBudget + +# Insufficient - only checks HTTP +successCriteria: + - condition: $statusCode == 200 +``` + +### Reusable Actions with Components + +For actions used across multiple steps or workflows, define them in the components section: + +```yaml +components: + failureActions: + # Reusable token refresh + refreshToken: + name: refreshExpiredToken + type: retry + workflowId: refreshTokenWorkflow + retryAfter: 1 + retryLimit: 3 + criteria: + - condition: $statusCode == 401 + + # Reusable backup API fallback + useBackupSystem: + name: switchToBackupApi + type: goto + workflowId: backupSystemWorkflow + stepId: retryWithBackup + criteria: + - condition: $statusCode >= 500 + - condition: $response.headers.retry-after == null +``` + +Then reference them in your steps: + +```yaml +- stepId: searchTrips + operationId: $sourceDescriptions.trainApi.searchTrips + onFailure: + - reference: $components.failureActions.refreshToken + - reference: $components.failureActions.useBackupSystem +``` + +This keeps your workflows clean and ensures consistent error handling across your entire API workflow description. diff --git a/src/_guides/arazzo/specification/v1.0/understanding-structure/workflows.md b/src/_guides/arazzo/specification/v1.0/understanding-structure/workflows.md new file mode 100644 index 00000000..b7e0f153 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/understanding-structure/workflows.md @@ -0,0 +1,514 @@ +--- +title: Workflows +authors: phil +excerpt: Learn how to define Arazzo workflows, similar to GitHub Actions and CI/CD workflows but for describing API interactions. +date: 2025-01-31 +--- + +- TOC +{:toc} + +Think of workflows as recipes for API operations. Just like a recipe breaks down cooking into steps (chop onions, sauté garlic, add tomatoes), an Arazzo workflow breaks down complex API tasks into manageable steps. If you've used GitHub Actions or Jenkins pipelines, the concept will feel familiar, but Arazzo is purpose-built for API orchestration rather than CI/CD. + +## What is a Workflow? + +A workflow is a named sequence of steps that performs a complete task using one or more APIs. Each workflow is self-contained. It declares what inputs it needs, what steps to execute, and what outputs to return. This makes workflows portable and reusable across different contexts. + +```yaml +workflows: + - workflowId: book-train-ticket + summary: Complete workflow for booking a train ticket + description: | + This workflow handles the end-to-end process of booking a train ticket: + 1. Search for available trips + 2. Select a trip + 3. Create a booking + 4. Process payment + 5. Receive confirmation + + inputs: + type: object + properties: + origin: + type: string + destination: + type: string + departureDate: + type: string + format: date + passengers: + type: array + + steps: + # ... step definitions + + outputs: + bookingId: $steps.createBooking.outputs.bookingId + ticketUrl: $steps.confirm.outputs.ticketUrl +``` + +## Workflow Structure + +### Required Fields + +Only two fields are actually required to create a workflow: + +**workflowId** identifies the workflow uniquely within the document. Pick something descriptive, which should conform to the regular expression `[A-Za-z0-9_\-]+` but why not use `kebab-case` to make it extra readable. + +```yaml +workflows: + - workflowId: create-and-retrieve-booking +``` + +**steps** is an array of actions to execute. We'll cover steps in detail later, but for now just know that every workflow needs at least one step. + +```yaml +workflows: + - workflowId: simple-workflow + steps: + - stepId: firstStep + # ... step configuration + - stepId: secondStep + # ... step configuration +``` + +### Optional Fields + +**summary** - A brief description: + +```yaml +workflows: + - workflowId: book-ticket + summary: Search for and book a train ticket +``` + +**description** - A longer description that can go much longer, even including CommonMark (Markdown) formatting. This is great for documenting complex workflows and will be used by tools to generate nice documentation pages. + +```yaml +workflows: + - workflowId: complex-booking + summary: Multi-step booking process + description: | + This workflow handles complex booking scenarios including: + + - Multi-city travel + - Group bookings with different passenger types + - Seat selection and preferences + - Special assistance requests + - Payment plan options + + The workflow includes extensive error handling for: + - Sold out trips + - Payment failures + - Booking timeouts +``` + +**inputs** - Define what data the workflow accepts using a JSON Schema object: + +```yaml +workflows: + - workflowId: search-and-book + inputs: + type: object + required: + - origin + - destination + properties: + origin: + type: string + description: Departure station code + example: BOS + destination: + type: string + description: Arrival station code + example: NYC + departureDate: + type: string + format: date + description: Date of travel +``` + +**outputs** - Define what data the workflow produces: + +```yaml +workflows: + - workflowId: book-ticket + outputs: + bookingId: + $steps.createBooking.outputs.bookingId + confirmationCode: + $steps.confirm.outputs.code + totalPrice: + $steps.payment.outputs.amount +``` + +**parameters** - Global parameters that apply to all steps (unless overridden): + +```yaml +workflows: + - workflowId: authenticated-workflow + parameters: + - name: Authorization + in: header + value: Bearer $inputs.token + + steps: + # All steps will include this Authorization header + # unless they override it +``` + +**dependsOn** - Specify dependencies on other workflows: + +```yaml +workflows: + - workflowId: authenticated-booking + dependsOn: + - authenticate-user + steps: + # This workflow assumes authentication has been completed +``` + +## Workflow Inputs + +Workflows need some data to get started. Maybe it's a user ID, search criteria, or a new resource being created. Arazzo uses JSON Schema to define what inputs a workflow accepts, which has the handy side effect of providing validation and documentation automatically. + +### Basic Inputs + +```yaml +workflows: + - workflowId: search-trips + inputs: + type: object + properties: + origin: + type: string + destination: + type: string + date: + type: string + format: date +``` + +Input objects can have any valid JSON Schema structure, including nested objects and arrays. If you are new to JSON Schema, check out the [official documentation](https://json-schema.org/learn) for a full guide. + +### Required vs Optional + +Some inputs are must-haves (where are you going?), while others can have sensible defaults (how many passengers? probably just one). + +The `required` array specifies which inputs must be provided, everything else is assumed to be optional, with `default` values where specified: + +```yaml +workflows: + - workflowId: flexible-search + inputs: + type: object + required: + - origin + - destination + properties: + origin: + type: string + destination: + type: string + departureDate: + type: string + format: date + description: Optional - defaults to today + passengers: + type: integer + default: 1 + minimum: 1 + maximum: 9 +``` + +### Complex Input Types + +Inputs can be as simple or complex as you need. Nested objects, arrays, enums,JSON Schema has a keyword for pretty much everything. + +```yaml +workflows: + - workflowId: book-with-preferences + inputs: + type: object + properties: + passengers: + type: array + items: + type: object + properties: + name: + type: string + age: + type: integer + seatPreference: + type: string + enum: [window, aisle, any] + + paymentMethod: + type: object + properties: + type: + type: string + enum: [credit_card, debit_card, paypal] + token: + type: string +``` + +### Using Inputs in Steps + +Once you've defined inputs at the workflow level, steps can access them using runtime expressions like `$inputs.origin`. This is how data flows from the workflow into individual API calls: + +```yaml +workflows: + - workflowId: search-workflow + inputs: + type: object + properties: + origin: + type: string + destination: + type: string + + steps: + - stepId: search + operationId: $sourceDescriptions.api.searchTrips + parameters: + - name: origin + in: query + value: $inputs.origin # Reference input + - name: destination + in: query + value: $inputs.destination # Reference input +``` + +## Workflow Outputs + +Workflows should return useful data. What good is running a booking workflow if you can't get the booking ID back? Outputs define what data the workflow makes available to whatever called it. + +### Simple Outputs + +```yaml +workflows: + - workflowId: create-booking + steps: + - stepId: book + # ... creates a booking + outputs: + bookingId: $response.body#/id + + outputs: + bookingId: $steps.book.outputs.bookingId + createdAt: $steps.book.outputs.timestamp +``` + +### Computed Outputs + +```yaml +workflows: + - workflowId: calculate-total + steps: + - stepId: getBasePrice + outputs: + basePrice: $response.body#/price + + - stepId: getTax + outputs: + taxAmount: $response.body#/tax + + outputs: + basePrice: $steps.getBasePrice.outputs.basePrice + tax: $steps.getTax.outputs.taxAmount + total: $steps.getBasePrice.outputs.basePrice + $steps.getTax.outputs.taxAmount +``` + +## Global Parameters + +Got a header or query parameter that every single step needs? Don't repeat yourself. Define them at the workflow level and all steps inherit it automatically: + +```yaml +workflows: + - workflowId: api-with-auth + parameters: + # This header will be included in all step requests + - name: Authorization + in: header + value: Bearer $inputs.apiKey + + # API version header + - name: API-Version + in: header + value: "2024-01" + + steps: + - stepId: getUser + operationId: $sourceDescriptions.api.getUser + # Automatically includes Authorization and API-Version headers + + - stepId: updateUser + operationId: $sourceDescriptions.api.updateUser + # Also automatically includes both headers +``` + +## Workflow Dependencies + +When workflows need to run in a specific order, the `dependsOn` field makes those dependencies explicit. This is particularly useful for authentication flows, multi-stage processes, or any scenario where one workflow produces data that another workflow requires: + +```yaml +workflows: + # Base workflow - no dependencies + - workflowId: authenticate + summary: Get an authentication token + steps: + - stepId: login + outputs: + token: $response.body#/access_token + + # Depends on authentication + - workflowId: get-user-bookings + dependsOn: + - authenticate + summary: Retrieve bookings for authenticated user + steps: + - stepId: fetchBookings + parameters: + - name: Authorization + in: header + value: Bearer $inputs.token # Assumes token from authenticate workflow +``` + +Smart tools can use this information to automatically run prerequisite workflows, validate execution order, or generate nice workflow diagrams. + +## Workflow Patterns + +Here are some common patterns for organizing workflows: + +### Linear Sequential Workflows + +The most straightforward pattern - steps execute one after another: + +```yaml +workflows: + - workflowId: linear-booking + steps: + - stepId: searchTrips + - stepId: selectTrip + - stepId: createBooking + - stepId: processPayment +``` + +### Multi-Source Workflows + +Workflows can orchestrate across multiple APIs by referencing operations from different source descriptions: + +```yaml +workflows: + - workflowId: complete-order + steps: + - stepId: checkInventory + operationId: $sourceDescriptions.inventoryApi.checkStock + + - stepId: processPayment + operationId: $sourceDescriptions.paymentApi.createCharge + + - stepId: scheduleShipping + operationId: $sourceDescriptions.shippingApi.createShipment +``` + +### Nested Workflows + +Break complex processes into smaller, reusable workflows: + +```yaml +workflows: + - workflowId: authenticate + summary: Get authentication token + steps: + - stepId: login + operationId: $sourceDescriptions.authApi.login + + - workflowId: authenticated-booking + dependsOn: + - authenticate + steps: + - stepId: getProfile + operationId: $sourceDescriptions.userApi.getProfile + + - stepId: createBooking + operationId: $sourceDescriptions.bookingApi.create +``` + +## Best Practices + +Here's a few tips for writing workflows that are easy to understand and maintain. + +### Descriptive Workflow IDs + +Pick workflow IDs that clearly describe what the workflow does: + +```yaml +# Good - clear and descriptive +- workflowId: search-and-book-train-ticket +- workflowId: cancel-booking-with-refund +- workflowId: update-passenger-details + +# Avoid - vague or cryptic +- workflowId: workflow1 +- workflowId: process +- workflowId: sab +``` + +### Write Summaries That Add Value + +Don't just repeat the workflow ID in different words. Your summary should tell readers what the workflow accomplishes and when they'd want to use it: + +```yaml +# Good +- workflowId: guest-checkout + summary: Complete checkout process for guest users without registration + +# Avoid +- workflowId: checkout + summary: Checkout workflow +``` + +### Document Complex Workflows + +For workflows with many steps or intricate logic, use the `description` field to explain the overall process, key decisions, and error handling: + +```yaml +- workflowId: multi-city-booking + summary: Book a multi-city train journey + description: | + This workflow handles the complexity of booking trips across multiple cities: + + **Process:** + 1. Validates that all cities form a valid route + 2. Searches for available trips for each leg + 3. Ensures compatible connection times + 4. Creates individual bookings for each leg + 5. Links bookings together + 6. Processes single payment for entire journey + + **Error Handling:** + - If any leg is unavailable, the entire booking fails + - If payment fails, all leg bookings are cancelled + - Connection validation ensures minimum 30 minute layovers +``` + +### Keep Workflows Focused + +Each workflow should do one thing well. Don't create a mega-workflow that handles everything from user registration to coffee machine calibration: + +```yaml +# Good - focused workflows +- workflowId: search-trips +- workflowId: create-booking +- workflowId: process-payment +- workflowId: cancel-booking + +# Avoid - overly broad workflow +- workflowId: do-everything + # ... handles search, booking, payment, cancellation, refunds, ... +``` + +Steps are where the real action happens in a workflow. They define the individual operations that get executed, whether that's calling an API endpoint, invoking another workflow, or performing conditional logic, so lets get stuck into that. diff --git a/src/_guides/arazzo/specification/v1.0/working-with-arazzo/api-testing.md b/src/_guides/arazzo/specification/v1.0/working-with-arazzo/api-testing.md new file mode 100644 index 00000000..fca14d16 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/working-with-arazzo/api-testing.md @@ -0,0 +1,9 @@ +--- +title: API Testing with Arazzo +authors: phil +excerpt: Learn how to use Arazzo workflows as integration tests throughout the API lifecycle, from design validation to continuous monitoring. +date: 2025-01-23 +--- + +- TOC +{:toc} diff --git a/src/_guides/arazzo/specification/v1.0/working-with-arazzo/runtime-expressions.md b/src/_guides/arazzo/specification/v1.0/working-with-arazzo/runtime-expressions.md new file mode 100644 index 00000000..689fb4f4 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/working-with-arazzo/runtime-expressions.md @@ -0,0 +1,348 @@ +--- +title: Runtime Expressions +authors: phil +excerpt: Learn how to use runtime expressions in Arazzo to create dynamic, data-driven workflows. +date: 2025-02-01 +--- + +- TOC +{:toc} + +Runtime expressions are the heart of Arazzo's dynamic capabilities. They allow you to reference data from inputs, previous steps, and responses, compute new values, and create conditional logic. Understanding runtime expressions is essential for building sophisticated workflows. + +## What Are Runtime Expressions? + +Runtime expressions are evaluated during workflow execution to produce values. They start with a `$` and use dot notation to access data: + +```yaml +# Reference workflow input +$inputs.origin + +# Reference previous step output +$steps.search.outputs.tripId + +# Reference response status +$statusCode + +# Reference response body +$response.body.booking.id +``` + +## Expression Contexts + +Expressions can reference data from several contexts: + +### Workflow Inputs + +Access data passed to the workflow: + +```yaml +workflows: + - workflowId: search-trips + inputs: + type: object + properties: + origin: + type: string + destination: + type: string + date: + type: string + + steps: + - stepId: search + parameters: + - name: from + in: query + value: $inputs.origin # Access workflow input + - name: to + in: query + value: $inputs.destination + - name: departure + in: query + value: $inputs.date +``` + +**Nested inputs:** + +```yaml +inputs: + type: object + properties: + passenger: + type: object + properties: + name: + type: string + age: + type: integer + +# Access nested values +value: $inputs.passenger.name +value: $inputs.passenger.age +``` + +**Array inputs:** + +```yaml +inputs: + type: object + properties: + userName: + type: string + email: + type: string + +# Access input properties +value: $inputs.userName +value: $inputs.email +``` + +### Previous Step Outputs + +Reference outputs from earlier steps: + +```yaml +steps: + - stepId: search + operationId: $sourceDescriptions.api.searchTrips + outputs: + selectedTripId: $response.body.trip.id + tripPrice: $response.body.trip.price + + - stepId: createBooking + operationId: $sourceDescriptions.api.createBooking + requestBody: + payload: + # Reference outputs from 'search' step + tripId: $steps.search.outputs.selectedTripId + price: $steps.search.outputs.tripPrice +``` + +To access an output from a previous step, use the syntax: + +``` +$steps.{stepId}.outputs.{outputName} +``` + +For this to work the output must be defined in the referenced step, and that step must have already run. + +**Learn more about [steps and outputs](_guides/arazzo/specification/v1.0/understanding-structure/steps-inputs-outputs.md).** + +### Current Response + +Within success/failure criteria and outputs, access the current response: + +```yaml +- stepId: getBooking + operationId: $sourceDescriptions.api.getBooking + successCriteria: + # Access response body + - condition: $response.body.status == 'confirmed' + + # Access response headers + - condition: $response.header.content-type == 'application/json' + + outputs: + # Extract from response + bookingId: $response.body.id + customerEmail: $response.body.customer.email + total: $response.body.pricing.total +``` + +**Response properties:** +- `$response.body` - Response body (parsed JSON/XML) +- `$response.header` - Response headers object +- `$response.header.content-type` - Specific header + +### HTTP Status + +Access the HTTP status code of the current response: + +```yaml +successCriteria: + - condition: $statusCode >= 200 && $statusCode < 300 + +outputs: + responseCode: $statusCode +``` + +### Source Descriptions + +Reference operations from source APIs: + +```yaml +sourceDescriptions: + - name: trainApi + url: ./train-api.yaml + type: openapi + +steps: + - stepId: search + # Reference operation from source + operationId: $sourceDescriptions.trainApi.searchTrips +``` + +That's `$sourceDescriptions.{sourceName}.{operationId}` or `$sourceDescriptions.{sourceName}.{workflowId}` for other Arazzo workflows. + +## Operators in Conditions + +Runtime expressions support various operators for use in success and failure criteria: + +### Comparison Operators + +```yaml +successCriteria: + # Equality + - condition: $response.body.status == 'confirmed' + - condition: $statusCode == 200 + + # Inequality + - condition: $response.body.error != null + - condition: $statusCode != 404 + + # Greater than + - condition: $response.body.price > 100 + - condition: $response.body.stock > $inputs.quantity + + # Greater than or equal + - condition: $response.body.rating >= 4.0 + + # Less than + - condition: $response.body.price < $inputs.maxPrice + + # Less than or equal + - condition: $response.body.quantity <= $response.body.maxQuantity +``` + +### Logical Operators + +```yaml +successCriteria: + # AND - all conditions must be true + - condition: $statusCode == 200 && $response.body.available == true + - condition: $response.body.price > 0 && $response.body.price < 1000 + + # OR - at least one condition must be true + - condition: $statusCode == 200 || $statusCode == 201 + - condition: $response.body.status == 'confirmed' || $response.body.status == 'pending' +``` + +### String Operators + +Concat strings using interpolation: + +```yaml +outputs: + fullName: '{$response.body.firstName} {$response.body.lastName}' + message: 'Booking {$response.body.id} confirmed' +``` + +## Null Safety + +Check for null values in conditions: + +```yaml +successCriteria: + # Check for null/undefined + - condition: $response.body.email != null + - condition: $response.body.address != null + - condition: $response.body.id != null +``` + +## Extracting Values + +Use outputs to extract values from responses: + +```yaml +outputs: + # Extract simple values + bookingId: $response.body.id + customerEmail: $response.body.customer.email + tripPrice: $response.body.trip.price + + # Extract nested values + originCity: $response.body.trip.origin.city + destinationCity: $response.body.trip.destination.city +``` + +## Using Expressions in Conditions + +Expressions are frequently used in success and failure criteria. Here's a comprehensive guide to condition patterns: + +### Status Code Checks + +```yaml + # Exact status + - condition: $statusCode == 200 + - condition: $statusCode == 201 + + # Status ranges + - condition: $statusCode >= 200 && $statusCode < 300 + + # Multiple acceptable codes + - condition: $statusCode == 200 || $statusCode == 201 +``` + +### Field Value Checks + +```yaml +# Simple field checks +- condition: $response.body.status == 'confirmed' +- condition: $response.body.stock > 0 +- condition: $response.body.payment.processed == true + +# Field existence +- condition: $response.body.id != null +- condition: $response.body.bookingReference != null +``` + +### Checking Response Structure + +```yaml +# Check for required object properties +- condition: $response.body.trip != null +- condition: $response.body.trip.available == true +- condition: $response.body.trip.price <= $inputs.maxPrice +``` + +### Header Checks + +```yaml + # Header values + - condition: $response.header['content-type'] == 'application/json' + - condition: $response.header['x-api-version'] == '2024-01' + + # Rate limiting (header values are strings) + - condition: $response.header['x-rate-limit-remaining'] != '0' + - condition: $response.header['retry-after'] == null +``` + +### JSONPath in Conditions + +For complex filtering, use JSONPath expressions with `type: jsonpath`. This is **only available in conditions** (`successCriteria`/`failureCriteria`), not in outputs or parameters. + +```yaml + +# Check all bookings are confirmed +- type: jsonpath + context: $response.body + condition: $.bookings[?(@.status != 'confirmed')][0] == null + +# At least one affordable trip +- type: jsonpath + context: $response.body + condition: $.trips[?(@.price < 100)][0] != null + +# Find child passengers +- type: jsonpath + context: $response.body + condition: $.passengers[?(@.age < 18)][0] != null + +# No critical errors +- type: jsonpath + context: $response.body + condition: $.errors[?(@.severity == 'critical')][0] == null +``` + +Mastering runtime expressions enables you to build dynamic, data-driven workflows that respond intelligently to API responses and user inputs. diff --git a/src/_guides/arazzo/specification/v1.0/working-with-arazzo/workflow-documentation.md b/src/_guides/arazzo/specification/v1.0/working-with-arazzo/workflow-documentation.md new file mode 100644 index 00000000..e9a6c104 --- /dev/null +++ b/src/_guides/arazzo/specification/v1.0/working-with-arazzo/workflow-documentation.md @@ -0,0 +1,9 @@ +--- +title: Workflow Documentation +authors: phil +excerpt: Discover how Arazzo workflows enhance API documentation beyond traditional reference docs, with tools and techniques for presenting workflows to API consumers. +date: 2025-01-23 +--- + +- TOC +{:toc}