#Process Models
A process is the set of steps an item goes through: a supplier is onboarded when its documents are in, it has passed screening and someone has approved it. You describe the steps once, in a process definition; a tenant activates the definition by name; people move the steps from an app page and workflows move them from outside signals. Every move is judged by the platform (is it declared, is it ready, is its evidence there, may this caller make it) and written to a log that says who moved what, when, and on what evidence.
This guide builds one process end to end against the API: a supplier onboarding process with three steps, moved by a workflow and by two app users, and read back from the API and from a rendered app page. Every request below was run, and every response is the platform's answer (trimmed).
Before you start, read Entities & Data Model, Workflows and Apps (Bridge). A process is made of their parts.
#How a process is put together
| Concept | What it is |
|---|---|
| Anchor | The item the process runs on: an entity of the type the definition names in anchor (here, a Supplier). |
| Step | One entry of emits. On each anchor, a step is a row of an entity type you own (here, OnboardingStep), linked to its anchor. The row holds the step's state in its attributes. |
| Preset | The state machine a step follows: milestone or alert. A preset fixes the states (rungs) and the transitions between them; a step declares the transitions it uses. |
| Move | One transition of one step on one anchor, made by a person or a producer. A move that passes its gates commits; a person's move that does not is refused; an automated move that does not is held. |
| Origin | Who makes a move: manual (a person, from an app), detection, linked, poller or backfill (a workflow). |
| Edge | A named one-hop path over a relationship rule, from the step row (or the anchor) to related rows. Edges link a step to its anchor and to its evidence. |
| Effect | Work a move runs before it commits (before), in the mover's request, as the mover. |
| Activation | A tenant's use of a definition, by name: which version, its config (and the credentials effects use), its job bindings, its override, and whether it is enabled. |
The steps of a process never move on their own: something makes each move, and the platform decides whether it lands.
#1. Model the data
The example has three entity types. Supplier is the anchor. OnboardingStep
holds the steps. SupplierDocument is the evidence a step waits on.
A step type has one rule: its identifier key is exactly the step's key
attribute, untransformed. Every step row carries a unique key (here
<supplier id>:documents), and the platform finds a step's row by it. The step
type's schema may declare the other attributes the platform writes (state,
source, state_at, occurrence); give them no required beyond the key.
POST/schema/schemasRegister the step schemaAPI docs ↗Try it
The two other schemas are supplier_attributes (name and status, both
required) and supplier_document_attributes (title required, and a
supplier_id).
POST/entity/entity-typesCreate the step typeAPI docs ↗Try it
Supplier and SupplierDocument are created the same way, with "config": {}.
Then two relationship rules: one from the anchor type to the step type (the
step's link to its supplier), and one from the step type to the evidence type.
POST/entity/relationship-rulesLink suppliers to their stepsAPI docs ↗Try it
POST/entity/relationship-rulesLink steps to their evidenceAPI docs ↗Try it
#2. Write the definition
A definition is authored in camelCase, as one JSON object, and published as the
definition_json of POST /definition/processes. Publish one version: the body
is checked as the runtime will run it, against your catalog (every type, edge
and rule must resolve, every expression may read only what its position can
see), and every refusal comes back at once.
POST/definition/processesPublish a process definitionAPI docs ↗Try it
What it says:
documentswaits for a signed document. A person can mark it received, but only with a document linked (until); a workflow can mark it received from adocuments_receivedsignal, linking the document the signal names. When the document is missing, a person can release the workflow's held move by hand (hold.manual).screeningis moved only by a workflow (by: ["detection"]), afterdocumentsis done (dependsOn).reviewfollows screening. A person starts it, approves it (which runsmark_supplier_approvedfirst), may undo, and may start a new review later (restart, a new occurrence).- The process applies only to suppliers whose
statusis notblocked(condition).
#Top-level keys
| Key | Required | Meaning |
|---|---|---|
key |
✓ | The process's name: letters, digits and _ (no -, no __). It must equal the name you publish under; activations and moves name the process by it. |
label |
✓ | The process's display name. |
anchor |
✓ | The anchor's entity type, by name. |
condition |
A filter over the anchor (the platform filter grammar): the process applies only where it holds. | |
policy |
✓ | { name, grants }, the shape of a workflow definition's policy. |
config |
The slots an activation fills: a JSON schema object (type: "object", properties, required). Read as $config.<slot>. A slot with "writeOnly": true holds a credential. |
|
auth |
Named HTTP auth profiles an effect's http action names ("auth": "<profile>"). A profile reads {{ config.<slot> }} only. |
|
edges |
Named one-hop paths (below). | |
fields |
The inputs transitions take (below). | |
effects |
Named effects that transitions run (below). | |
emits |
✓ | The steps, keyed by name (at least one). |
actions |
Non-transition controls: { label, href?, form?, writes? }. |
A definition carries words only (label, cta, rung labels, field and
evidence labels). How a step is shown (order, icons, confirmations, which
surface a control appears on) belongs to the app's presentation (see
Show the process on an app page).
#Presets, rungs and transitions
A step's preset is its state machine. It declares only the transitions it uses; a transition it does not declare does not exist on that step.
milestone rungs: READY → IN_PROGRESS (only when initiate or
restart is declared) → PENDING_APPROVAL (only when approve is declared) →
DONE.
| Transition | From | To | Waits on dependsOn |
Default label |
|---|---|---|---|---|
initiate |
READY |
IN_PROGRESS |
yes | Start |
complete |
READY, IN_PROGRESS |
PENDING_APPROVAL when approve is declared, else DONE |
yes | Complete |
approve |
PENDING_APPROVAL |
DONE |
yes | Approve |
restart |
DONE, latest only |
IN_PROGRESS on a new occurrence |
yes | Start again |
undo |
any rung above READY |
mode: "one" (default): the rung before the undone move; mode: "clear": READY |
no | Undo |
alert rungs: OPEN → ACKNOWLEDGED → RESOLVED.
| Transition | From | To | Waits on dependsOn |
Default label |
|---|---|---|---|---|
acknowledge |
OPEN |
ACKNOWLEDGED |
yes | Acknowledge |
resolve |
OPEN, ACKNOWLEDGED |
RESOLVED |
no | Resolve |
reopen |
RESOLVED |
OPEN, on the same row |
yes | Reopen |
raise |
RESOLVED, latest |
OPEN on a new occurrence |
yes | — |
Publish refuses an approve without complete, both reopen and raise on
one step, and a restart or raise whose key does not template the
occurrence. A person is offered complete from READY only when the step
declares no initiate; a workflow keeps every from rung the table lists.
Each rung shows a word: the presentation's, else the step's labels, else the
rung's own name in words (Ready, In progress, Pending approval, Done,
Open, Acknowledged, Resolved). An undo from PENDING_APPROVAL is labelled
Send back.
#Steps (emits entries)
| Key | Required | Meaning |
|---|---|---|
preset |
✓ | milestone or alert. |
type |
✓ | The step row's entity type, by name. Its identifier key is exactly the key role attribute, with transform: "none". |
anchor |
✓ | The edge from the step row to its anchor. A step row the platform creates is linked through it. |
key |
✓ | A {{ }} template for the row's identity. A step that opens occurrences templates {{ $row.attributes.occurrence }}. |
fields |
Which attribute plays each role, only where it differs from the default: key → key, state → state, source → source, stateAt → state_at, occurrence → occurrence, occurrenceKey → occurrence_key. |
|
labels |
Words per rung, e.g. { "READY": "Waiting" }. |
|
attributes |
Other attributes written when the platform creates the row (never a role attribute). | |
dependsOn |
[{ key, state?, whenInapplicable?, condition? }]: the steps (<emit>, or <process>.<emit> in another process) that must be done before this step's forward transitions. |
|
triggers |
The signals your workflows act on: detection: [{ signal, transition, from? }], linked: [{ edge, where, transition, from? }], poll: [{ on, transition, when?, create? }]. Publish checks them; workflows run them. |
|
transitions |
✓ | The preset transitions this step declares (below). |
The platform writes the role attributes on every move: the step's state, its
source (manual for a person, detected for a detection move, data for a
linked or poller move, a backfill's own source), when it reached the
state, and its occurrence number. Never write them yourself.
#Transitions
| Key | Meaning |
|---|---|
by |
The origins that may make it. Default ["manual"]: a person only. A person's moves are exactly the transitions whose by includes manual. |
label, cta |
The control's word, and true for the step's call to action. |
mode |
undo only: one or clear. |
enabledWhen |
A filter over the step row ({ id, attributes, edges }): the transition is offered only where it holds. |
inputs, bind |
The fields a person fills in; bind.<origin> fills inputs for an automated origin, usually from $payload. |
set |
Attributes the move writes on the step row (smart values over $inputs, $row, $anchor, …). |
links |
[{ edge, to }]: rows the move links through a declared edge. A detection transition must link the row its signal names ($payload.<name>). |
until |
[{ edge, label, where?, min? }]: evidence. The move needs at least min (default 1, at most 25) rows linked through the edge, counting the move's own links. |
hold |
{ "manual": true }: a held move may also be released by a person, by hand. |
before |
Effects run in the mover's request, as the mover, before the move commits. A list, or { "<origin>": [...], "default": [...] }. |
compensate |
Effects run when this move is undone. |
machineContinues |
Origins that may move a step past a person's earlier decision. |
after, onSuccess, onFailure, hold.after and an await effect describe
durable work that runs after a move commits. Durable effects are not supported:
publish refuses each with DURABLE_EFFECTS_UNSUPPORTED, so a hold is released
by data or by hand, never by time.
#Edges and conditions
An edge is { via, from?, where?, sort?, limit? }: via names a relationship
rule, and the direction follows from the rule's two types. from is the root:
row (the step row, the default), anchor, another edge, or
{ "value": <smart value> }. Conditions (condition, enabledWhen, where,
item when) use the platform filter grammar and reach relationships only
through edges: { "operator": "rel_exists", "path": ["edges", "documents"] }.
#Fields
A field is
{ label, type, required?, max?, options?, dependsOn?, accept?, default? } with
type one of text, textarea, date, select, multiselect, upload.
max is required on text, textarea and multiselect; options belongs to
select and multiselect (a literal list, { query }, { edge } or
{ enum }); accept belongs to upload. A transition names the fields it
takes in inputs, and a move's inputs are checked against them
(INPUT_INVALID).
#Effects
An effect declares exactly one of:
actions— an inline list of actions, as a workflow's inline job step, with an optionaloutput;kind— a job, bound to the sloteffect_<kind>by the definition'sdefault-job-bindingsor by the activation'sjob_bindings(see Job Bindings).
Its input is a record of smart values over the move's scopes, and the actions
read it as input.*:
| Scope | What |
|---|---|
$anchor |
The anchor row (id, attributes). |
$row |
The step row as the move will leave it. |
$inputs |
The move's inputs. |
$payload |
A detection or linked move's signal. |
$edges.<edge> |
A declared edge's rows. |
$effects.<name>.output |
An earlier effect's output in the same move. |
$config.<slot> |
The activation's config slot. |
$itemKey |
A key stable across retries of the same move from the same place, for idempotent effects. |
onError is abort (the default) or continue. An effect runs as whoever
makes the move, under their own permissions: a move whose effect writes what the
mover may not write fails. A failed effect fails the move: the step keeps its
rung, shows the Failed badge, and the next press of the same control performs
it again.
Credentials come from the activation only. A definition holds no secret
reference (publish refuses one with SECRET_REF_IN_DEFINITION), and a job bound
to an effect slot pins none (SECRET_REF_IN_EFFECT_JOB). An effect reaches a
credential through an auth profile that reads a writeOnly config slot, which
the activation fills with a secret reference
(Activate it).
#Occurrences
restart (milestone) and raise (alert) open a new occurrence: a new step
row with the next occurrence number, whose key templates
{{ $row.attributes.occurrence }}. A move may name its occurrence with
occurrenceKey (for example a port name), stored in the occurrenceKey role
attribute. The app's step view shows the latest occurrence; the status read
lists every occurrence.
#3. Publish the definition
The request in step 2 answers 201, with the stored
definition under data and the lints it published with under warnings. The
stored body has its defaults filled in (an edge's from: "row", an effect's
onError: "abort", a dependency's whenInapplicable: "satisfied"):
{
"data": {
"id": "b259f319-10de-4030-a072-7e7376105348",
"name": "supplier_onboarding",
"version": "1.0.0",
"status": "active",
"label": "Supplier onboarding",
"description": null,
"definition_json": {
"key": "supplier_onboarding",
"edges": {
"supplier": { "via": "supplier_onboarding_steps", "from": "row" },
"documents": { "via": "onboarding_step_documents", "from": "row" }
},
"…": "…"
},
"source_type": "owned"
},
"warnings": []
}
A body that does not publish answers 422 with every refusal at its path. This
one adds an after list, a time release and a step type that does not exist:
{
"error": {
"status": 422,
"code": "VALIDATION_FAILED",
"message": "The process definition does not publish.",
"details": {
"findings": [
{
"path": "emits.documents.transitions.complete.hold.after",
"code": "DURABLE_EFFECTS_UNSUPPORTED",
"message": "a time release is durable: durable effects are not supported, a hold releases by data or by hand"
},
{
"path": "emits.screening.type",
"code": "NAME_UNRESOLVED",
"message": "ScreeningStep does not resolve"
},
{
"path": "emits.review.transitions.complete.after",
"code": "DURABLE_EFFECTS_UNSUPPORTED",
"message": "after is durable: durable effects are not supported, only before runs"
}
]
}
}
}
A body that does not parse answers 422 with details.issues instead. The
finding codes are INVALID_BODY, KEY_NOT_NAME, NAME_UNRESOLVED,
NAME_AMBIGUOUS, IDENTITY_KEY, EDGE_UNDECLARED, EDGE_ROOT, EDGE_DEPTH,
FIELD_UNDECLARED, FIELD_RULE, SWITCH_COVERAGE, EFFECT_UNDECLARED,
EFFECT_POSITION, CHAIN_READ, SCOPE, EXPRESSION_INVALID, IDENTITY_GATE,
ROLE_WRITE, TRIGGER_FROM, SECRET_REF_IN_DEFINITION, INLINE_ASYNC_ACTION,
DURABLE_EFFECTS_UNSUPPORTED, DETECTION_EVIDENCE, CONFIG_UNDECLARED,
CONFIG_SECRET_READ, AUTH_PROFILE_UNDECLARED, GATE_OPERAND_TEMPLATE,
GATE_CONFIG_OPERAND_UNSUPPORTED and ACTION_GATE_UNSUPPORTED. Publishing a
name and version that already exist answers 409 (CONFLICT).
The other definition routes:
| Route | Does |
|---|---|
GET /definition/processes |
The versions you can read, your own and those shared with you (?name=, ?status_in=, ?label=, …). |
GET /definition/processes/{id} |
One version. |
GET /definition/processes/{id}/definition |
The body alone. |
PUT /definition/processes/{id}/definition |
Overwrite the body of a version you own, judged as a publish is. |
PUT /definition/processes/{id} |
Change its label, description, status or body. |
PUT/PATCH /definition/processes/{id}/default-job-bindings |
The definition's own effect_<kind> bindings. |
A definition is shared with another tenant through a manifest like any other definition (see Share with Another Tenant); the other tenant activates it by name.
#4. Activate it in your tenant
A process runs in a tenant only once the tenant activates it, by name:
PUT /workflows/processes/{name}/activation. One activation per name binds one
version (name@version, your own definition over a shared one) and fills the
config slots the definition declares.
Without the required config slot, nothing is written:
{
"error": {
"status": 422,
"code": "VALIDATION_FAILED",
"message": "The activation's config does not fit the process's config slots.",
"details": {
"code": "CONFIG_INVALID",
"findings": [
{
"path": "approved_status",
"code": "CONFIG_REQUIRED",
"message": "\"approved_status\" is a required config slot"
}
]
}
}
}
PUT/workflows/processes/{name}/activationActivate supplier_onboardingAPI docs ↗Try it
{
"data": {
"id": "71422330-db89-43d7-a788-8ebe90f455ed",
"tenant_id": "645347e8-1f7a-429b-819c-bdd327b43f62",
"name": "supplier_onboarding",
"definition_id": "b259f319-10de-4030-a072-7e7376105348",
"job_bindings": {},
"overrides": {},
"config": { "approved_status": "approved" },
"is_enabled": true,
"created_at": "2026-10-06T01:00:01.96672+00:00",
"updated_at": "2026-10-06T01:00:01.96672+00:00"
}
}
GET /workflows/processes/supplier_onboarding/activation answers the same
object. The request's fields:
| Field | Meaning |
|---|---|
definition |
Required. name@version of an active or deprecated version you can read. |
config |
A value for each slot the definition declares, checked against the slot's schema; every required slot set. A slot whose value differs by origin is an object keyed by origin, with an entry for every origin a transition admits. |
job_bindings |
effect_<kind> → job id or name@version, for kind effects. PATCH …/activation/job-bindings changes single slots. |
overrides |
Your tenant's adjustments: a condition used in place of the definition's, extra fields, per step a dependsOn used in place of the step's, per transition an enabledWhen combined with the definition's and extra inputs, and extra alert steps under addEmits. |
is_enabled |
false turns the process off in your tenant: every move is refused BINDING_DISABLED. |
A field left out keeps its stored value. An activation is judged whole before
anything is written (422 with details.findings or details.reasons); a
request that changes nothing writes nothing; one that only turns the activation
off is written without being judged. Writing an activation takes the
workflow_config capability in your tenant (403 otherwise).
#Credentials for effects
An effect that calls an external API takes its credential from the activation.
The definition declares a writeOnly slot and an auth profile that reads it;
an effect's http action names the profile. This definition (published with
POST /definition/processes as supplier_registry, version 1.0.0) registers
a supplier with an external registry:
{
"key": "supplier_registry",
"label": "Supplier registry",
"anchor": "Supplier",
"policy": {
"name": "supplier_registry",
"grants": [
{
"resourceType": "entity",
"resourceName": "OnboardingStep",
"actions": ["create", "read", "update"]
}
]
},
"config": {
"type": "object",
"properties": {
"registry_url": { "type": "string", "format": "uri" },
"registry_token": { "type": "string", "writeOnly": true }
},
"required": ["registry_url", "registry_token"]
},
"auth": {
"registry": { "type": "bearer", "token": "{{ config.registry_token }}" }
},
"edges": { "supplier": { "via": "supplier_onboarding_steps" } },
"effects": {
"register_supplier": {
"actions": [{
"stepId": "post",
"name": "http",
"config": {
"url": "{{ input.url }}/suppliers",
"method": "POST",
"auth": "registry",
"body": { "supplier": "{{ input.supplier }}" }
}
}],
"input": {
"url": { "$jsonata": "$config.registry_url" },
"supplier": { "$jsonata": "$anchor.id" }
}
}
},
"emits": {
"registry": {
"preset": "milestone",
"type": "OnboardingStep",
"anchor": "supplier",
"key": "{{ $anchor.id }}:registry",
"transitions": {
"complete": { "label": "Register", "before": ["register_supplier"] }
}
}
}
}
The activation fills the secret slot with a scope-explicit secret reference, never a value:
PUT/workflows/processes/{name}/activationActivate with a secret referenceAPI docs ↗Try it
A plain value in a secret slot is refused:
{
"error": {
"status": 422,
"code": "VALIDATION_FAILED",
"message": "The activation's config does not fit the process's config slots.",
"details": {
"code": "CONFIG_INVALID",
"findings": [
{
"path": "registry_token",
"code": "CONFIG_SECRET_REF",
"message": "a secret slot holds one scope-explicit reference, e.g. {{ secrets.tenant.NAME }}"
}
]
}
}
}
The reference resolves only inside the effect, for the move being made. A
person's own (user) secret may sit only under the manual entry of an
origin-keyed slot.
#5. Who makes a move
| Who | How | Origin |
|---|---|---|
| A person, on an app page | Presses a control the page drew from the process (no steps to author), or an authored command that runs performTransition, undoTransition or releaseHold. |
Always manual. |
| A producer: a workflow job | Runs performTransition (one move) or performTransitions (up to 1,000), undoTransition or releaseHold. |
detection, linked, poller or backfill. |
A person may make only the transitions whose by includes manual. A workflow
names its origin on each move, with the trigger entry it fired
({ "kind": "detection" | "linked" | "poll", "index": <n> }) for detection,
linked and poller, the signal it carries (detection and linked), and
optionally stateAt, the time its evidence says the state was reached. A
backfill move declares its own source and writes history: it skips
readiness, enabledWhen, evidence and effects and is never held, but its
transition must still be declared, admit backfill in by and be permitted. A
workflow move that names no origin is made as detection. Every move is also
checked against the mover's permissions on the step type: a first move creates
the step row and a later one updates it, so a person needs create and update
on the step type through the app's policy.
A move:
| Field | Meaning |
|---|---|
target |
{ "process", "emit", "anchorId" } (a step on an anchor, by name; a first move creates its row), or { "row": { "type", "id" } } (a step row). |
transition |
The transition key. |
inputs |
A person's inputs, by field name. |
links |
A person's evidence: [{ "edge", "to" }], linked by the move itself, through an edge its transition waits on. |
expectedUpdatedAt |
The step row's updated_at as you last read it; a row that changed since is refused STALE. |
occurrenceKey |
The name of the occurrence a by-name move opens. |
origin, trigger, signal, stateAt, source |
A workflow's move only (above). |
#How a move is judged
In order: the process is activated and enabled for your tenant; the transition
is declared and the origin is in its by; the process's condition holds on
the anchor; the transition is legal from the step's rung; the steps it depends
on are done; its enabledWhen holds; its evidence is linked; the mover may
write the step. A person's move that fails a gate is refused and runs
nothing. An automated move that fails readiness, enabledWhen or evidence is
held: it is logged with the move's origin, signal and evidence time, and a
step row that does not exist yet is created on its first rung.
A held move is released three ways:
- by data, always: every later move that commits on the same anchor re-judges the anchor's held moves, and a held move whose gates are met commits, with the origin, signal and evidence time it was held with;
- by hand, only when the transition declares
"hold": { "manual": true }: a person'sreleaseHold, judged again (evidence still missing stays held); - any other release is refused
RELEASE_NOT_DECLARED.
Writing a relationship or an attribute directly releases nothing; the next committed move on the anchor, or a hand release, does.
#6. Move it from a workflow
A workflow that moves whatever its trigger names:
POST/definition/workflowsA workflow that moves process stepsAPI docs ↗Try it
Activate it (POST /workflows/{id}/activate). The supplier Northwind Metals
(cb273df2-…) has nothing linked yet, and Southgate Plastics (71edee6d-…)
has status: "blocked". The screening service reports a pass for Northwind
before its documents are in, and the same run carries three moves the platform
refuses:
POST/workflows/wrDispatch four movesAPI docs ↗Try it
The run succeeds, and the performTransitions action's output (read from
GET /workflows/runs/actions?workflow_run_id=…&job_run_id=…) answers each move
in order:
{
"outcomes": [
{
"index": 0,
"outcome": "held",
"code": null,
"message": null,
"details": null,
"result": {
"status": "held",
"transition_id": "d2deca15-3a41-5900-bf4d-0cbcbe0f349b",
"entity_id": "0443acf0-d998-40d9-9d1e-f8e3a3223bb7",
"updated_at": "2026-10-06T01:00:20.717804+00:00",
"from_state": null,
"to_state": "DONE",
"links": ["13c66c29-e3ae-4a55-a127-3e0f41855d16"],
"lane_dedup_key": null,
"dispatched": false,
"replayed": false,
"reason": "NOT_READY",
"effects": {}
}
},
{
"index": 1,
"outcome": "illegal",
"code": "ORIGIN_NOT_ADMITTED",
"message": "complete is not made by detection",
"details": null,
"result": null
},
{
"index": 2,
"outcome": "illegal",
"code": "TRANSITION_NOT_DECLARED",
"message": "documents declares no approve",
"details": { "reason": "transition_not_declared" },
"result": null
},
{
"index": 3,
"outcome": "illegal",
"code": "NOT_APPLICABLE",
"message": "supplier_onboarding does not apply to this anchor",
"details": null,
"result": null
}
]
}
- 0 is held: screening waits on documents (
reason: "NOT_READY"). Its step row was created onREADYand linked to the supplier (the one id inlinks); the report is linked when the hold is released. - 1 —
reviewadmits only a person. - 2 —
documentsdeclares noapprove. - 3 — the process does not apply to a blocked supplier.
outcome is committed, held, conflict, refused or illegal; code is
set on every outcome but committed and held. performTransitions reports
each move and does not fail; performTransition (one move) fails its step with
the refusal.
#7. Show the process on an app page
An app page reads the process with two sources and draws it with the generic
table and actions widgets: there is no process widget. The spec below names
nothing of the supplier model in its pages; the activation's config does (see
Apps (Bridge) → Process pages).
list shows every supplier with its three steps; detail shows one supplier's
steps with their controls, its documents (each can be attached as evidence), and
the history of one step (each held move can be released by hand).
POST/definition/specsThe onboarding appAPI docs ↗Try it
The activation's config names the process, the columns and the presentation:
PUT/apps/{specId}/configActivate the app with its process configurationAPI docs ↗Try it
Then, as in Apps (Bridge) steps 4–6:
register the provider organisation (portal, northwind-buyers), create an
integration and mint its app token. Two buyers open the app, ana and ben.
ben only looks: read his auth_user_id from GET /access/external-users
after his first visit and give him the read-only policy.
PUT/apps/{specId}/users/{authUserId}/policyMake ben a viewerAPI docs ↗Try it
#What the page reads
$query with process folds each row's steps onto it. With
"rows": "anchor", a supplier row gets
steps.<process>.<emit>.{status, actions} for each step it can read
("rows": "step" gives a step row its own {status, actions}). can folds
can.<action> onto every row: whether the viewer may write that row, answered
by the same read (display only: every write is judged again). A process the
tenant has not activated folds nothing, and the answer carries a warning naming
it:
{
"id": "process-unresolved:vendor_audit",
"level": "WARNING",
"message": "Rows show no steps for a process that isn't available here: vendor_audit."
}
$job with describeProcessActions returns the same envelopes for the
anchors (or step rows) you name:
{ unresolved, anchors: [{ anchorId, steps }] }. Here is ana's view of
Northwind's review step once it is in progress (trimmed):
{
"process": "supplier_onboarding",
"emit": "review",
"type": "OnboardingStep",
"entityId": "2e189c3f-937e-4348-832d-265c7a369c06",
"occurrence": 2,
"label": "Supplier onboarding",
"category": null,
"priority": null,
"status": {
"state": "IN_PROGRESS",
"label": "In progress",
"tone": "active",
"badges": [],
"stateAt": "2026-10-06T01:00:41.425Z"
},
"actions": [
{
"actionId": "supplier_onboarding:review:2e189c3f-937e-4348-832d-265c7a369c06:complete",
"transition": "complete",
"label": "Approve",
"icon": null,
"variant": null,
"cta": true,
"surfaces": null,
"enabled": true,
"reason": null,
"details": null,
"confirm": "Approve this supplier?",
"intent": {
"kind": "process.perform",
"target": {
"row": {
"type": "OnboardingStep",
"id": "2e189c3f-937e-4348-832d-265c7a369c06"
}
},
"transition": "complete",
"inputs": {},
"expectedUpdatedAt": "2026-10-06T01:00:41.476635+00:00"
}
}
]
}
ben reads the same step with "enabled": false and
"reason": { "code": "PERMISSION_DENIED", "label": "Not permitted" } on each
control.
- The step envelope.
label,categoryandpriorityare the process's, shared by every step of the process; tell steps apart byemit.statusis the status view: the rung, its word, itstone(idle,active,waiting,done), its badge words (Held,Running,Failed,Blocked) and when the step reached the rung. A step the viewer cannot read is left out (and still counts for the steps that depend on it). An anchor the process does not apply to has no steps. - The descriptor. Each control is a decision made for this viewer: its
words,
cta,confirm,surfaces(nullis everywhere), and either"enabled": trueor areasonwith the refusal code and its word. - The intent. What a press asks for, as data:
process.perform,process.undo(with the step'sexpectedUpdatedAt),process.release(a held move whose transition declareshold.manual, with itstransitionId) ornavigate(a transition that takes inputs and has a presentationformpage). The engine maps the intent to one action; you author no steps for it.
Presentation (priority, category, surfaces, hidden, rung labels and
tone, badges, reasons, per transition label, icon, variant, form,
confirm, hidden, per field help, per evidence edge supply) is keyed by
process, then by step, transition and field. It changes words and placement
only, never what a viewer may do.
#8. Move it from the page
Every call below is POST /apps/run/{integrationId} with the app token as
Authorization: Bearer <app token>. An interaction sends the state of the
previous answer and the action id of what was pressed (see
Apps (Bridge) → Interactions).
POST/apps/run/{integrationId}ana opens the appAPI docs ↗Try it
The list table, before anything has moved (trimmed to its rows):
[
{
"id": ":p/list/items/71edee6d-583d-4507-aaa0-220c50591dc8:",
"name": "Southgate Plastics",
"steps.supplier_onboarding.documents.status": "",
"steps.supplier_onboarding.screening.status": "",
"steps.supplier_onboarding.review.status": "",
"editable": "Yes"
},
{
"id": ":p/list/items/cb273df2-d96c-4c11-b682-e7e87054680d:",
"name": "Northwind Metals",
"steps.supplier_onboarding.documents.status": "Waiting",
"steps.supplier_onboarding.screening.status": "Ready · Blocked",
"steps.supplier_onboarding.review.status": "Ready · Blocked",
"editable": "Yes"
}
]
The blocked supplier has no steps. ana opens Northwind (the row menu's Open,
":p/list/items/actions/0/menu/0:-cb273df2-d96c-4c11-b682-e7e87054680d"). Its
checklist draws one row menu of the steps' controls, each enabled only on the
rows whose control is enabled; review offers Start review but not Approve,
since a person approves only from IN_PROGRESS, and screening offers nothing,
since only a workflow moves it:
{
"type": "table",
"id": ":p/detail/checklist/table:",
"columnDefinitions": [
{ "type": "string", "accessor": "emit", "header": "Step" },
{ "type": "string", "accessor": "status", "header": "Status" },
{
"type": "actions",
"actions": [{
"options": [
{
"label": "Mark received",
"action": "custom",
"id": ":p/detail/checklist/fromRow/0/menu/complete-Mark%20received:",
"accessor": "__action_complete-Mark%20received"
},
{
"label": "Start review",
"action": "custom",
"id": ":p/detail/checklist/fromRow/0/menu/initiate-Start%20review:",
"accessor": "__action_initiate-Start%20review"
}
]
}]
}
],
"data": [
{
"id": ":p/detail/checklist/0:",
"emit": "documents",
"status": "Waiting",
"__action_complete-Mark%20received": false,
"__action_initiate-Start%20review": false
},
{
"id": ":p/detail/checklist/1:",
"emit": "review",
"status": "Ready · Blocked",
"__action_complete-Mark%20received": false,
"__action_initiate-Start%20review": false
},
{
"id": ":p/detail/checklist/2:",
"emit": "screening",
"status": "Ready · Blocked",
"__action_complete-Mark%20received": false,
"__action_initiate-Start%20review": false
}
]
}
#A refused press
Mark received is disabled because no document is linked. Pressing it anyway
runs nothing; the answer names the refusal:
POST/apps/run/{integrationId}ana presses Mark receivedAPI docs ↗Try it
{
"notifications": [
{
"id": "press-refused-fromRow/0/menu/complete-Mark%20received",
"level": "ERROR",
"message": "Evidence required",
"details": { "refusal_code": "EVIDENCE_REQUIRED" }
}
]
}
Start review (row 1) answers "message": "Not ready",
"refusal_code": "NOT_READY": screening is not done. An authored command whose
action is refused fails its step instead; when ben presses
Attach as evidence on the signed agreement
(":p/detail/documents/actions/0/menu/0:-0fc6906c-fa04-4dd4-b652-a32c48b54e04"):
{
"notifications": [
{
"id": "step-error-9f127f24-0b35-4e23-a20d-63b7f3f921a5",
"level": "ERROR",
"message": "Not permitted",
"details": {
"error_code": "UNPROCESSABLE_ENTITY",
"refusal_code": "PERMISSION_DENIED"
}
}
]
}
Nothing is written and no effect runs.
#A release that is not declared
After the workflow run, Northwind's screening step reads Ready · Held, and its
history lists the held move (d2deca15-…, status held, by
Onboarding signals). screening declares no hold.manual, so ana's
Release
(":p/detail/history/actions/0/menu/0:-d2deca15-3a41-5900-bf4d-0cbcbe0f349b")
is refused:
{
"notifications": [
{
"id": "step-error-b679fe30-d249-488d-b9c1-49ef9c2397b8",
"level": "ERROR",
"message": "Cannot release",
"details": {
"error_code": "UNPROCESSABLE_ENTITY",
"refusal_code": "RELEASE_NOT_DECLARED"
}
}
]
}
#Evidence, and a release by data
ana attaches the signed agreement
(":p/detail/documents/actions/0/menu/0:-0fc6906c-fa04-4dd4-b652-a32c48b54e04").
The command runs performTransition on documents with
"links": [{ "edge": "documents", "to": "0fc6906c-…" }]: the move links the
document itself, counts it toward until, and commits. Its commit re-judges the
held screening move, which commits too. The answer re-renders the page:
[
{
"id": ":p/detail/checklist/0:",
"emit": "documents",
"status": "Received",
"__action_initiate-Start%20review": false,
"__action_undo-Undo": true
},
{
"id": ":p/detail/checklist/1:",
"emit": "review",
"status": "Ready",
"__action_initiate-Start%20review": true,
"__action_undo-Undo": false
},
{
"id": ":p/detail/checklist/2:",
"emit": "screening",
"status": "Done",
"__action_initiate-Start%20review": false,
"__action_undo-Undo": false
}
]
and the screening history reads:
[
{
"id": ":p/detail/history/2d1586ea-85c9-4f0d-9d00-f62c5117b98e:",
"move": "complete",
"origin": "detection",
"status": "done",
"by": "ana@northwind.example",
"at": "2026-10-06T01:00:29.401813+00:00"
},
{
"id": ":p/detail/history/d2deca15-3a41-5900-bf4d-0cbcbe0f349b:",
"move": "complete",
"origin": "detection",
"status": "released",
"by": "Onboarding signals",
"at": "2026-10-06T01:00:21.054949+00:00"
}
]
The release is a detection move made by the session whose commit met the gate
(ana), with the held move's signal and evidence time; the held move reads
released.
#Approve, undo, review again
Start review (row 1) moves review to In progress, and the bar draws its
call to action:
{
"type": "button",
"label": "Approve",
"variant": "primary",
"action": "custom",
"id": ":p/detail/bar/action/supplier_onboarding%3Areview%3A0ba333dd-277f-437f-a71a-e8d9be7d76be%3Acomplete:"
}
Pressing it runs mark_supplier_approved as ana, then commits: review reads
Done, and GET /entity/entities/cb273df2-… answers
"attributes": { "name": "Northwind Metals", "status": "approved" } (the
activation's approved_status). The checklist then offers Undo and
Review again on review.
Undo(":p/detail/checklist/fromRow/0/menu/undo-Undo:-1") moves it back toIn progress; the undone move readsundonein the log, with the undo's id inundone_by. An undo removes the links the undone move made (never the anchor link) and runs its transition'scompensateeffects.Approveagain:Done.Review again(":p/detail/checklist/fromRow/0/menu/restart-Review%20again:-1") opens occurrence2: a new step row (2e189c3f-…) onIn progress. The checklist shows the new occurrence; the first staysDone.
#A disabled activation
With the activation turned off
(PUT /workflows/processes/supplier_onboarding/activation with
"is_enabled": false), every move is refused. ana's Approve answers
"message": "Disabled", "refusal_code": "BINDING_DISABLED", and the bar draws
Disabled as text; a workflow's move answers
{ "outcome": "illegal", "code": "BINDING_DISABLED", "message": "supplier_onboarding is not enabled here" },
and a move naming a process the tenant never activated answers
{ "outcome": "illegal", "code": "PROCESS_NOT_ACTIVATED", "details": { "reason": "not_activated" } }.
Turn it back on with "is_enabled": true.
#A release by hand
For a second supplier, Eastbay Textiles, the mailbox workflow reports that
documents arrived but names no document:
POST/workflows/wrDocuments arrived, no document namedAPI docs ↗Try it
The move is held ("outcome": "held", "reason": "EVIDENCE_REQUIRED"), and
documents reads Waiting · Held. Because the transition declares
hold.manual, its control (Mark received) is enabled and its intent is
process.release. Pressed with nothing linked, the release is judged again and
refused ("refusal_code": "EVIDENCE_REQUIRED"). Someone links the agreement
directly:
POST/entity/relationshipsLink the evidence directlyAPI docs ↗Try it
The link releases nothing by itself: documents still reads Waiting · Held.
ana presses Mark received again, and the release commits: Received.
#9. Read it back
Three read actions answer from the log and the step rows. They run in a workflow
job, or on an app page as a $job source.
getProcessStatus ({ processes, anchorIds }) lists each step's state,
every occurrence, whether it is satisfied, what blocks it and its latest move
(trimmed to review):
{
"unresolved": [],
"anchors": [{
"anchor_id": "cb273df2-d96c-4c11-b682-e7e87054680d",
"steps": [
{
"process": "supplier_onboarding",
"emit": "review",
"entity_id": "2e189c3f-937e-4348-832d-265c7a369c06",
"occurrence": 2,
"state": "IN_PROGRESS",
"satisfied": false,
"blocked_by": [],
"updated_at": "2026-10-06T01:00:41.476635+00:00",
"latest": {
"transition_id": "9f5d67ab-50e3-4930-9230-a467b267ca79",
"transition": "restart",
"status": "done",
"reason": null
}
},
{
"process": "supplier_onboarding",
"emit": "review",
"entity_id": "0ba333dd-277f-437f-a71a-e8d9be7d76be",
"occurrence": 1,
"state": "DONE",
"satisfied": false,
"blocked_by": [],
"updated_at": "2026-10-06T01:00:40.919965+00:00",
"latest": {
"transition_id": "dd3f4b47-a3e6-4ef7-8da1-5c04f455395b",
"transition": "complete",
"status": "done",
"reason": null
}
}
]
}]
}
listProcessTransitions
({ target: { row: { type, id } }, limit?, includePrincipals?, linkedTo? }) is
a step row's log, newest first. With includePrincipals: true each move names
who made it (actor: a user, a workflow with its run, or a token). The
documents step's one move, trimmed:
{
"transitions": [{
"id": "4257995d-1bb2-4775-9756-819039460c57",
"process_key": "supplier_onboarding",
"emit_key": "documents",
"definition_version": "1.0.0",
"transition": "complete",
"from_state": null,
"to_state": "DONE",
"creates": true,
"origin": "manual",
"source": "manual",
"links": [
{
"edge": "supplier",
"id": "01185642-a450-45fa-a983-82571686eebf",
"from": "cb273df2-d96c-4c11-b682-e7e87054680d",
"to": "c4f6518b-fd06-4acf-abd2-32b3d2a9be42"
},
{
"edge": "documents",
"id": "d1739299-b865-42ae-bc5b-1cefff601f04",
"from": "c4f6518b-fd06-4acf-abd2-32b3d2a9be42",
"to": "0fc6906c-fa04-4dd4-b652-a32c48b54e04"
}
],
"status": "done",
"actor": {
"kind": "user",
"id": "6034d906-dc27-47fa-87da-a9ab8f8638eb",
"label": "ana@northwind.example",
"email": "ana@northwind.example",
"external_id": "ana"
}
}]
}
A move's status is done, held, released (a hold that a later move
committed, named by that move's release_of), undone (with undone_by),
failed (an effect failed; reason says which and why) or superseded. Each
log row also keeps the move's inputs, signal, effect outputs and state_at.
linkedTo: [<row id>, …] answers, for each row, the newest move that linked it:
who attached a piece of evidence.
describeProcessActions is the page read above; describeProcessForm
({ target, transition, presentation?, values? }) returns a transition's fields
with their defaults and options read as the viewer, and its evidence entries.
#Refusal codes
A refusal carries its code in code (an action's outcome) or in
details.refusal_code (an app notification), and its word in message / the
descriptor's reason.label. These were returned in this guide:
| Code | Word | When |
|---|---|---|
EVIDENCE_REQUIRED |
Evidence required | Fewer rows linked through an until edge than it needs. An automated move is held instead. |
NOT_READY |
Not ready | A step in dependsOn is not done. An automated move is held instead. |
PERMISSION_DENIED |
Not permitted | The mover may not write the step (checked before any effect runs). |
ORIGIN_NOT_ADMITTED |
Not allowed | The origin is not in the transition's by. |
TRANSITION_NOT_DECLARED |
Not available | The step declares no such transition. |
TRANSITION_NOT_ALLOWED |
Not allowed | The transition is not legal from the step's rung (complete is not legal from DONE). |
TRIGGER_FROM |
Not allowed from here | The move's trigger entry does not exist or does not fire this transition. |
NOT_APPLICABLE |
Not applicable | The process's condition is false for the anchor. |
RELEASE_NOT_DECLARED |
Cannot release | A release by hand of a transition without hold.manual. |
BINDING_DISABLED |
Disabled | The activation is turned off. |
PROCESS_NOT_ACTIVATED |
Not activated | The tenant has no activation of that name. |
EFFECT_FAILED |
Failed | A before effect failed; the step shows Failed and the move can be made again. |
The other codes a move can carry: UNDECIDABLE (a gate could not be decided, so
it does not admit), GUARD_DROPPED, INPUT_INVALID, BINDING_UNRESOLVED,
NOT_HELD, NOT_RETRYABLE, RACE, TRANSITION_ID_TAKEN, ROW_IN_BATCH,
ROW_NOT_FOUND, STALE, STATUS_INVALID, WRONG_STEP_TYPE,
OCCURRENCE_MISMATCH, IDENTITY_MISSING, IDENTITY_TAKEN, LINK_EDGE,
ANCHOR_NOT_FOUND, NO_SUCH_TRANSITION, NOT_REPEATABLE,
REPEATABLE_IN_PLACE, UNDO_MISMATCH, TRANSITION_NOT_SETTLED,
TRANSITION_NOT_HELD, FIELD_MISMATCH, LINK_ENDPOINT_NOT_FOUND,
LINK_CARDINALITY, CHECK_IMAGE and LINK_PERMISSION_DENIED. A presentation
may give any code its own word (emits.<emit>.reasons.<code>).
#Related guides
- Apps (Bridge) → Process pages — list and detail pages, forms and the module configuration in depth.
- Workflows — triggers, runs and jobs.
- Job Bindings — binding the jobs
kindeffects run. - Secrets and HTTP Auth & External Data — credentials for effects.
- Share with Another Tenant — offering a definition to another tenant.