- Status: Implemented — matches the engine
AplParsercontract and the Studio canvas (abada.io/v1). - Purpose: the plain-language, node-by-node guide to the Abada Process
Language (APL). It is the shared reference for three audiences:
- Studio authors — what each node does and what you must configure.
- Studio developers — the property panel contract: every property below is something the panel should let the author edit for its node.
- Engine users — the user-facing APL documentation alongside the formal technical specification.
The formal grammar, validation rules and error codes live in
apl-specification.md. This document explains the
same nodes in plain language and is deliberately non-grammatical: it tells you
what to fill in, not the full parser rules.
Each node section contains:
- What it does — a plain-language description.
- When to use it — the situation it is designed for.
- BPMN equivalent — the standard BPMN element it compiles to, or the pattern it replaces.
- Properties — a table of the node's configuration fields.
Requiredmeans the engine rejects the deployment if the field is missing or empty. Optional fields have sensible engine defaults when omitted. - Example — a minimal YAML snippet.
The same rules are machine-readable: GET /api/v1/apl/schema serves the APL
JSON Schema (the source of Studio's types), and POST /api/v1/apl/validate
checks a draft exactly as deployment would. A field that is not listed for a
node is reported as an ABADA-APL-SCHEMA-001 warning (an error from 1.1.0);
see the APL specification §6.1.
The vocabulary table in §2 gives the one-glance summary; §3 covers the properties every node shares; §4 walks each node type; §5 summarizes the gateway fork/join forms.
This is the complete node vocabulary of abada.io/v1. The right-hand column
is the BPMN construct each APL node compiles to — or, where noted, the common
BPMN pattern it replaces with a simpler, safer shape.
| APL node | Studio palette | BPMN equivalent | What it replaces / notes |
|---|---|---|---|
webhook |
Start Event | Start Event (webhook trigger) | The process trigger — one per process |
end |
End Event | End Event | — |
agent |
AI Agent Node | Service Task (external task on abada:agent) |
The "AI/LLM service task" pattern: probabilistic work as a durable external task |
engine-task |
Engine Task | Service Task (external task on a worker topic) | Camunda external task / generic service task |
script |
Script Step | Script Task | Sandboxed in-transaction JavaScript without Java access; operator opt-in (ABADA_SCRIPTS_ENABLED) |
human-input (alias approval-gate) |
Human Input | User Task | Human review task with candidate groups |
decision-table |
DMN Rule Table | Business Rule Task (DMN table) | Camunda DMN table binding — executed by the engine in-transaction |
condition |
Exclusive Gateway | Exclusive Gateway | BPMN exclusive gateway (exactly one branch, with a default flow) |
inclusive |
Inclusive Gateway | Inclusive Gateway | BPMN inclusive gateway (every matching branch, zero or more) |
parallel |
Parallel Gateway | Parallel Gateway | BPMN parallel gateway (fork/join, all branches) |
event-gateway |
Event Gateway | Event-Based Gateway | BPMN event-based gateway (first competing event wins, siblings cancelled) |
message-catch |
Message Catch | Intermediate Catch Event (message) | BPMN message catch — wait for a correlated message |
timer |
Timer Catch | Intermediate Catch Event (timer, duration form) | BPMN timer catch — wait for a duration |
signal |
Signal Catch | Intermediate Catch Event (signal) | BPMN signal catch — wait for a broadcast |
call-process |
Call Process | Call Activity (notation only) | A governed child process; BPMN callActivity itself is not imported |
What "replaced" means. APL is not a BPMN dialect; it is the canonical authoring format. The Studio canvas serializes to APL and the engine parses APL natively — BPMN XML remains an import/export format. When a node is listed as "replaces" a BPMN pattern, it means the same intent expressed in BPMN would require that element (or that combination of elements), and APL gives you a simpler, stricter, one-block form.
All 14 node types share these base fields:
| Property | Type | Required | What it does |
|---|---|---|---|
id |
string | yes | Unique name of the node inside the process. Becomes the BPMN element id. Letters, numbers and underscores are safe. |
type |
string | yes | The node kind (one of the left column of §2). |
description |
string | no | A human-readable name for the node. Becomes the BPMN element name. For an approval gate this is the task name people see. |
next |
nodeId | depends | The id of the node that runs next. Required on linear nodes; forbidden on branching nodes (condition, event-gateway, and parallel/inclusive forks). |
ui |
{ x, y } |
no | Canvas layout hint (where Studio draws the node). Ignored by the engine. |
loop |
{ max_iterations, on_exhausted? } |
when a cycle returns here | Bound of the loop that returns to this node: it may run at most max_iterations (1–1000) times per pass; past that the token goes to on_exhausted, or an incident is opened. The current count is the variable <id>_iteration. Not allowed on webhook and end; see the APL specification §2.3. |
Boundaries of wait steps. agent, engine-task and human-input also
accept on_error and on_timeout: { after, then }: the step leaves through
that route when its work fails for good or is not done in time, and its
unfinished work is cancelled. Human tasks add an enforced sla_hours with
escalate_to. See the APL specification §2.6.
What it does. The entry point of your process. A process instance starts when this trigger fires — in practice an incoming API call or the "start process" action from an application. The payload of that call becomes the instance's initial variables.
When to use it. Every process needs exactly one. It is the node the flow begins at.
BPMN equivalent. Start Event (webhook trigger). APL declares a single
start node per process; the engine enforces this (a second webhook is a
deployment error).
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
next |
nodeId | yes | The first executable step of the process. |
The webhook node itself has no URL property — the engine does not generate a per-process inbound URL. External applications start the process by calling the engine's REST API with the process key; the request body becomes the instance's initial variables:
- Project-scoped:
POST /api/v1/projects/{projectId}/processes/{processKey}/start - Global:
POST /api/v1/processes/start?processId={processKey}
Both accept an optional Idempotency-Key header so retries do not create
duplicate instances, and both require the caller to be authenticated with
permission to start the process. If you need a stable inbound URL that an
external system (a SaaS webhook, a partner service) can POST to without
knowing the API, that is a separate inbound-webhook feature, not part of the
webhook node.
Example.
- id: onStart
type: webhook
description: Loan application received
next: extractAgentWhat it does. The finish line. When a token reaches an end node, that branch of the process is done; when the last token arrives, the process instance completes.
When to use it. At every point where the flow may finish. A process can have several end nodes (one per outcome branch).
BPMN equivalent. End Event.
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
| — | — | — | No extra properties. next is forbidden — an end node has no successor. |
Example.
- id: done
type: end
description: Onboarding concludedWhat it does. A step that delegates probabilistic work to an AI model — reading, extracting, judging, drafting. The agent runs on an external worker (a separate process that picks the job up, calls the model, and reports back). The engine never advances the process from inside the agent; a worker completion is what moves the flow on. This keeps AI work honest: probabilistic output can never silently corrupt the workflow state.
When to use it. Any step where the "right answer" is not deterministic — e.g. extracting fields from an unstructured document, or a judgment call.
BPMN equivalent. Service Task executed as an external task on the reserved
topic abada:agent. APL replaces the ad-hoc "LLM service task" pattern with a
typed node plus a versioned worker profile (abada.agent/v1).
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
profile |
string | no | Worker profile version. Default and only supported value: abada.agent/v1. |
model |
string | no | The model to use. Omitted → the worker's default model. Must be on the engine's allowed-model list. |
prompt |
string | no | The instructions given to the model. Multi-line YAML blocks are fine. |
inputs |
map | no | Named bindings from process variables to prompt inputs, e.g. payload: ${payload}. |
result_variable |
string | no | The variable the agent's output is written to. Defaults to <nodeId>_result. |
output_schema |
map | no | Expected JSON shape of the response. The engine validates the result against it before any state changes; see on_invalid_output. |
tools |
list | no | Tools from the project's tool servers: crm/get_customer, or { ref: crm/refund, policy: approval_required, approvers: [finance] } to tighten the server's policy (never loosen it) and name who approves its calls (an approval_required tool needs approvers here or on its server: ABADA-APL-TOOL-003). Resolved and frozen at deployment; see the APL specification §3.2.1. A name without a server is advisory only and warns. |
max_turns |
integer | no | 1–32, default 8. Answered model calls one attempt may make while using tools; a call that failed (an unavailable model before a fallback or a deferral) spends no turn. |
max_tokens_total |
integer | no | Tokens the whole task may use across its attempts. Default 50 000 when the node binds tools, otherwise no limit. |
budget_usd |
number | no | Engine-computed cost the whole task may reach across its attempts. A model without a price (Settings → Model Prices) cannot run under a budget. Hitting any limit takes on_error with code AGENT_BUDGET_EXHAUSTED. |
evidence |
{ payloads, retention_days } |
no | What the step journal keeps of this agent's prompts, tool arguments and results: none, redacted or full, for retention_days. Can only be stricter than the project's evidence policy. |
confidence_threshold |
number | no | 0–100. Below this confidence the output is treated as a low-confidence result. |
temperature |
number | no | 0–2. Higher = more creative, lower = more deterministic. Default 0.2. |
max_tokens |
integer | no | Cap on the model response size. Default 2048. |
timeout_ms |
integer | no | Worker call timeout. Default 60 000. |
max_attempts |
integer | no | Durable retries before the job fails. Default 3. A rate-limited attempt that never ran does not count. |
retry_backoff_ms |
integer | no | Pause between retries. Default 2 000. |
fallback_models |
string[] | no | Up to 3 allowed models tried in order while the model before is rate-limited or unavailable — never because of an answer the engine rejected. |
on_low_confidence |
nodeId | no | Where to go when the result's _confidence is missing or below confidence_threshold. |
on_invalid_output |
nodeId | no | Where to go when the result does not match output_schema. |
on_error |
nodeId or [{code?, then}] |
no | Where to go when the worker reports an error code, or when the last attempt fails (code WORK_FAILED). Without it a last failure opens an incident. |
on_timeout |
{ after, then } |
no | If the step is not done within after (e.g. PT1H), its work is cancelled and the flow continues at then. |
delegates |
list of {process, outputs, approval?, approvers?, description?, max_depth?} |
no | Processes the agent may start as a governed child through the engine-provided tool delegate:<process>; pinned at deployment, inputs checked against the child's declared variables, only outputs come back, approval: required goes to approvers first (APL specification §3.2.3). |
reviewed_by |
{node, accept} |
no | The human-input node (with outcomes) that reviews this agent's output, and the outcome that means the answer was kept. Any other outcome counts as an override in the Insight loop. Metadata only: it changes no routing. The node must exist, have outcomes and be reachable from the agent, or deployment fails with ABADA-APL-REVIEW-001. |
routes |
map of {next, description, when?} |
no | 2–8 next steps the agent chooses from by description; the engine adds a required route enum to the output contract, vetoes a route whose CEL when is false (invalid output), writes <id>_route and records ROUTE_TAKEN. Replaces next (APL specification §2.8). |
next |
nodeId | yes, unless routes |
The step that runs after the agent completes. |
Example.
- id: extractAgent
type: agent
description: Extract structured application data
model: gemini-3.6-flash
prompt: |
Extract {income, creditScore, requestedAmount} from the payload.
Return strict JSON only.
inputs:
payload: ${payload}
result_variable: extracted
confidence_threshold: 85
next: creditRulesWhat it does. A step that hands work to an external system on a named topic. Workers subscribe to that topic, pick the durable job up, do the work, and report back. This is the standard "call an integration / webhook sink / microservice" step — the deterministic sibling of the AI agent.
When to use it. Any service call your own workers (or partner workers) perform: persisting records, calling a webhook, integrating with an ERP, etc.
BPMN equivalent. Service Task executed as an external task on the declared topic. APL replaces the Camunda external-task pattern with a typed node.
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
service |
string | yes | The topic workers subscribe to, e.g. decision.record. |
on_error |
nodeId or [{code?, then}] |
no | Where to go when the worker reports an error (a single target, or one per error code plus an optional catch-all), or when the last attempt fails (code WORK_FAILED). Studio draws it as an error edge. |
on_timeout |
{ after, then } |
no | If the task is not done within after, it is cancelled and the flow continues at then. |
next |
nodeId | yes | The step that runs after the task completes. |
Example.
- id: persist
type: engine-task
description: Persist decision record
service: decision.record
next: doneWhat it does. Starts a governed child instance of another process in the same project, waits for it, and continues with only the variables the node maps back. The child runs under its own definition version, locks and history; nothing else of the parent leaks in or out.
When to use it. A sub-case with its own lifecycle and reviewers (a fraud check, a KYC refresh) that several processes reuse.
BPMN equivalent. Call Activity. Studio draws it that way; importing a BPMN
callActivity is still rejected.
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
process |
string | yes | Key of the process to call, in this project. Its current version is pinned when this process is deployed; redeploy this process to call a newer one. A process cannot call itself. |
inputs |
map | no | Child variable → expression on this instance's variables, e.g. case_id: ${case.id}. When the child declares metadata.variables, only those names are accepted and values must fit their types. Nothing else is passed. |
outputs |
map | yes | This instance's variable → child variable copied back when the child completes. Nothing else returns. |
max_depth |
integer | no | 1–10. A stricter nesting limit than the engine's abada.call-process.max-depth (default 4). |
on_error |
nodeId or [{code?, then}] |
no | Where to go when the child fails or is cancelled (CHILD_FAILED), its inputs do not fit (CHILD_INPUT_INVALID), the depth limit is reached (CHILD_DEPTH_EXCEEDED) or it cannot start (CHILD_START_REFUSED). Without it, a CHILD_FAILED incident opens; retrying it starts a new child. |
on_timeout |
{ after, then } |
no | If the child has not completed within after, it is cancelled and the flow continues at then. |
next |
nodeId | yes | The step after the child completes. |
Example.
- id: fraud_check
type: call-process
process: fraud_check
inputs: { case_id: "${case_id}", amount: "${amount}" }
outputs: { fraud_verdict: verdict }
on_error: manual_review
on_timeout: { after: PT4H, then: manual_review }
next: decideWhat it does. Runs a governed child process once per item of a list, a bounded
number at a time, and gathers one result per item into a list. It is a
call-process repeated over a list: the child
version is pinned at deployment, every item is its own instance with its own lock,
audit trail and contract, and nothing but the declared outputs returns.
- id: check_each
type: for-each
items: "${documents}" # evaluates to a list
process: document_check
item_input: document # the child variable that receives each item
inputs: { case_id: "${case_id}" }
outputs: { verdict: verdict } # result field: child variable
max_items: 50 # required: the list is never unbounded
max_parallel: 5 # default 1
results: check_results
on_error: manual_review
next: decide| Field | Required | Meaning |
|---|---|---|
items |
yes | Expression evaluated on the instance's variables. It must give a list; anything else fails the node with FOR_EACH_ITEMS_INVALID. |
process, inputs, outputs, max_depth |
yes (inputs, max_depth optional) |
As for call-process. outputs maps a field of each result entry to the child variable it is copied from. |
item_input |
yes | The child variable that receives the current item. The engine sets it; it cannot also be in inputs. |
max_items |
yes | At most 1000, and never more than the engine's abada.for-each.max-items (default 100). A longer list fails the node with FOR_EACH_TOO_MANY before any child exists. |
max_parallel |
no (1) | At most 50, and never more than abada.for-each.max-parallel (default 20). |
results |
yes | The variable that receives the list, in item order: { index, ok: true, outputs } or { index, ok: false, error }. |
on_error, on_timeout, next |
as for call-process |
Failure. An item that fails (its child fails or is cancelled, its input does not fit the child's declared
type, it cannot start) is recorded as a failed result; the other items run to the end. When every item has
settled, results is written and, if any failed, on_error is taken once with the code
FOR_EACH_ITEM_FAILED (a node without on_error opens a CHILD_FAILED incident, and retrying it runs the whole
list again). An empty list passes straight to next with an empty results. A timeout, a cancel or a
failure of the parent cancels the running children and starts no more. Item values are never written to the
audit history, only counts.
BPMN equivalent. None: BPMN callActivity with a multi-instance marker is rejected on import.
What it does. A small JavaScript snippet the engine runs inside the
workflow transaction — no worker, no network call. Script steps are rejected
at deployment unless the operator sets ABADA_SCRIPTS_ENABLED=true (the dev
profile does). The sandbox has no Java access; instance variables enter as JSON
and are available by name and through a variables map, and changed or new
variables are written back as JSON. Numbers produced by a script are doubles.
There is no CPU time limit.
When to use it. Fast, deterministic, in-process work: derive a value,
transform data, compute a flag. Do not use it for slow or unreliable calls
(that is what engine-task is for) — the transaction stays open while the
script runs.
BPMN equivalent. Script Task. BPMN camunda:class delegates are a
separate, allow-listed mechanism (ABADA_DELEGATES_ALLOWED_CLASSES).
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
script |
string | yes | The JavaScript body. A blank body is rejected at deployment. |
format |
string | no | Script engine name. Default javascript. |
next |
nodeId | yes | The step that runs after the script. |
Example.
- id: deriveTier
type: script
description: Derive the shipping tier
script: |
variables.put('shippingTier', variables.orderValue >= 100 ? 'premium' : 'standard');
next: doneWhat it does. A human review step. The engine creates a task that a person
(or the listed groups) must complete before the flow continues. The task name
people see is the node's description.
When to use it. Any step that requires human judgment, sign-off or data entry: approving a loan, reviewing a claim, filling in a missing field.
BPMN equivalent. User Task with candidate groups.
approval-gate is a deprecated alias that still deploys. It accepts
assignees, mode and sla_hours but not formKey; use human-input for
new documents (Studio always writes human-input).
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
assignees |
string[] | yes | The candidate groups whose members may claim the task, e.g. [risk-officers]. Values are group names, not usernames. At least one is required. |
formKey |
string | no | Form key for task-form rendering (BPMN camunda:formKey). Resolved by bare slug to a project FORM resource. formId is a deprecated alias. |
mode |
serial | parallel |
no | Authoring hint only. The engine does not enforce serial or parallel sign-off. |
sla_hours |
number | no | Service level in hours (e.g. 24). When it passes with the task still open, the engine escalates the task in place: it stays open, escalate_to groups are added and TASK_SLA_BREACHED is emitted. slaHours is a deprecated alias. |
escalate_to |
string[] | no | Groups added as candidates when the task misses sla_hours. Requires sla_hours. |
on_error |
nodeId | no | Where to go when the task is failed. Without it a failed task opens an incident. |
on_timeout |
{ after, then } |
no | If the task is not done within after (e.g. P3D), it is cancelled and the flow continues at then. |
outcomes |
map | no | Decisions the reviewer chooses from, e.g. approve: { next: publish } and reject: { next: draft, comment: required }. Each outcome has its own next; the node then declares no next. The engine writes <id>_outcome and <id>_comment. See the APL specification §2.7. |
requireDoubleSignOff |
boolean | no | Reserved; it currently has no runtime effect. |
next |
nodeId | yes, unless outcomes |
The step that runs after the task is completed. |
Example.
- id: approvalGate
type: human-input
description: Senior credit officer sign-off
assignees: [risk-officers]
formKey: credit-sign-off
sla_hours: 24
next: routingExample with a decision. The reviewer approves, or rejects with a comment
that the agent reads on its next attempt as ${review_comment}:
- id: review
type: human-input
description: Review the draft
assignees: [reviewers]
outcomes:
approve: { next: done }
reject: { next: draft, comment: required }What it does. A deterministic decision table — the same idea as a DMN table, but executed by the engine itself inside the workflow transaction. You declare inputs (expressions over instance variables), a list of ordered rules, and the outputs each rule produces. No AI involved: the same inputs always give the same decision, and every application is audited.
When to use it. Any policy that can be written as if-then rules: credit scoring, risk classification, entitlement checks. This is the "law" that constrains AI agents in the Abada doctrine.
BPMN equivalent. Business Rule Task with a DMN table. APL replaces the external DMN-table binding with a native in-transaction table.
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
decisionKey |
string | no | Stable audit key for the table. Defaults to DMN_<nodeId>. |
hitPolicy |
FIRST | UNIQUE | COLLECT |
no | How to apply matching rules. Default FIRST. |
inputs |
array | map | no | The inputs the rules test against, each bound to a variable expression, e.g. score: "${applicant.creditScore}". |
rules |
array | yes | The ordered rules (§4.7.1). At least one is required. |
next |
nodeId | yes | The step that runs after the decision is applied. |
Hit policies, simply:
| Policy | What it means |
|---|---|
FIRST |
The first matching rule (top to bottom) wins. |
UNIQUE |
Exactly one rule may match. More than one → the step fails and rolls back. |
COLLECT |
Every matching rule's outputs are merged (later rules win name conflicts). |
4.7.1 Rules. Each rule is either a when rule or the fallback:
| Property | Type | Required | What it does |
|---|---|---|---|
when |
string | yes* | The condition, evaluated against the resolved input names, e.g. score >= 750 and income >= 60000. |
otherwise |
boolean | map | no | Marks the fallback rule — applied when no when rule matches. At most one per table. |
then |
map | yes | The outputs written to instance variables, e.g. { riskLevel: LOW, autoApprove: true }. |
* required unless the rule is the otherwise fallback.
Example.
- id: creditRules
type: decision-table
description: Credit risk classification
decisionKey: DMN_CREDIT_V1
hitPolicy: FIRST
inputs:
score: "${applicant.creditScore}"
income: "${applicant.annualIncome}"
rules:
- when: "score >= 750 and income >= 60000"
then: { riskLevel: LOW, autoApprove: true }
- otherwise: true
then: { riskLevel: HIGH, autoApprove: false }
next: approvalGateWhat it does. An either/or router. The flow arrives, the conditions
are evaluated in order, and exactly one branch is taken. If no condition
matches, the flow takes the default branch (the else rule — or the last
rule when no else is declared).
When to use it. Routing on a single value or decision: "if low risk → notify, otherwise → screen for fraud".
BPMN equivalent. Exclusive Gateway with a default flow.
Properties. Routes via rules — next must not be set.
| Property | Type | Required | What it does |
|---|---|---|---|
rules[].if |
string | yes* | The condition expression, e.g. ${riskLevel == 'LOW'}. |
rules[].else |
nodeId | no | The id of the default branch target — taken when no condition matches. |
rules[].then |
nodeId | yes | The id of the node this branch routes to. |
* required unless the rule is the else default.
Example.
- id: routing
type: condition
description: Route by risk
rules:
- if: "${riskLevel == 'LOW'}"
then: notify
- else: persist
then: persistWhat it does. A fork/join switch for running branches at the same time. As a fork it sends one token down every declared branch unconditionally. As a join it waits until every one of those branches has arrived, then continues.
When to use it. Steps that must all happen and none depends on the others — e.g. run a credit check and a fraud check concurrently, then merge.
BPMN equivalent. Parallel Gateway.
Properties. One form per role:
| Form | Property | Type | Required | What it does |
|---|---|---|---|---|
| Fork | branches |
nodeId[] | yes | At least two distinct node ids; one token is sent to each. next forbidden. |
| Join | next |
nodeId | yes | The single successor after every branch token arrives. branches omitted. |
Example.
- id: fanout
type: parallel
description: Run credit and fraud checks concurrently
branches:
- creditDesk
- fraudDesk
- id: rejoin # both creditDesk and fraudDesk route here
type: parallel
next: archiveWhat it does. A "every branch that matches" router. As a fork it
evaluates each rule and sends a token down every branch whose condition
matches — zero, one, or several. If nothing matches, the explicit else
rule fires; without an else, the step fails loudly (the engine never guesses
an empty route). As a join it waits for exactly the branches it actually
started, then continues.
When to use it. Combinations of conditions, e.g. "notify the customer AND notify the broker if both flags are set", where the two paths partially overlap.
BPMN equivalent. Inclusive Gateway.
Properties. One form per role:
| Form | Property | Type | Required | What it does |
|---|---|---|---|---|
| Fork | rules[].if |
string | yes* | Condition expression; every matching rule fires a token. |
| Fork | rules[].else |
nodeId | no | Default branch — fires when no rule matches. Only way to declare a default. |
| Fork | rules[].then |
nodeId | yes | The target node of this branch. |
| Join | next |
nodeId | yes | The single successor after the started branches arrive. |
* required unless the rule is the else default.
Example.
- id: route
type: inclusive
description: Route by path
rules:
- if: "${path == 'C' || path == 'CD'}"
then: taskC
- if: "${path == 'D' || path == 'CD'}"
then: taskD
- id: rejoin # taskC and taskD both route here
type: inclusive
next: archiveWhat it does. A race between competing waits. The process registers a wait on every catch child (a message, a timer, or a signal) at the same time. The first one to fire wins: the flow continues down that child's path, and every sibling wait is cancelled in the same transaction — a late loser can never trigger a duplicate transition. Pending races survive a restart.
When to use it. "First of several things happens" scenarios: wait for a fast-track message or a one-hour timeout, whichever comes first.
BPMN equivalent. Event-Based Gateway with intermediate catch events.
Properties. Routes via events — next must not be set. At least two
competing children are required.
| Property | Type | Required | What it does |
|---|---|---|---|
events[].type |
message-catch | timer | signal |
yes | What this child waits on. |
events[].message |
string | message-catch | The message name. Correlated against the correlationKey variable. |
events[].duration |
string | timer | ISO-8601 duration, e.g. PT1H (one hour). |
events[].signal |
string | signal | The broadcast signal name. |
events[].next |
nodeId | yes | The node that runs if this child wins the race. |
Example.
- id: eventRace
type: event-gateway
description: FastTrack or timeout
events:
- type: message-catch
description: Fast-track the review
message: FastTrackMessage
next: rejoin
- type: timer
description: Escalate after an hour
duration: PT1H
next: rejoinWhat it does. Pauses the process until a message arrives. The instance
registers a durable subscription; when a message with this name arrives and
correlates against the correlationKey variable, the process resumes.
When to use it. Waiting on an external system's asynchronous response (approval from another service, a webhook callback, a human pressing a button in another app).
BPMN equivalent. Intermediate Catch Event (message).
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
message |
string | yes | The message name the runtime matches during correlation. |
next |
nodeId | yes | The node that runs after the message arrives. |
Example.
- id: catchFastTrack
type: message-catch
description: Wait for the fast-track message
message: FastTrackMessage
next: doneWhat it does. Pauses the process for a fixed duration, then resumes. The wait is durable: the engine schedules a job and the process survives a restart while waiting.
When to use it. Time-based waits: "wait one hour", "give the human 24 h before escalating".
BPMN equivalent. Intermediate Catch Event (timer, duration form).
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
duration |
string | yes | ISO-8601 duration, e.g. PT1H, PT30M, P1D. Validated at deployment. |
next |
nodeId | yes | The node that runs when the timer fires. |
Example.
- id: catchTimeout
type: timer
description: Wait one hour
duration: PT1H
next: doneWhat it does. Pauses the process until a broadcast signal fires. Unlike a message (which correlates to one specific instance), a signal is broadcast: one signal resumes every waiting instance subscribed to that name.
When to use it. Fan-out notifications shared across many process instances — "the freeze is lifted", "it's GO time", "proceed to release".
BPMN equivalent. Intermediate Catch Event (signal).
Properties.
| Property | Type | Required | What it does |
|---|---|---|---|
signal |
string | yes | The broadcast signal name. |
next |
nodeId | yes | The node that runs when the signal fires. |
Example.
- id: catchGo
type: signal
description: Wait for the go signal
signal: proceed
next: doneFour nodes can act as forks (split one token into several) and three as joins (wait for several tokens, then continue). This table summarizes the two forms so the property panel can guide authors to the right shape.
| Node | Fork — how you declare it | Join — how you declare it |
|---|---|---|
condition (exclusive) |
rules with if/else; exactly one branch is taken |
n/a — an exclusive gateway never joins |
parallel |
branches (≥2) — all fire unconditionally |
next — waits for every branch |
inclusive |
rules — every matching if fires; else is the only default |
next — waits for the branches it actually started |
event-gateway |
inline events (≥2) — waits on all, first to fire wins, rest cancelled |
n/a — children route to downstream nodes; a downstream join counts it as one logical stream |
Forks and joins are the same node type in APL — the shape you give it
(branches/rules/events vs. next) decides which role it plays. The
engine rejects mixing the two forms (branches + next, rules + next,
events + next).
The property tables above are the contract for each node's configuration panel:
- Every Required property must be editable from the panel, and the panel should flag it while empty.
- Every optional property is something the panel may expose; where a default
exists (e.g. agent
temperature= 0.2), the panel should show the default when the field is left empty, exactly as the engine would apply it. - Fork/join nodes (§5) should offer a clear "fork vs. join" shape selector and only show the relevant fields for the chosen role.
- The BPMN equivalent column is author-facing context: the panel can show it as a subtitle ("compiles to: User Task") to help BPMN-savvy authors orient themselves.
See the technical specification for validation rules, error codes, and the complete production example.