# Arazzo workflows

Source: https://permdock.com/docs/standards/arazzo

How an Arazzo 1.1.0 workflow is an agent plan, and how permdock simulate pre-flights every step by resolving its operationId to the operation's x-permdock-permissions and returning one Decision per step before anything executes.

## What it is [#what-it-is]

The [Arazzo Specification 1.1.0](https://spec.openapis.org/arazzo/v1.1.0.html) (released 17 May 2026, [announcement](https://www.openapis.org/arazzo-specification)) describes workflows: sequences of API calls with dependencies between them, inputs, outputs and success criteria. An Arazzo document has:

| Field | Meaning |
| --- | --- |
| `arazzo` | The Arazzo specification version, `1.1.0` |
| `info` | Title and version of the workflow document |
| `sourceDescriptions` | The API descriptions the workflows call into: `type: openapi`, `type: arazzo` (another workflow document) and, since 1.1, `type: asyncapi` |
| `workflows` | Named workflows, each with `inputs`, ordered `steps`, `outputs` and success criteria |
| `steps[]` | Each step points at one `operationId` or `operationPath` in a source description, or at another `workflowId`; it declares `parameters`, `requestBody`, `successCriteria`, `outputs` and what to do on success or failure |

1.1 adds AsyncAPI sources (send / receive style steps with correlation), typed `parameters` on actions that call another `workflowId`, and better data selection (Selector Object, JSONPath). 1.0.x documents remain valid.

## Why it matters for PermDock [#why-it-matters-for-permdock]

An Arazzo workflow is a plan: an ordered list of operations somebody intends to call, with the data flowing between them. That is exactly what an agent produces before it acts, and exactly what `permdock.simulate` was designed to evaluate ([decisions](/docs/concepts/decisions), [for AI agents](/docs/for-ai-agents)). Today `simulate` takes an array of `[permission, data]` pairs the caller assembles by hand. An Arazzo document assembles them for free: each step names an `operationId`, the referenced OpenAPI description carries `x-permdock-permissions` on that operation ([OpenAPI](/docs/standards/openapi)), and the permission keys resolve to leaves in the catalog.

The result is a pre-flight for a whole workflow, not a single call: before an agent (or a workflow runner, or a CI job) executes step one, PermDock reports which steps would be `granted`, which `denied` and with what alternatives, and which would stop at `approval-required`, so the approval can be requested up-front instead of halfway through a plan with side effects already committed. This is the OWASP ASI02 "bound the plan before the first side effect" control applied to a standard workflow format ([OWASP agentic](/docs/security/owasp-agentic)).

## How PermDock uses it [#how-permdock-uses-it]

### A workflow is an agent plan [#a-workflow-is-an-agent-plan]

```ts
import { readFile } from "node:fs/promises";
import { PermDockDeniedError } from "permdock";
import { createPermDock } from "permdock/node";
import { policy } from "./policy";

const arazzo = JSON.parse(
  await readFile("./workflows/publish-post.arazzo.json", "utf8"),
);
const openapi = JSON.parse(await readFile("./openapi.json", "utf8")); // after the Overlay is applied

const permdock = await createPermDock(policy, user);

const plan = permdock.simulate({ arazzo, openapi, workflowId: "publishPost" });
// {
//   workflowId: 'publishPost',
//   outcome: 'approval-required',            // worst outcome across steps: denied > approval-required > granted
//   steps: [
//     { stepId: 'loadDraft',  operationId: 'getPost',     permissions: [permissions.post.read],    decision: { outcome: 'granted', ... } },
//     { stepId: 'editDraft',  operationId: 'updatePost',  permissions: [permissions.post.update],  decision: { outcome: 'granted', ... } },
//     { stepId: 'publish',    operationId: 'publishPost', permissions: [permissions.post.publish], decision: { outcome: 'approval-required', reason: 'human', ... } },
//   ],
// }

const blocked = plan.steps.filter((s) => s.decision.outcome !== "granted");
if (blocked.some((s) => s.decision.outcome === "denied"))
  throw new PermDockDeniedError(blocked[0].decision);
for (const step of blocked) await requestApproval(step); // approval-required steps, before step one runs
```

What `simulate({ arazzo, openapi })` does, step by step (subject is the instance's, [Arazzo](/docs/standards/arazzo)):

1. Validates the Arazzo document against the fields the 1.0 and 1.1 schemas require: an `arazzo` version of `1.0.x` or `1.1.x`, `info`, at least one source description with a `name` and `url`, at least one workflow, and at least one step per workflow. Each step needs a `stepId` that is unique in its workflow and exactly one of `operationId`, `operationPath` or `workflowId`, and each parameter needs a `name` and a `value` or a resolvable `$components.parameters` reference. A document that fails any of these produces one `document` step denied with reason `validation`. The `openapi` argument supplies the resolved description for the `type: openapi` sources: one description, or a map keyed by source `name` for a workflow with several.
2. For each step of the selected workflow, resolves `operationId` (or `operationPath`) to an operation in the description and reads its `x-permdock-permissions`. A qualified `$sourceDescriptions.<name>.<operationId>` and an `operationPath` of the form `{$sourceDescriptions.<name>.url}#/paths/...` select their source by name. A plain `operationId` must match exactly one source; one that matches several is `undocumented`, as the spec requires the qualified form there. Steps that reference a local `workflowId` are expanded recursively, with the calling step's `parameters` as the nested workflow's `inputs`; cycles and unknown workflows are a `validation` error, and a `workflowId` in another Arazzo document is `unsupported`. A nested step's result carries the `workflowId` that owns it, so equal `stepId`s in different workflows stay separate.
3. Each permission key is looked up with `findPermission` in the catalog. An operation with no `x-permdock-permissions` produces a step with `permissions: []` and `decision.outcome: 'denied'` with reason `undocumented`; a workflow cannot be cleared through a hole in the description (fail closed).
4. For `actions` permissions (those that take an instance), the workflow `inputs`, the workflow-level `parameters` and the step's `parameters` (the step's win on a name clash) build the resource data. A `$inputs.<path>` value is read from the inputs. Any other runtime expression, such as an earlier step's `$steps.<id>.outputs`, or an input that is absent, cannot be known ahead of time: the decision is evaluated without that value and marked `provisional: true`, meaning `protect` will decide again at execution time with the real row.
5. All `[permission, data]` pairs are evaluated with the `simulate` batch semantics: no `on('decision')` events per step, no approval tokens issued, no quota consumed. One `simulate` event is emitted for the plan, carrying the worst step decision and `counts` of granted, denied and approval-required checks.
6. The result lists one `Decision` per step in workflow order plus the worst `outcome` across steps.

### Where the documents come from [#where-the-documents-come-from]

`simulate` reads the **applied** description: the producer's output with PermDock's Overlay merged, not the source before it. Without the Overlay no operation carries `x-permdock-permissions` and every step is `denied` with reason `undocumented` ([OpenAPI Overlay](/docs/standards/openapi-overlay)). Arazzo documents themselves come from wherever the team writes them; two producers worth naming because they share the `operationId` join key with PermDock:

* [next-openapi-gen](https://github.com/tazo90/next-openapi-gen/blob/main/docs/arazzo.md) compiles an `arazzo` block into workflow files after the spec write, against the generated `operationId`s, and applies Overlay files first, so its output is already the applied description.
* Redocly CLI's `generate-arazzo` drafts a workflow from an OpenAPI description and `respect` executes one ([commands](https://redocly.com/docs/cli/commands)); `simulate` sits between the two as the authorization pre-flight.

PermDock composes with these rather than wrapping them; none is a dependency.

### Surfacing approval-required steps [#surfacing-approval-required-steps]

An `approval-required` step tells the caller that executing it will stop for a human (or a policy-defined approver) unless approval is obtained first. Because `simulate` issues no tokens, the caller uses the step list to request approval before running the workflow; the approval flow and the binding of an approval token to permission key, resource id, subject and actor are described on [approvals](/docs/security/approvals). There is no plan-level token: each `approval-required` step yields its own approval request, a reviewer may approve them together, and each step is still re-checked at execution. The step's `decision` carries what to ask for.

### `permdock arazzo check` [#permdock-arazzo-check]

A CLI command that runs the resolution half of `simulate` without a subject ([CLI: arazzo](/docs/cli/arazzo)):

```bash
permdock arazzo check --doc workflows/publish-post.arazzo.json --openapi openapi.json
```

It fails (exit `1`) when a step's `operationId` does not exist in the referenced description, when the operation has no `x-permdock-permissions`, or when a key does not exist in the catalog. It is the workflow counterpart of `permdock openapi --check`: the document says what each call requires, and the workflow must only call documented operations.

### Out of scope [#out-of-scope]

* **AsyncAPI steps.** Arazzo 1.1 allows `sourceDescriptions` of `type: asyncapi` and send / receive style steps (an `operationId` in an AsyncAPI source, or a `channelPath`). PermDock has no AsyncAPI adapter and does not emit `x-permdock-permissions` into AsyncAPI documents, so those steps are reported as `unsupported` and the plan's outcome is `denied`.
* **Workflows in other Arazzo documents.** A step whose `workflowId` points into a `type: arazzo` source is `unsupported`; resolve it into the document first.
* **Executing workflows.** PermDock pre-flights and guards; it does not run steps. Runners keep calling the API, where `protect` decides for real.
* **Import from Arazzo.** Generating permission definitions from a workflow document is not planned; permissions come from the OpenAPI description or the catalog ([CLI: openapi](/docs/cli/openapi)).

## Mapping table [#mapping-table]

| Arazzo concept | PermDock concept |
| --- | --- |
| Workflow | A plan passed to `simulate({ arazzo, openapi })` on the instance |
| Step with `operationId` / `operationPath` | One `[permission, data]` entry per key in the operation's `x-permdock-permissions` |
| Step with `workflowId` | Expanded into that workflow's steps, its `parameters` becoming that workflow's `inputs` |
| `sourceDescriptions[type=openapi]` | The `openapi` argument (one description or a map by source `name`) |
| `sourceDescriptions[type=asyncapi]` | Unsupported until an AsyncAPI adapter exists; step `denied` |
| `inputs`, workflow and step `parameters`, `$inputs.*` | Resource data for instance-level permissions |
| `outputs` feeding a later step | `provisional: true` decision; re-decided by `protect` at execution |
| `successCriteria` | Not evaluated; PermDock decides authorization, not success |
| Operation without `x-permdock-permissions` | `denied`, reason `undocumented` (fail closed) |
| Whole workflow | `outcome` = worst step outcome; one `simulate` audit event |

## Sources [#sources]

* [Arazzo Specification 1.1.0](https://spec.openapis.org/arazzo/v1.1.0.html).
* [Arazzo announcement, OpenAPI Initiative](https://www.openapis.org/arazzo-specification).
* [Standards watch list](/docs/standards/watch-list): the "workflow-step permission requirements" item.
* [OpenAPI ecosystem research](/docs/research/ecosystem-index): the producers that emit Arazzo files and the applied-description rule.

## Related [#related]

* [Decisions](/docs/concepts/decisions): `simulate` semantics.
* [OpenAPI](/docs/standards/openapi) and [OpenAPI Overlay](/docs/standards/openapi-overlay): where `x-permdock-permissions` comes from.
* [OpenAPI registries](/docs/standards/openapi-registry): the extension namespace.
* [Approvals](/docs/security/approvals): what happens to `approval-required` steps.
* [OWASP agentic](/docs/security/owasp-agentic): plan bounding as an ASI02 control.
* [AuthZEN](/docs/standards/authzen): the boxcar wire form `simulate` already uses.
* [For AI agents](/docs/for-ai-agents).

A `provisional: true` step is not a final grant. The agent must not treat it as cleared; `protect` decides again with the real row.
