POST /api/v1/workflow/instances # Start a workflow instance
GET /api/v1/workflow/instances?entity-type=&entity-id= # Find an entity's instances
POST /api/v1/workflow/instances/:id/transition # Perform a state transition
GET /api/v1/workflow/instances/:id # Get current state
GET /api/v1/workflow/instances/:id/audit # Get audit trail
Every route requires authentication (session or bearer token) and answers 401 without it.
The three reads and the start also need the :admin role or a :workflow/read grant from one of the caller’s groups, and answer 403 without either.
If your application declares the permissions a group may grant, declare :workflow/read among them.
A transition needs no grant: its :required-permissions decide.
In a multi-tenant application every route answers 403 to a caller outside the request’s tenant: not a member of it, not one of its users and not a platform admin.
Without a tenant, only a user of no tenant is served.
Inside a tenant, the admin that reads is the tenant’s: a platform admin, an admin by membership, or an admin user of that tenant; another tenant’s admin is not one.
A grant counts there only when its scope names the tenant, {:tenant-id "<id>"}.
The admin pages under /web/admin/workflows admit the tenant’s admins on the same terms, so a tenant admin sees their own tenant’s instances.
Bodies are kebab-case JSON, as in a scaffolded module’s API. Start an instance with:
{"workflow-id": "order-workflow",
"entity-type": "order",
"entity-id": "5b1c9a52-6f0e-4d7a-9a57-2d9e3c1b8f40"}
A transition answers the instance and its audit entry. The instance carries the
transitions available from its new state to the caller, as GET /instances/:id does:
{"instance": {"id": "0d4b7f3e-2a61-4c55-8f0e-9b7a1c2d3e4f",
"workflow-id": "order-workflow",
"entity-type": "order",
"entity-id": "5b1c9a52-6f0e-4d7a-9a57-2d9e3c1b8f40",
"current-state": "delivered",
"available-transitions": [{"id": "paid", "to": "paid", "enabled": true}],
"created-at": "2026-09-27T12:00:00Z",
"updated-at": "2026-09-27T12:05:00Z"},
"audit-entry": {"id": "7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
"instance-id": "0d4b7f3e-2a61-4c55-8f0e-9b7a1c2d3e4f",
"transition": "delivered",
"from-state": "entered",
"to-state": "delivered",
"actor-id": "2f6c8a1e-5b3d-4e7f-9a0b-1c2d3e4f5a6b",
"actor-roles": ["admin"],
"occurred-at": "2026-09-27T12:05:00Z"}}
A transition not enabled for the caller has "enabled": false and a reason:
insufficient-permissions, or guard-rejected when a guard refuses it.
One the workflow does not make from the current state answers 422:
{"error": {"type": "transition-not-found",
"message": "Transition 'paid' is not allowed from state 'entered'",
"correlation-id": "3f2b6c1e-8d4a-4e7b-9c1f-2a5d6e7f8a9b"}}
An unknown instance is a 404 and a malformed body a 400, in the platform’s error shape.
To find an entity’s instance, look it up by the entity’s type and id:
GET /api/v1/workflow/instances?entity-type=order&entity-id=5b1c9a52-6f0e-4d7a-9a57-2d9e3c1b8f40
It answers a list, one instance per workflow the entity is in, and [] for none.
Both parameters are required. GET /instances/:id adds the available transitions.
[{"id": "0d4b7f3e-2a61-4c55-8f0e-9b7a1c2d3e4f",
"workflow-id": "order-workflow",
"entity-type": "order",
"entity-id": "5b1c9a52-6f0e-4d7a-9a57-2d9e3c1b8f40",
"current-state": "entered",
"created-at": "2026-09-27T12:00:00Z",
"updated-at": "2026-09-27T12:00:00Z"}]
available-transitions is the same list wherever it appears, next to the state
it was computed from: on the instance here, and on a scaffolded entity’s
workflow after POST /api/v1/<entities>/:id/transition. Each item has id,
to and enabled, a label when the transition has one, and a reason when
it is not enabled. The entity’s transition answers:
{"id": "5b1c9a52-6f0e-4d7a-9a57-2d9e3c1b8f40",
"number": "A-1",
"status": "delivered",
"workflow": {"instance-id": "0d4b7f3e-2a61-4c55-8f0e-9b7a1c2d3e4f",
"state": "delivered",
"available-transitions":
[{"id": "paid", "to": "paid", "label": "Paid", "enabled": true},
{"id": "void", "to": "void", "enabled": false, "reason": "insufficient-permissions"}]}}