Skip to main content

Structure

Structural elements group flow nodes into reusable scopes, invoke other processes, and iterate over collections.

ElementWhen to use it
Embedded sub-processGroup steps into an inline scope with isolated variables
Event sub-processReact to events inside a scope without disrupting the main flow
Ad-hoc sub-processActivate a dynamic set of activities chosen at runtime
Call activityInvoke another, separately-defined process as a child
Multi-instance loopRun an activity once per item in a collection
Standard loopRepeat an activity while a condition holds

Embedded sub-process

An inline scope. The sub-process has its own start and end events and contains any flow elements, including nested sub-processes.

Attributes

AttributeDefaultNotes
idRequired
nameOptional
triggeredByEventfalsetrue makes this an event sub-process
defaultID of the outgoing flow taken when every other outgoing condition is false. See Sequence Flow → Default flows

Child loop elements

ElementEffect
<multiInstanceLoopCharacteristics>Run the body once per item in a collection (or loopCardinality times) — see Multi-instance loop
<standardLoopCharacteristics>Re-run the entire body in series until loopMaximum is hit or loopCondition becomes false — see Standard loop

The two are mutually exclusive on the same activity.

Scope behavior

  • The sub-process scope has isolated variables. I/O mappings on the sub-process control what crosses the boundary.
  • Boundary events on the sub-process element interrupt or extend the entire sub-process — see Boundary events.

Example

<bpmn:subProcess id="payment-sub" name="Process Payment">
<bpmn:extensionElements>
<quantum:ioMapping>
<quantum:input source="=orderId" target="order_id" />
<quantum:output source="=paymentStatus" target="status" />
</quantum:ioMapping>
</bpmn:extensionElements>
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>

<bpmn:startEvent id="sub-start"><bpmn:outgoing>sub-f1</bpmn:outgoing></bpmn:startEvent>
<bpmn:serviceTask id="charge" name="Charge Card">
<bpmn:extensionElements>
<quantum:taskDefinition type="payment-worker" />
</bpmn:extensionElements>
<bpmn:incoming>sub-f1</bpmn:incoming>
<bpmn:outgoing>sub-f2</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:endEvent id="sub-end"><bpmn:incoming>sub-f2</bpmn:incoming></bpmn:endEvent>
<bpmn:sequenceFlow id="sub-f1" sourceRef="sub-start" targetRef="charge" />
<bpmn:sequenceFlow id="sub-f2" sourceRef="charge" targetRef="sub-end" />
</bpmn:subProcess>

Validation

CheckSeverity
Sub-process contains no start eventError
Sub-process contains no end eventWarning

Event sub-process

A <subProcess triggeredByEvent="true"> lives inside a parent scope but isn't connected by sequence flows. It's started by its own typed start event when a matching event happens in the parent scope.

Differences from a normal sub-process

PropertyNormalEvent sub-process
triggeredByEventfalsetrue
Sequence flow connectionHas incoming/outgoing flowsNone — triggered by its start event
Start event typeNoneTyped (message, timer, signal, error, escalation, conditional, compensation)
Start event isInterruptingN/Atrue cancels the parent; false runs in parallel

Examples

<!-- Interrupting message-triggered event sub-process -->
<bpmn:subProcess id="esp-cancel" triggeredByEvent="true">
<bpmn:startEvent id="esp-start" name="Order Cancelled" isInterrupting="true">
<bpmn:outgoing>esp-f1</bpmn:outgoing>
<bpmn:messageEventDefinition messageRef="Msg_Cancel" />
</bpmn:startEvent>
<bpmn:serviceTask id="esp-cleanup" name="Cleanup Order">
<bpmn:extensionElements>
<quantum:taskDefinition type="cleanup-worker" />
</bpmn:extensionElements>
<bpmn:incoming>esp-f1</bpmn:incoming>
<bpmn:outgoing>esp-f2</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:endEvent id="esp-end"><bpmn:incoming>esp-f2</bpmn:incoming></bpmn:endEvent>
<bpmn:sequenceFlow id="esp-f1" sourceRef="esp-start" targetRef="esp-cleanup" />
<bpmn:sequenceFlow id="esp-f2" sourceRef="esp-cleanup" targetRef="esp-end" />
</bpmn:subProcess>

<!-- Non-interrupting conditional event sub-process: fires on each false→true transition -->
<bpmn:subProcess id="esp-watch" triggeredByEvent="true">
<bpmn:startEvent id="esp-cond-start" isInterrupting="false">
<bpmn:outgoing>esp-c1</bpmn:outgoing>
<bpmn:conditionalEventDefinition>
<bpmn:condition xsi:type="bpmn:tFormalExpression">priority = "high"</bpmn:condition>
</bpmn:conditionalEventDefinition>
</bpmn:startEvent>
<!-- ... -->
</bpmn:subProcess>

Validation

CheckSeverity
Event sub-process has incoming or outgoing sequence flowsError
Event sub-process has more or fewer than one typed start eventError
Conditional start event has empty conditionError
Error / escalation / compensation start event placed outside an event sub-process (BPMN 2.0 §10.4.2)Error

Ad-hoc sub-process

An ad-hoc sub-process contains activities that are activated dynamically — not via fixed sequence flows. Which activities run, and when the sub-process completes, are controlled by FEEL expressions.

Attributes and extensions

Attribute / ExtensionNotes
default (attribute)ID of the outgoing flow taken when every other outgoing condition is false. See Sequence Flow → Default flows
quantum:adHoc activeElementsCollection="..."FEEL expression returning the list of activity IDs to activate
<completionCondition> childFEEL expression evaluated after each activity completes; the sub-process ends when it returns true
<multiInstanceLoopCharacteristics>Run the entire ad-hoc once per item in a collection
<standardLoopCharacteristics>Re-run the entire ad-hoc until loopMaximum / loopCondition stop. Mutually exclusive with multi-instance

Each activity inside an ad-hoc sub-process must carry a quantum:taskDefinition (or be one of the supported task types).

Behavior with end events

End events behave differently inside an ad-hoc sub-process:

End event variantEffect
None / message / signal / escalationConsumes the token; the ad-hoc keeps running until completionCondition is satisfied
TerminateEnds the ad-hoc scope immediately, cancelling all sibling tokens; the parent process continues from the ad-hoc's outgoing flow

Example

<bpmn:adHocSubProcess id="ad-hoc-tasks" name="Dynamic Tasks">
<bpmn:extensionElements>
<quantum:adHoc activeElementsCollection="=[&quot;task-review&quot;, &quot;task-approve&quot;]" />
<quantum:ioMapping>
<quantum:output source="=allCompleted" target="allDone" />
</quantum:ioMapping>
</bpmn:extensionElements>
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>

<bpmn:serviceTask id="task-review" name="Review">
<bpmn:extensionElements>
<quantum:taskDefinition type="review-worker" />
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:serviceTask id="task-approve" name="Approve">
<bpmn:extensionElements>
<quantum:taskDefinition type="approve-worker" />
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:completionCondition xsi:type="bpmn:tFormalExpression">=reviewDone and approveDone</bpmn:completionCondition>
</bpmn:adHocSubProcess>

Validation

CheckSeverity
Ad-hoc sub-process contains no activitiesError
Ad-hoc sub-process contains a start eventError

Call activity

Invokes a separately-defined process as a child. The child runs in its own scope; the parent waits until the child completes.

Attributes and extensions

Attribute / ExtensionRequiredNotes
calledElement (attribute)Standard BPMN attribute; ID of the called process. Overridden by the extension when both are present
default (attribute)noID of the outgoing flow taken when every other outgoing condition is false. See Sequence Flow → Default flows
quantum:calledElement processId="..."yesID of the process to invoke
quantum:calledElement propagateAllChildVariables="..."notrue merges all child output variables into the parent scope. Default is false
quantum:ioMappingnoInputs are passed to the child; outputs are pulled back
multiInstanceLoopCharacteristicsnoRun the called process once per item — see Multi-instance loop
standardLoopCharacteristicsnoRe-call the child process serially up to loopMaximum times. Mutually exclusive with multi-instance — see Standard loop

Variable propagation

ModeWhat flows back
propagateAllChildVariables="false" (default)Only variables explicitly mapped via output mappings
propagateAllChildVariables="true"All child variables are merged into the parent scope on completion

Boundary events on the call activity are supported. An interrupting boundary cancels the child workflow.

Example

<bpmn:callActivity id="run-sub-process" name="Run Sub-Process">
<bpmn:extensionElements>
<quantum:calledElement processId="child-process-id" propagateAllChildVariables="false" />
<quantum:ioMapping>
<quantum:input source="=customerId" target="customer_id" />
<quantum:output source="=result" target="subProcessResult" />
</quantum:ioMapping>
</bpmn:extensionElements>
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
</bpmn:callActivity>

Validation

CheckSeverity
processId and calledElement are both emptyError

Multi-instance loop

Attached as <multiInstanceLoopCharacteristics> to a task, sub-process, or call activity. Runs the host activity multiple times — in parallel or sequentially.

Attributes and child elements

Attribute / ElementNotes
isSequential (attribute)true runs iterations one after another; false (default) runs them in parallel
<loopCardinality> childFEEL expression returning the number of iterations (e.g. "5", "=items.size()")
<completionCondition> childFEEL expression; the loop exits early when it returns true

The quantum:loopCharacteristics extension holds collection-based iteration:

AttributeNotes
inputCollectionFEEL expression returning the collection to iterate over (e.g. "=orderItems")
inputElementVariable name for the current item in each iteration (e.g. "item")
outputCollectionVariable name to collect per-iteration results into
outputElementFEEL expression extracting the result from each iteration scope (e.g. "=result")

<loopCardinality> and inputCollection are mutually exclusive — pick one. outputCollection / outputElement only make sense with inputCollection.

Variables visible inside the loop

The engine injects spec-standard counters that any FEEL expression inside the activity can read:

VariableScopeDescription
numberOfInstancesOuterTotal inner instances created
numberOfActiveInstancesOuterCurrently executing instances (always 0 or 1 for sequential)
numberOfCompletedInstancesOuterInstances completed normally so far
numberOfTerminatedInstancesOuterInstances cancelled or interrupted
loopCounterPer instance1-based sequence number of the current instance

A completionCondition can use any of these to exit early:

=numberOfCompletedInstances > 0          // exit after the first finishes
=numberOfCompletedInstances >= 2 // exit once at least two have finished
=loopCounter = 1 and numberOfInstances > 3 // exit after instance 1 in a large batch

Variable visibility after the loop completes

The injected counters and loopCounter live on the multi-instance scopes — they are never propagated into the activity's outer scope when the loop finishes. The only thing that flows out is the configured outputCollection.

If you need a count after the activity, derive it from data that already propagated:

WantFEEL on the next activity
Total instances run=length(inputCollection) (your original input collection)
Instances that completed normally=length(outputCollection)
Instances cancelled by completionCondition=length(inputCollection) - length(outputCollection)

Boundary events on a multi-instance activity

A boundary event attached to an MI activity sees the counters via the normal scope chain — at the moment the boundary fires, the firing instance's parent is the MI wrapper, so the boundary handler's input/output FEEL can read numberOfCompletedInstances, numberOfActiveInstances, loopCounter, etc. directly. Once the MI scope cleans up these values are gone, so capture anything you need into the parent scope via the boundary handler's <output> mapping.

<bpmn:boundaryEvent id="mi-cancel" attachedToRef="process-items">
<bpmn:extensionElements>
<quantum:ioMapping>
<quantum:output source="=numberOfCompletedInstances" target="completedBeforeCancel" />
<quantum:output source="=numberOfTerminatedInstances" target="cancelledCount" />
</quantum:ioMapping>
</bpmn:extensionElements>
<bpmn:errorEventDefinition errorRef="ItemErr" />
</bpmn:boundaryEvent>

Examples

<!-- Parallel multi-instance service task iterating a collection -->
<bpmn:serviceTask id="process-items" name="Process Each Item">
<bpmn:extensionElements>
<quantum:taskDefinition type="item-processor" />
<quantum:ioMapping>
<quantum:output source="=itemResult" target="itemResult" />
</quantum:ioMapping>
</bpmn:extensionElements>
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics>
<bpmn:extensionElements>
<quantum:loopCharacteristics
inputCollection="=orderItems"
inputElement="item"
outputCollection="processedItems"
outputElement="=itemResult" />
</bpmn:extensionElements>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:serviceTask>

<!-- Sequential multi-instance call activity -->
<bpmn:callActivity id="call-each" name="Call Sub Per Item">
<bpmn:extensionElements>
<quantum:calledElement processId="item-sub-process" propagateAllChildVariables="false" />
<quantum:ioMapping>
<quantum:input source="=item" target="currentItem" />
<quantum:output source="=subResult" target="subResult" />
</quantum:ioMapping>
</bpmn:extensionElements>
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:multiInstanceLoopCharacteristics isSequential="true">
<bpmn:extensionElements>
<quantum:loopCharacteristics
inputCollection="=batches"
inputElement="item"
outputCollection="results"
outputElement="=subResult" />
</bpmn:extensionElements>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>

Validation

CheckSeverity
Both loopCardinality and inputCollection are setError
multiInstanceLoopCharacteristics and standardLoopCharacteristics are both set on the same activityError

Standard loop

A standard loop re-executes the same activity sequentially while a FEEL condition holds, capped by a maximum iteration count. Supported on:

Attributes and child elements

Attribute / ElementNotes
loopMaximum (attribute)Required. Maximum number of iterations (≥ 1). The loop ends when loopCounter reaches this value, regardless of the condition
testBefore (attribute)If true, the condition is checked before the first iteration — the activity is skipped entirely if the condition is false at that point. Default false
<loopCondition> childOptional FEEL expression. The loop continues as long as it returns true. Without it, the activity runs exactly loopMaximum times

Each iteration sets a loopCounter variable in the activity's surrounding scope (1-based). For sub-processes, the body re-enters in a freshly minted scope each iteration; for call activities, a fresh child workflow is started each time.

Behavior

SettingBehavior
testBefore="false" (default)Run at least once; check the condition after each completion
testBefore="true"Check first; skip entirely if the condition is false
loopMaximum="N"Hard cap — after N executions the loop exits
No loopConditionRun exactly loopMaximum times

Example

<!-- Run while counter < 5, capped at 10 iterations -->
<bpmn:serviceTask id="retry-task" name="Retry Until Done">
<bpmn:extensionElements>
<quantum:taskDefinition type="retry-processor" />
<quantum:ioMapping>
<quantum:output source="=counter" target="counter" />
</quantum:ioMapping>
</bpmn:extensionElements>
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:standardLoopCharacteristics loopMaximum="10">
<bpmn:loopCondition>=counter &lt; 5</bpmn:loopCondition>
</bpmn:standardLoopCharacteristics>
</bpmn:serviceTask>

Validation

CheckSeverity
loopMaximum is missing or ≤ 0Error
Both standardLoopCharacteristics and multiInstanceLoopCharacteristics are setError

When to use which

Use a standard loop when iterations are sequential and condition-driven, and the count isn't known up front. Use a multi-instance loop when you need parallel execution or are iterating a known collection.