Arazzo Workflow Orchestration
Parse, validate, and execute Arazzo workflow specificationsThe 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
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)
}
}
}
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)
}