Arazzo Workflow Orchestration

Parse, validate, and execute Arazzo workflow specifications

The Arazzo Specification defines a standard way to describe sequences of API calls as workflows.

Arazzo documents reference OpenAPI specifications and describe step-by-step orchestrations on which operations to call, in what order, how to pass data between them, and what constitutes success or failure.

libopenapi provides full support for:

  • Parsing Arazzo 1.0.1 documents into strongly-typed Go models
  • Validating documents against 21 structural rules with line/column error reporting
  • Executing workflows via a pluggable engine with retry, branching, and dependency ordering
Arazzo support was added in libopenapi v0.34. The implementation follows the Arazzo Specification v1.0.1.

What is Arazzo?

An Arazzo document describes one or more workflows, each containing a sequence of steps. Each step either calls an API operation (referenced from a source OpenAPI document) or invokes another workflow. Steps can pass data between each other using runtime expressions, and each step can define success criteria to determine if it passed.

Document structure

arazzo: 1.0.1
info:
  title: Pet Store Workflows
  version: 1.0.0
sourceDescriptions:
  - name: petStoreApi
    url: https://petstore.swagger.io/v2/swagger.json
    type: openapi
workflows:
  - workflowId: createAndVerifyPet
    steps:
      - stepId: createPet
        operationId: addPet
        requestBody:
          contentType: application/json
          payload:
            name: fluffy
            status: available
        successCriteria:
          - condition: $statusCode == 200
        outputs:
          petId: $response.body#/id
      - stepId: getPet
        operationId: getPetById
        parameters:
          - name: petId
            in: path
            value: $steps.createPet.outputs.petId
        successCriteria:
          - condition: $statusCode == 200

Key concepts

  • Source Descriptions - References to OpenAPI or other Arazzo documents that provide the API operations
  • Workflows - Named sequences of steps, optionally depending on other workflows
  • Steps - Individual actions that call an API operation or invoke a workflow
  • Success Criteria - Conditions evaluated after each step to determine pass/fail
  • Actions - Success and failure handlers that control flow (end, retry, goto)
  • Runtime Expressions - Dynamic references like $statusCode, $response.body#/id, $steps.X.outputs.Y
  • Components - Reusable parameters, success actions, failure actions, and inputs

Parsing Arazzo Documents

Use NewArazzoDocument to parse an Arazzo document from bytes:

import "github.com/pb33f/libopenapi"

func main() {
    arazzoYAML := []byte(`arazzo: 1.0.1
info:
  title: My Workflows
  version: 1.0.0
sourceDescriptions:
  - name: myApi
    url: ./openapi.yaml
    type: openapi
workflows:
  - workflowId: healthCheck
    steps:
      - stepId: checkHealth
        operationId: getHealth
        successCriteria:
          - condition: $statusCode == 200`)

    doc, err := libopenapi.NewArazzoDocument(arazzoYAML)
    if err != nil {
        panic(err)
    }

    fmt.Printf("Arazzo: %s v%s\n", doc.Info.Title, doc.Info.Version)
    fmt.Printf("Workflows: %d\n", len(doc.Workflows))
    fmt.Printf("Sources: %d\n", len(doc.SourceDescriptions))
}

The returned *high.Arazzo object gives you direct access to all fields as simple Go types. If you need the underlying YAML details (line numbers, column positions, raw nodes), call GoLow() on any model object:

doc, _ := libopenapi.NewArazzoDocument(arazzoYAML)

// High-level: simple string access
fmt.Println(doc.Info.Title)

// Low-level: source location access
lowInfo := doc.Info.GoLow()
fmt.Printf("Title at line %d, col %d\n",
    lowInfo.Title.ValueNode.Line,
    lowInfo.Title.ValueNode.Column)

Validating Documents

Use arazzo.Validate to check an Arazzo document against 21 structural validation rules. Validation returns nil when the document is valid:

import "github.com/pb33f/libopenapi/arazzo"

doc, _ := libopenapi.NewArazzoDocument(arazzoYAML)

result := arazzo.Validate(doc)
if result != nil {
    for _, err := range result.Errors {
        fmt.Printf("Error at line %d: %s\n", err.Line, err.Cause)
    }
    for _, warn := range result.Warnings {
        fmt.Printf("Warning at line %d: %s\n", warn.Line, warn.Message)
    }
}

Validation rules

Validation covers the full Arazzo specification:

Rule Description
Version arazzo field must be present and valid
Required fields info, sourceDescriptions, workflows must exist
Unique IDs Workflow IDs and step IDs must be unique
Step targeting Each step must have exactly one of operationId, operationPath, or workflowId
Parameter validation Required fields (name, value), valid in values
Action validation Type must be end/goto (success) or end/retry/goto (failure)
Reference resolution workflowId, stepId, source description references must resolve
Expression syntax Runtime expressions must be syntactically valid
Component keys Must match [a-zA-Z0-9._-]+ pattern
Dependency cycles Circular dependsOn references are detected
Source description names Must match [A-Za-z0-9_-]+ pattern

Error types

All validation errors include source location information and can be inspected with errors.Is:

result := arazzo.Validate(doc)
if result != nil {
    for _, err := range result.Errors {
        if errors.Is(err, arazzo.ErrDuplicateWorkflowId) {
            fmt.Printf("Duplicate workflow at line %d\n", err.Line)
        }
    }
}
Validation also checks operation references against attached OpenAPI source documents. When you use ResolveSources, OpenAPI documents are automatically attached to the Arazzo model. You can also attach them manually using doc.AddOpenAPISourceDocument(openAPIDoc) before calling Validate.

Resolving Source Descriptions

Before executing workflows, source descriptions need to be resolved to actual documents. The ResolveSources function fetches and parses each referenced source:

import "github.com/pb33f/libopenapi/arazzo"

sources, err := arazzo.ResolveSources(doc, &arazzo.ResolveConfig{
    OpenAPIFactory: func(sourceURL string, bytes []byte) (*v3high.Document, error) {
        d, err := libopenapi.NewDocument(bytes)
        if err != nil {
            return nil, err
        }
        m, err := d.BuildV3Model()
        if err != nil {
            return nil, err
        }
        return &m.Model, nil
    },
    ArazzoFactory: func(sourceURL string, bytes []byte) (*higharazzo.Arazzo, error) {
        return libopenapi.NewArazzoDocument(bytes)
    },
    BaseURL: "https://api.example.com/specs/",
})
// OpenAPI documents are automatically attached to doc for validation

Executing Workflows

The execution engine runs workflows by calling API operations through a pluggable Executor interface.

The Executor interface

You provide the HTTP layer:

type Executor interface {
    Execute(ctx context.Context, req *ExecutionRequest) (*ExecutionResponse, error)
}

The engine calls your executor with structured request details. You make the actual HTTP call and return the response:

type myExecutor struct {
    client *http.Client
}

func (e *myExecutor) Execute(ctx context.Context, req *arazzo.ExecutionRequest) (*arazzo.ExecutionResponse, error) {
    // Build HTTP request from req.Source.URL, req.OperationPath, req.Method, req.Parameters
    // Make the HTTP call
    // Return structured response
    return &arazzo.ExecutionResponse{
        StatusCode: resp.StatusCode,
        Headers:    resp.Header,
        Body:       parsedBody,
    }, nil
}

Creating and running the engine

import "github.com/pb33f/libopenapi/arazzo"

// Parse the document
doc, _ := libopenapi.NewArazzoDocument(arazzoYAML)

// Resolve source descriptions
sources, _ := arazzo.ResolveSources(doc, resolveConfig)

// Create the engine
engine := arazzo.NewEngine(doc, &myExecutor{}, sources)

// Run a single workflow
result, err := engine.RunWorkflow(context.Background(), "createPet", map[string]any{
    "petName": "fluffy",
})
if err != nil {
    panic(err)
}

fmt.Printf("Success: %v\n", result.Success)
fmt.Printf("Steps: %d\n", len(result.Steps))
fmt.Printf("Duration: %v\n", result.Duration)

Running all workflows

RunAll executes every workflow in dependency order using topological sort:

result, err := engine.RunAll(context.Background(), map[string]map[string]any{
    "createPet": {"petName": "fluffy"},
    "deletePet": {"petId": "123"},
})

fmt.Printf("Overall success: %v\n", result.Success)
for _, wf := range result.Workflows {
    fmt.Printf("  %s: success=%v duration=%v\n", wf.WorkflowId, wf.Success, wf.Duration)
}

If a workflow has dependsOn references, its dependencies are guaranteed to run first. If a dependency fails, the dependent workflow is skipped with an error.


Results

Every execution produces structured results:

RunResult

Returned by RunAll:

type RunResult struct {
    Workflows []*WorkflowResult
    Success   bool
    Duration  time.Duration
}

WorkflowResult

Returned by RunWorkflow:

type WorkflowResult struct {
    WorkflowId string
    Success    bool
    Inputs     map[string]any
    Outputs    map[string]any
    Steps      []*StepResult
    Error      error
    Duration   time.Duration
}

StepResult

One per step executed:

type StepResult struct {
    StepId     string
    Success    bool
    StatusCode int
    Outputs    map[string]any
    Error      error
    Duration   time.Duration
    Retries    int
}

Runtime Expressions

Arazzo uses runtime expressions to pass data between steps. The expression engine supports all expression types defined in the specification:

Expression Description
$statusCode HTTP status code from the current step
$response.body Full response body
$response.body#/path/to/field JSON Pointer into response body
$response.header.X-Header Response header value
$request.body#/field JSON Pointer into request body
$request.header.Authorization Request header value
$request.query.page Request query parameter
$request.path.userId Request path parameter
$steps.stepId.outputs.name Output from a previous step
$workflows.wfId.outputs.name Output from a completed workflow
$inputs.paramName Workflow input value
$sourceDescriptions.name.url Source description URL
$components.parameters.name Reusable component reference

Expressions can appear in parameter values, request bodies, success criteria conditions, and workflow outputs.


Success Criteria

Each step can define conditions that determine whether it passed:

successCriteria:
  - condition: $statusCode == 200
  - condition: $response.body#/status == "active"

Criterion types

Three criterion types are supported:

Simple (default) - Compares two values with ==, !=, >, <, >=, <=:

successCriteria:
  - condition: $statusCode == 200

Regex - Matches a value against a regular expression:

successCriteria:
  - condition: $response.body#/id
    type:
      type: regex
      version: draft-2020-12
    context: ^[0-9a-f]{8}-

JSONPath - Evaluates a JSONPath expression against the response body:

successCriteria:
  - condition: $.items[?@.status == 'active']
    type:
      type: jsonpath
      version: draft-goessner-dispatch-jsonpath-00
    context: $response.body

All criteria must pass for a step to be considered successful.


Success and Failure Actions

Actions control what happens after a step succeeds or fails:

Success actions

onSuccess:
  - name: endWorkflow
    type: end
  - name: jumpToVerify
    type: goto
    stepId: verifyStep
  - name: runSubWorkflow
    type: goto
    workflowId: verificationWorkflow

Failure actions

Failure actions support the same end and goto types, plus retry:

onFailure:
  - name: retryOnce
    type: retry
    retryAfter: 1.5
    retryLimit: 3
  - name: fallback
    type: goto
    stepId: fallbackStep
  - name: giveUp
    type: end

When retryAfter is specified, the engine waits the given number of seconds before retrying. The retryLimit controls how many times a step can be retried before moving to the next action.


Reusable Components

Components define reusable parameters, actions, and inputs:

components:
  parameters:
    apiKey:
      name: api_key
      in: header
      value: $inputs.apiKey
  successActions:
    logAndEnd:
      name: logAndEnd
      type: end
  failureActions:
    retryDefault:
      name: retryDefault
      type: retry
      retryAfter: 2.0
      retryLimit: 5
  inputs:
    userId:
      type: string

Reference components using the $components expression prefix:

steps:
  - stepId: callApi
    operationId: getUser
    parameters:
      - reference: $components.parameters.apiKey

Workflow Dependencies

Workflows can declare dependencies on other workflows using dependsOn:

workflows:
  - workflowId: setup
    steps:
      - stepId: createUser
        operationId: createUser

  - workflowId: test
    dependsOn:
      - setup
    steps:
      - stepId: getUser
        operationId: getUser

When running with RunAll, the engine performs a topological sort to ensure dependencies execute first. Circular dependencies are detected and reported as errors.


Rendering

Arazzo documents can be rendered back to YAML:

doc, _ := libopenapi.NewArazzoDocument(arazzoYAML)

rendered, err := doc.Render()
if err != nil {
    panic(err)
}
fmt.Println(string(rendered))

Every model object also has a Render() method for rendering individual components.


Complete Example

Here is a complete example that parses, validates, and executes an Arazzo workflow:

import (
    "context"
    "fmt"
    "github.com/pb33f/libopenapi"
    "github.com/pb33f/libopenapi/arazzo"
)

func main() {
    arazzoYAML := []byte(`arazzo: 1.0.1
info:
  title: Pet Store Workflows
  version: 1.0.0
sourceDescriptions:
  - name: petStore
    url: https://petstore.swagger.io/v2/swagger.json
    type: openapi
workflows:
  - workflowId: createPet
    steps:
      - stepId: addPet
        operationId: addPet
        requestBody:
          contentType: application/json
          payload:
            name: fluffy
            status: available
        successCriteria:
          - condition: $statusCode == 200
        outputs:
          petId: $response.body#/id`)

    // Parse the document
    doc, err := libopenapi.NewArazzoDocument(arazzoYAML)
    if err != nil {
        panic(err)
    }

    // Validate structure
    if result := arazzo.Validate(doc); result != nil {
        for _, e := range result.Errors {
            fmt.Printf("Validation error: %s\n", e)
        }
        return
    }

    // Resolve source descriptions (auto-attaches OpenAPI docs to the Arazzo model)
    sources, err := arazzo.ResolveSources(doc, &arazzo.ResolveConfig{
        OpenAPIFactory: func(sourceURL string, bytes []byte) (*v3high.Document, error) {
            d, err := libopenapi.NewDocument(bytes)
            if err != nil {
                return nil, err
            }
            m, err := d.BuildV3Model()
            if err != nil {
                return nil, err
            }
            return &m.Model, nil
        },
    })
    if err != nil {
        panic(err)
    }

    // Create engine with your executor
    engine := arazzo.NewEngine(doc, &myHTTPExecutor{}, sources)

    // Run the workflow
    result, err := engine.RunWorkflow(context.Background(), "createPet", nil)
    if err != nil {
        panic(err)
    }

    fmt.Printf("Workflow: %s\n", result.WorkflowId)
    fmt.Printf("Success: %v\n", result.Success)
    fmt.Printf("Duration: %v\n", result.Duration)
    for _, step := range result.Steps {
        fmt.Printf("  Step %s: status=%d success=%v\n",
            step.StepId, step.StatusCode, step.Success)
    }
    if result.Outputs != nil {
        fmt.Printf("Outputs: %v\n", result.Outputs)
    }
}

Error Handling

The arazzo package uses sentinel errors that work with errors.Is and errors.As:

import "github.com/pb33f/libopenapi/arazzo"

// Check for specific error types
if errors.Is(err, arazzo.ErrCircularDependency) {
    fmt.Println("Circular workflow dependency detected")
}

// Extract structured validation errors
var valErr *arazzo.ValidationError
if errors.As(err, &valErr) {
    fmt.Printf("At %s (line %d): %s\n", valErr.Path, valErr.Line, valErr.Cause)
}

// Extract step failure context
var stepErr *arazzo.StepFailureError
if errors.As(err, &stepErr) {
    fmt.Printf("Step %s failed: %s\n", stepErr.StepId, stepErr.Message)
}