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.

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), 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 = 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):

  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 stepIds 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

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; none is a dependency.

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. 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

A CLI command that runs the resolution half of simulate without a subject (CLI: arazzo):

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 (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).

Mapping table

Arazzo conceptPermDock concept
WorkflowA plan passed to simulate({ arazzo, openapi }) on the instance
Step with operationId / operationPathOne [permission, data] entry per key in the operation's x-permdock-permissions
Step with workflowIdExpanded 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 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

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

Last updated on

On this page