BPMN Design Criteria¶
This page documents design constraints and conventions that apply when authoring BPMN processes and DMN decisions for deployment in the RONL Business API platform. Following these criteria ensures correct runtime behaviour and prevents issues in the citizen portal and caseworker interface.
BusinessRuleTask: camunda:mapDecisionResult¶
A BusinessRuleTask that calls a DMN decision must declare how the engine hands the decision output to the process. For a decision with one output column that is the camunda:mapDecisionResult attribute together with camunda:resultVariable; for a decision with several output columns it is one camunda:outputParameter per column, and no result variable at all.
Operaton supports four mapping modes:
| Value | Returns | Use when |
|---|---|---|
singleEntry |
The value of a single output column from a single matched rule | The decision has one output column and a hit policy that guarantees at most one result (UNIQUE, FIRST, ANY) |
singleResult |
All output columns of a single matched rule as a Map object |
Not with a result variable โ read a multi-output decision through output parameters instead |
collectEntries |
A List of values from a single output column across all matched rules |
The decision uses COLLECT and you need all values from one column |
resultList |
A List of Map objects, one per matched rule |
The decision uses COLLECT and you need all columns of all matched rules |
Why this matters¶
The mapping mode determines the Java type stored in the process variable. If the wrong mode is used, the value stored is not a primitive (String, Integer, Boolean) but a complex object (Map, List). Any downstream expression, script task, or UI component that expects a simple value will fail silently or display [object Object].
singleEntry โ the default for single-output decisions¶
For decisions with one output column and a UNIQUE hit policy, always use singleEntry. This stores the raw value directly in the result variable.
<bpmn:businessRuleTask
id="Task_AssessPermit"
name="Assess tree felling permit"
camunda:resultVariable="permitDecision"
camunda:decisionRef="TreeFellingDecision"
camunda:mapDecisionResult="singleEntry">
The process variable permitDecision will contain the string "Permit" or "Reject" directly, usable in gateway conditions and script tasks without any unwrapping.
Multi-output decisions: one output parameter per column¶
For a decision with several output columns, leave out camunda:resultVariable and camunda:mapDecisionResult, and give the task one camunda:outputParameter per output column that the process needs. Each reads its column from the decision result. HrOnboardingProcess (examples/organizations/flevoland/HrOnboardingProcess.bpmn) does this for the three outputs of EmployeeRoleAssignment:
<bpmn:businessRuleTask id="Task_DetermineRoles" name="Determine roles" camunda:decisionRef="EmployeeRoleAssignment">
<bpmn:extensionElements>
<camunda:inputOutput>
<camunda:outputParameter name="assignedRoles">${decisionResult.getSingleResult().getEntry("assignedRoles")}</camunda:outputParameter>
<camunda:outputParameter name="candidateGroups">${decisionResult.getSingleResult().getEntry("candidateGroups")}</camunda:outputParameter>
<camunda:outputParameter name="accessLevel">${decisionResult.getSingleResult().getEntry("accessLevel")}</camunda:outputParameter>
</camunda:inputOutput>
</bpmn:extensionElements>
...
</bpmn:businessRuleTask>
Each output is then a plain String, Boolean or Integer process variable, usable directly in gateway conditions, forms and script tasks. The variable may be named differently from its column: AwbShellProcess maps the legalBasis column of ArchivesActRetention to archivingLegalBasis. As with singleEntry, the decision needs a hit policy that yields a single result.
AwbShellProcess maps only the column it branches on: Task_Phase3_Completeness calls AwbCompletenessCheck and maps isComplete through an output parameter, and Gateway_Complete tests ${isComplete == true} and ${isComplete == false}.
Why not singleResult with a result variable¶
camunda:mapDecisionResult="singleResult" with a camunda:resultVariable stores the whole matched rule as one variable: a Java-serialised java.util.HashMap. A REST read that deserialises the process variables can write that object back, so concurrent reads of the same instance collide with ENGINE-03005, Operaton's optimistic-locking failure (sgort/linked-data-explorer#273). The example processes were rewritten to output parameters for that reason (f3117dd). The map is also no use where a simple value is expected: a gateway has to reach into it by key, and a frontend that renders it with String(value) shows [object Object].
A script task that only copies entries out of the map into separate variables is unnecessary: the output parameters produce those variables directly, without the map ever being stored.
Gateway conditions and variable types¶
Gateway conditionExpression values must match the actual type of the process variable being tested.
| Variable type | Correct expression | Incorrect |
|---|---|---|
String |
${permitDecision == "Permit"} |
${permitDecision == true} |
Boolean |
${isComplete == true} or ${isComplete} |
${isComplete == "true"} |
Integer |
${treeDiameter > 30} |
${treeDiameter > "30"} |
An output of a multi-output decision, mapped through an output parameter, is one of these types and is tested the same way. Do not branch on a field of a Map variable (${completenessResult.isComplete == true}): it needs the map stored, which is the pattern to avoid โ see Why not singleResult with a result variable.
Process variable naming conventions¶
All process variables set by the RONL Business API platform follow these conventions:
| Convention | Example | Reason |
|---|---|---|
| camelCase | permitDecision, treeDiameter |
Consistent with JavaScript and Java conventions |
No underscores in names used for processVariables filtering |
municipality not tenant_id |
Operaton's processVariables query filter uses _ as a separator: municipality_eq_utrecht |
| Tenant context variables reserved | municipality, initiator, assuranceLevel, applicantId |
Injected automatically by tenant.middleware.ts; do not reuse these names in DMN outputs |
Tenant context variables¶
The backend middleware automatically injects the following variables into every process instance at start time. These must not be overwritten by DMN outputs or script tasks.
| Variable | Type | Source | Value |
|---|---|---|---|
municipality |
String |
JWT municipality claim |
e.g. utrecht |
initiator |
String |
JWT sub claim |
Keycloak user ID |
applicantId |
String |
JWT sub claim |
Same as initiator; used for history queries |
assuranceLevel |
String |
JWT loa claim |
laag, midden, or hoog |
These variables are available in all process expressions and DMN input columns. The municipality variable is used by the task queue to filter tasks to the correct tenant.
Two more variables are stamped by the backend, never by a process: edocsAuthor (the employee's e-mail, or username) and edocsAuthorName (display name), set from the caller's token when a staff member starts a process or completes a task. Background eDOCS archiving reads them to title documents "namens โฆ". Together with municipality, originTenantId and applicantId they are reserved (RESERVED_PROCESS_VARIABLES in auth/tenant-access.ts): a task completion that carries one is refused, a /v1 start drops a sent edocsAuthor/edocsAuthorName, and an M2M start that carries one is refused. Do not use these names for your own variables.
camunda:historyTimeToLive¶
Every process definition must declare camunda:historyTimeToLive on the <bpmn:process> element. Without it, Operaton logs a warning on deployment and may refuse deployment depending on engine configuration.
<bpmn:process
id="AwbShellProcess"
name="AWB General Administrative Law Act - Generic Process"
isExecutable="true"
camunda:historyTimeToLive="365">
Recommended values:
| Context | Value | Rationale |
|---|---|---|
| AWB citizen processes | 365 |
One year; aligns with Awb appeal and audit requirements |
| Short-lived subprocess | 180 |
Six months for intermediate processes with no independent legal value |
| Development / test | 30 |
Avoids accumulation of test instances in Operaton Cockpit |
Lanes and phase markers: the caseworker process view¶
The caseworker board shows where an open task stands in its process โ the phase stepper ("Waar sta ik"), the steps per role, and the whole process as a swimlane โ only for processes whose deployed BPMN carries the information. The backend reads it with parseSwimlane() in packages/backend/src/rip-swimlane/bpmn-swimlane.ts, served by GET /v1/process/definition/key/:key/swimlane; nothing about a process is configured anywhere else.
The stepper shows one of two phase schemes, and a process uses exactly one: the Awb phases, marked with ronl:awbPhase, or phases the process declares itself, marked with ronl:phase. The model the endpoint returns carries the scheme as phaseSet (scheme awb or bpmn, a label, and the ordered phases) and every node's phase code as phase; both are absent when the process has no valid marker.
Lanes¶
- Without a
laneSet, there is no process view. The task keeps the flat list of steps it had before. - Put the user tasks in the lanes. A lane's roles are derived from the
candidateGroupsof the user tasks drawn in it (itsflowNodeRefs), never configured. A lane with no user task has no roles, so it cannot be recognised as the caseworker's. candidateGroupsmust be literals. Comma-separated group names are read; an expression (${โฆ}or#{โฆ}) names no group until runtime and is dropped.- Lanes are ordered by the
yof their shape in the diagram; node positions are recomputed, not read from the diagram.
ronl:awbPhase¶
Declare the namespace on <bpmn:definitions> and mark the nodes where a phase begins:
<bpmn:definitions
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:ronl="http://ronl.nl/schema/1.0"
...>
<bpmn:process id="AwbShellProcess" ...>
<bpmn:userTask id="Task_Phase6_Notify"
name="Fase 6: Aanvrager informeren over besluit (Awb 3:6)"
camunda:formRef="awb-notify-applicant"
camunda:candidateGroups="caseworker"
ronl:documentRef="example_treefelling_beschikking"
ronl:awbPhase="6">
Any element in the list of flow-node types below can carry a marker โ in AwbShellProcess they sit on the start event, a script task, business-rule tasks, the call activity, a user task and two gateways.
| Value | Stepper label |
|---|---|
1 |
Rechtsbetrekking |
2 |
Ontvangst |
3 |
Ontvankelijkheid |
4+5 |
Behandeling en besluit |
6 |
Bekendmaking |
7 |
Betaling |
8 |
Ketenproces |
archivering |
Archivering |
The values are exactly those of AWB_PHASES in @ronl/shared. Phases 4 and 5 are one step, because the kapvergunning subprocess handles treatment and decision together. Any other value is ignored without a warning, so a typo such as 4-5 or Archivering leaves the node unmarked.
- An unmarked node inherits. It takes the latest phase among its forward predecessors, so a join after an optional step (payment) lands in the later phase. Back edges โ the flows that close a rework loop โ are excluded, so a loop cannot pull an earlier step into a later phase. Marking the first node of each phase is therefore enough.
- No markers, no stepper. A process with no valid marker gets no phases, and the board hides the stepper. A task in an unmarked subprocess takes the phase of the call activity that started it, walking up the call chain.
On the board the eyebrow names the legal phase number, Waar sta ik ยท Awb-fase 4+5 ยท stap 4 van 8, and the caption under the stepper reads Fase 4+5 ยท Behandeling en besluit. Archiving has no phase number: the eyebrow names it (Awb-fase Archivering) and the caption reads Archiefwet ยท Archivering.
A process's own phases: ronl:phases, ronl:phaseLabel, ronl:phase¶
A process that does not follow the Awb declares its phases on <bpmn:process> and marks nodes with ronl:phase. Besluitvorming onder gedelegeerde bevoegdheid does this:
<bpmn:process id="GedelegeerdBesluitProcess"
name="Besluitvorming onder gedelegeerde bevoegdheid"
ronl:phases="voorbereiding:Voorbereiding;toetsing:Advies en toetsing;memorandum:Memorandum;ondertekening:Ondertekening;escalatie:Escalatie;registratie:Registratie en archivering"
ronl:phaseLabel="Fase"
...>
<bpmn:startEvent id="StartEvent_Besluit" name="Besluit voorbereiden" ronl:phase="voorbereiding">
...
<bpmn:userTask id="Task_AdviesToetsing" name="Advies en toetsing" ronl:phase="toetsing" ...>
| Attribute | On | Meaning |
|---|---|---|
ronl:phases |
<bpmn:process> |
The phases in order, as code:Name entries separated by ; |
ronl:phaseLabel |
<bpmn:process> |
The word before a phase's position, Fase when absent |
ronl:phase |
a flow node | The code of the phase that begins at this node |
- The order of
ronl:phasesis the stepper's order, and a phase is referred to by its position: the stepper labels the second phase Fase 2, the eyebrow reads Waar sta ik ยท Fase 2 ยท stap 2 van 6, and the caption Fase 2 ยท Advies en toetsing. The codes are identifiers and never appear on screen. - An unusable entry is skipped, not guessed at: one without a code or a name, or one repeating an earlier code. Only the first
:separates code from name, so a name may itself contain a colon. When no usable entry remains, the process is read as an Awb process. - A marker must name a declared code. A
ronl:phasevalue that is not inronl:phasesis ignored without a warning, like an unknownronl:awbPhasevalue. - The two schemes never mix. Once
ronl:phasesdeclares a usable phase,ronl:awbPhaseis not read anywhere in the process; without it,ronl:phaseis not read. - Inheritance, rework loops, subprocesses and "no markers, no stepper" work as for
ronl:awbPhaseabove, so marking the first node of each phase is enough.
Other attributes the view reads¶
| Element or attribute | Shown as |
|---|---|
scriptTask |
A step of its own kind (script) |
businessRuleTask with camunda:decisionRef |
A decision step carrying its DMN key |
callActivity with calledElement |
A subprocess step; the view can open the called process |
camunda:formRef |
The form a step uses, per node |
ronl:documentRef |
The documents a step produces; a comma-separated list for several |
Only the first <bpmn:process> in a file is read, and only these flow-node types become nodes: start, end and intermediate events, user, manual, receive, script, business-rule, service and send tasks, call activities, subprocesses, and exclusive, inclusive, event-based and parallel gateways.
Related pages¶
- Business Rules Execution โ BPMN/DMN execution via Operaton
- Operaton DMN Compatibility โ DMN authoring constraints for the Linked Data Explorer
- API Specification โ
/v1/processand/v1/taskendpoints