PermDock
Standards

Arazzo workflows

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.

Status: planned Phase: 4

What it is

The Arazzo Specification 1.1.0 (released 17 May 2026, announcement) describes workflows: sequences of API calls with dependencies between them, inputs, outputs and success criteria. An Arazzo document has:

FieldMeaning
arazzoThe Arazzo specification version, 1.1.0
infoTitle and version of the workflow document
sourceDescriptionsThe API descriptions the workflows call into: type: openapi, type: arazzo (another workflow document) and, since 1.1, type: asyncapi
workflowsNamed 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

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, 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 3.2), 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).

How PermDock uses it

A workflow is an agent plan

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 = createPermDock(policy, { subject })                          // subject: the principal (and actor) the plan runs for

const plan = await permdock.simulate({ arazzo, openapi, subject, workflowId: 'publishPost' })   // subject shape: see open questions
// {
//   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, subject }) does, step by step:

  1. Validates the Arazzo document (arazzo: 1.1.0 or 1.0.x) and finds the sourceDescriptions of type: openapi. The openapi argument supplies the resolved description for those sources; a workflow with several OpenAPI sources takes a map keyed by source name.
  2. For each step of the selected workflow, resolves operationId (or operationPath) to an operation in the description and reads its x-permdock-permissions. Steps that reference a workflowId are expanded recursively; cycles are an error.
  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 step's parameters and the workflow inputs are used to build the resource identifier; when the instance cannot be loaded ahead of time (it is produced by an earlier step's outputs), the decision is evaluated with the instance-independent part of the grant and marked provisional: true, meaning protect will decide again at execution time with the real row.
  5. All [permission, data] pairs are passed to the existing simulate batch, so the semantics are unchanged: no on('decision') events per step (one simulate event for the plan), no approval tokens issued, no quota consumed.
  6. The result lists one Decision per step in workflow order plus the worst outcome across steps.

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). 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 compiles an arazzo block into workflow files after the spec write, against the generated operationIds, 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); simulate sits between the two as the authorization pre-flight.

PermDock composes with these rather than wrapping them (ADR 0023); none is a dependency, and none moves simulate earlier than Phase 4.

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. Whether one approval can cover a whole plan is an open question on that page; until it is decided, each approval-required step needs its own approval and the step's decision carries what to ask for.

permdock arazzo check (later)

A CLI command, planned for Phase 4 and not yet listed in the CLI command table, that runs the resolution half of simulate without a subject:

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

  • AsyncAPI steps. Arazzo 1.1 allows sourceDescriptions of type: asyncapi and send / receive style steps. 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 until an adapter exists. This is not a Phase 4 deliverable.
  • 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).

Mapping table

Arazzo conceptPermDock concept
WorkflowA plan passed to simulate({ arazzo, openapi, subject })
Step with operationId / operationPathOne [permission, data] entry per key in the operation's x-permdock-permissions
Step with workflowIdExpanded into that workflow's steps
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, step parametersResource identifier for instance-level permissions
outputs feeding a later stepprovisional: true decision; re-decided by protect at execution
successCriteriaNot evaluated; PermDock decides authorization, not success
Operation without x-permdock-permissionsdenied, reason undocumented (fail closed)
Whole workflowoutcome = worst step outcome; one simulate audit event

Sources

Open questions

  • The exact simulate overload: an object argument on the instance as shown, or a separate permdock/arazzo entry, and whether subject is taken from the instance or passed explicitly for pre-flighting on behalf of another principal.
  • How provisional decisions should be presented so an agent does not treat them as final grants.
  • Whether permdock arazzo check belongs under permdock openapi as a subcommand, since it reads the same description.
  • Whether a plan-level approval token (one approval for all approval-required steps) is acceptable; tracked on approvals.

On this page