Standalone Arazzo workflow execution SDK and CLI
Project description
arazzo-exec
arazzo-exec is an extensible Python SDK and CLI for parsing, validating, planning, and executing Arazzo workflows.
It is built for workflows that combine standard Arazzo/OpenAPI/AsyncAPI steps with custom execution extensions such as DB assertions, jobs, Temporal workflows, Kafka messages, auth providers, and local Testcontainers resources.
Design goals
- One package, optional extras.
- Core without globals or service-locator patterns.
- DI-compatible, but not coupled to a DI framework.
- Typed dataclass AST that preserves
x-*extensions and raw source data. - Explicit operation resolution and executor selection through registries.
- Async-first runner with a sync wrapper.
- Core does not know about DB, Temporal, Kafka, Testcontainers, or company-specific integrations.
- Extensions are public ABC-based contracts, not hidden callbacks.
- Typed execution events, event readers, and JSONL logs.
- Per-run security policy and secret redaction.
- Run-scoped resource lifecycle for clients, pools, containers, and future integrations.
Installation
Run the CLI without adding it to a project:
uvx --from "arazzo-exec[all]" arazzo-exec doctor
uvx --from "arazzo-exec[all]" arazzo-exec extensions
Add the SDK to a Python project:
uv add arazzo-exec
Traditional PyPI install:
python -m pip install arazzo-exec
Optional extras:
uv add "arazzo-exec[mapping]" # adaptix Retort support
uv add "arazzo-exec[di]" # dishka for CLI/integration DI helpers
uv add "arazzo-exec[kafka]" # confluent-kafka
uv add "arazzo-exec[db]" # psycopg + pymysql
uv add "arazzo-exec[temporal]" # temporalio
uv add "arazzo-exec[containers]" # testcontainers
uv add "arazzo-exec[schemathesis]" # schemathesis bridge
uv add "arazzo-exec[extensions]" # common heavy extension deps
uv add "arazzo-exec[all]" # all extras
arazzo-exec[core] is intentionally a no-op extra for UX compatibility. With one Python distribution, extras can only add dependencies; they cannot make the base install smaller.
Package layout
arazzo_exec/
core/
ast/ # ArazzoDocument, Workflow, Step, Action, Criterion, ExtensionBag
io/ # YAML/JSON parsing, source loading, source index/cache
operations/ # operationId/operationPath/channelPath resolver registry
execution/ # runner, planner, DAG scheduler, state, resources, results
executors/ # explicit step executor registry and selectors
extensions/ # public ABCs, registry, manifest, discovery, extension policy
events/ # typed events, sinks, readers, JSONL, redaction
auth/ # auth provider ABCs and default providers
security/ # per-run policy, network checks, secrets, redaction
validation/ # diagnostics and executable-profile validation
expressions/ # runtime expression engine and namespaces
criteria/ # standard criterion evaluation
extensions/
runtime/ # x-ae-timeout, x-ae-retry, x-ae-assert, x-ae-outputs
auth/ # runtime auth provider
jobs/ # x-ae-job
asyncapi/ # in-memory AsyncAPI executor
kafka/ # Kafka AsyncAPI executor
db/ # x-ae-db, x-ae-db-assert
temporal/ # x-ae-temporal
testcontainers/# runtime resources for Postgres, MySQL, Redpanda
schemathesis/ # optional future bridge
cli/ # arazzo-exec command line interface
Quick SDK usage
from arazzo_exec import ArazzoRunner
from arazzo_exec.extensions import build_default_registry
runner = ArazzoRunner.from_path(
"workflow.arazzo.yaml",
registry=build_default_registry(),
)
result = runner.run(
"happyPath",
inputs={"invoiceId": "inv-1001"},
runtime={"baseUrl": "http://127.0.0.1:8000"},
)
if not result.success:
print(result.error)
raise SystemExit(1)
print(result.outputs)
Async runner:
from arazzo_exec import AsyncArazzoRunner
from arazzo_exec.extensions import build_default_registry
runner = AsyncArazzoRunner.from_path(
"workflow.arazzo.yaml",
registry=build_default_registry(),
)
result = await runner.run(
"happyPath",
inputs={"invoiceId": "inv-1001"},
runtime={"parallel": True},
)
Parse only:
from arazzo_exec import parse_arazzo_path
document = parse_arazzo_path("workflow.arazzo.yaml")
workflow = document.workflow_by_id("happyPath")
step = workflow.step_by_id("createInvoice")
print(document.arazzo)
print(step.extensions.values)
Custom source types can provide an OperationResolver and a StepExecutor. The resolver answers "what operation does this Arazzo reference mean?" and the executor answers "how do I run it?"
from arazzo_exec.core import StepExecutorKind, StepExecutorRegistration
from arazzo_exec.core.executors import ResolvedOperationKindSelector
from arazzo_exec.core.operations import (
OperationResolverRegistration,
SourceTypeOperationResolverSelector,
)
registry.register_operation_resolver(
OperationResolverRegistration(
name="company.graphql.schema",
selector=SourceTypeOperationResolverSelector("graphql"),
resolver=CompanyGraphQLOperationResolver(),
produced_kinds=("graphql.operation",),
)
)
registry.register_step_executor(
StepExecutorRegistration(
name="company.graphql.http",
kind=StepExecutorKind.STANDARD_OPERATION,
selector=ResolvedOperationKindSelector("graphql.operation"),
executor=CompanyGraphQLExecutor(),
)
)
sourceDescriptions:
- name: users
type: graphql
url: ./schema.graphql
workflows:
- workflowId: userFlow
steps:
- stepId: getUser
operationId: $sourceDescriptions.users.GetUser
x-ae-executor:
name: company.graphql.http
Validate:
from arazzo_exec import build_validation_pipeline
from arazzo_exec.extensions import build_default_registry
pipeline = build_validation_pipeline(build_default_registry())
result = pipeline.validate(document)
for diagnostic in result.diagnostics:
print(diagnostic.severity, diagnostic.code, diagnostic.pointer, diagnostic.message)
CLI quick start
arazzo-exec doctor
arazzo-exec extensions
arazzo-exec validate workflow.arazzo.yaml
arazzo-exec list-workflows workflow.arazzo.yaml
arazzo-exec inspect workflow.arazzo.yaml --workflow happyPath
arazzo-exec env-check workflow.arazzo.yaml
Run a workflow:
arazzo-exec run workflow.arazzo.yaml \
--workflow happyPath \
--input inputs/happy-path.yaml \
--base-url http://127.0.0.1:8000
Run with JSONL events:
arazzo-exec run workflow.arazzo.yaml \
--workflow happyPath \
--input inputs/happy-path.yaml \
--events-jsonl reports/events.jsonl
arazzo-exec events reports/events.jsonl --count-by-type
arazzo-exec events reports/events.jsonl --errors
Run with security controls:
arazzo-exec run workflow.arazzo.yaml \
--workflow happyPath \
--dry-run \
--allow-host api.company.local \
--allow-method GET \
--allow-method POST \
--deny-private-network \
--no-remote-sources \
--no-job-execution
Load custom extensions:
arazzo-exec run workflow.arazzo.yaml \
--workflow happyPath \
--extension my_package.arazzo:MyExtension
arazzo-exec run workflow.arazzo.yaml \
--workflow happyPath \
--discover-extensions
Use Kafka executor explicitly:
arazzo-exec run workflow.arazzo.yaml \
--workflow kafkaFlow \
--kafka
Arazzo example
arazzo: 1.1.0
info:
title: Invoice workflow
version: 1.0.0
sourceDescriptions:
- name: billing
type: openapi
url: ./openapi.yaml
workflows:
- workflowId: happyPath
inputs:
type: object
properties:
invoiceId:
type: string
steps:
- stepId: createInvoice
operationId: $sourceDescriptions.billing.createInvoice
requestBody:
payload:
invoiceId: $inputs.invoiceId
successCriteria:
- condition: $statusCode == 201
x-ae-db-assert:
dsnEnv: AE_DATABASE_URL
queryOne: "select status from invoices where id = :invoice_id"
args:
invoice_id: $response.body.id
expect:
status: CREATED
x-ae-outputs:
invoiceId: $response.body.id
status: $response.body.status
outputs:
invoiceId: $steps.createInvoice.outputs.invoiceId
status: $steps.createInvoice.outputs.status
Built-in extension keys
Core/registry:
x-ae-runtime
x-ae-executor
x-ae-inputs
x-ae-parameters
Runtime controls:
x-ae-timeout
x-ae-retry
x-ae-successCriteria
x-ae-assert
x-ae-outputs
Execution extensions:
x-ae-job
x-ae-db
x-ae-db-assert
x-ae-temporal
x-ae-kafka
Runtime resources:
x-ae-runtime.testcontainers
x-ae-runtime.containers
Use your own namespace for project-specific extensions:
x-company-*
x-team-*
x-product-*
Executor selection
Step execution is registry-driven. There is no hidden priority or fallback logic.
A step gets exactly one primary executor from registered selectors:
- OpenAPI operation executor for OpenAPI
operationId/operationPath. - In-memory AsyncAPI executor for AsyncAPI
operationId/operationPath/channelPathin the default registry. - Kafka AsyncAPI executor when registered through
build_kafka_registry()or CLI--kafka. - Nested workflow executor for
workflowIdsteps. - Extension executors such as
x-ae-job,x-ae-db, orx-ae-temporal.
If more than one executor matches, validation and execution fail with an ambiguity error. Use x-ae-executor.name to select explicitly:
- stepId: publishOrder
operationId: $sourceDescriptions.events.publishOrder
action: send
x-ae-executor:
name: kafka.asyncapi
x-ae-kafka:
connection: default
Runtime config
Runtime can be passed from SDK as a dict or declared at the root via x-ae-runtime.
x-ae-runtime:
parallel: true
baseUrl: http://127.0.0.1:8000
auth:
billing:
bearerEnv: BILLING_TOKEN
headers:
X-Tenant: tenant-a
security:
dryRun: false
allowedHosts:
- 127.0.0.1
- api.company.local
allowedMethods:
- GET
- POST
denyPrivateNetwork: false
allowJobExecution: true
maxResponseBytes: 1048576
testcontainers:
postgres:
billing:
image: postgres:16
redpanda:
default: {}
SDK runtime overrides root runtime:
result = runner.run(
"happyPath",
runtime={"parallel": False, "security": {"dryRun": True}},
)
Events
Every run emits typed events:
result = runner.run("happyPath")
reader = result.event_reader()
print(reader.count_by_type())
print(reader.failed_steps())
Use JSONL for persistent logs:
from arazzo_exec import ArazzoRunner, JsonlEventSink, EventBus, RuntimeServices
from arazzo_exec.extensions import build_default_registry
sink = JsonlEventSink("events.jsonl")
services = RuntimeServices.default_local(event_bus=EventBus(sinks=(sink,)))
runner = ArazzoRunner.from_path("workflow.arazzo.yaml", registry=build_default_registry(), services=services)
result = runner.run("happyPath")
Read logs back:
from arazzo_exec import JsonlEventLog
reader = JsonlEventLog.read("events.jsonl")
print(reader.by_type("step.failed"))
Custom extension skeleton
from dataclasses import dataclass
from typing import Any
from arazzo_exec.core import (
ArazzoExtension,
ExtensionManifest,
ExtensionRegistry,
StepExecutor,
StepExecutorKind,
StepExecutorRegistration,
StepResult,
)
from arazzo_exec.core.executors import ExtensionFieldSelector
@dataclass(frozen=True, slots=True)
class CompanyCheckExecutor(StepExecutor):
async def execute(self, request, spec: Any) -> StepResult:
actual = request.evaluator().evaluate(spec["actual"])
expected = request.evaluator().evaluate(spec["expected"])
success = actual == expected
return StepResult.succeeded(
request.step.step_id,
outputs={"actual": actual, "expected": expected, "success": success},
) if success else StepResult.failed(
request.step.step_id,
f"Expected {expected!r}, got {actual!r}",
)
@dataclass(frozen=True, slots=True)
class CompanyExtension(ArazzoExtension):
@property
def manifest(self) -> ExtensionManifest:
return ExtensionManifest(
name="company.check",
version="1.0.0",
keys=("x-company-check",),
package_extra=None,
description="Company assertion executor.",
)
def register(self, registry: ExtensionRegistry) -> None:
registry.register_step_executor(
StepExecutorRegistration(
name="company.check",
kind=StepExecutorKind.EXTENSION_OPERATION,
selector=ExtensionFieldSelector("x-company-check"),
executor=CompanyCheckExecutor(),
)
)
Register manually:
from arazzo_exec.extensions import build_default_registry
registry = build_default_registry()
registry.register_extension(CompanyExtension())
Or expose an entry point:
[project.entry-points."arazzo_exec.extensions"]
company_check = "my_package.arazzo:CompanyExtension"
More documentation
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arazzo_exec-0.1.0.tar.gz.
File metadata
- Download URL: arazzo_exec-0.1.0.tar.gz
- Upload date:
- Size: 97.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eaa9c7be8755884a832da759dd69a23d940418b3dcb96bde77db53dc0bdf4425
|
|
| MD5 |
432bd9757d6920683144ee0729f670bb
|
|
| BLAKE2b-256 |
e5f0a30effbc9ac9a381ad16f419f1d6e977063a05e1d3129a1d9bb8e1367818
|
File details
Details for the file arazzo_exec-0.1.0-py3-none-any.whl.
File metadata
- Download URL: arazzo_exec-0.1.0-py3-none-any.whl
- Upload date:
- Size: 154.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8c76fdfedd7929b1453cf2aa450f2137575e91e4e605a85aeb7e69920650be7
|
|
| MD5 |
bedfc94b804d243376ebef1b2895c649
|
|
| BLAKE2b-256 |
809d8841b48308f8d275fe8e169e1a4664bee9efd50c52e23552cd1091876e50
|