Gateways
Gateways control how tokens split and merge in a process. Each gateway type has its own rules for which outgoing flows fire (split) and what it takes to advance (join).
Common attributes
| Attribute | Notes |
|---|---|
id | Required |
name | Optional |
default | Flow ID — fallback flow taken when no condition matches (exclusive and inclusive only) |
gatewayDirection | Informational, not enforced |
A default flow must be one of the gateway's outgoing flows and must not carry a conditionExpression.
Exclusive gateway (XOR)
Picks exactly one outgoing flow at a split. The first flow whose condition evaluates to true is taken, all others receive a "dead" token (the path is skipped). When used as a join, it's a pass-through merge — no synchronisation.
Behavior
| Scenario | Behavior |
|---|---|
| Split with conditions | Flows are evaluated in document order, first true wins |
Split with no match and a default | Default flow is taken |
Split with no match and no default | Runtime error |
| Join | Pass-through — every incoming token advances |
Example
<bpmn:exclusiveGateway id="gw-route" name="Route?" default="flow-default">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>flow-vip</bpmn:outgoing>
<bpmn:outgoing>flow-default</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="flow-vip" name="VIP" sourceRef="gw-route" targetRef="task-vip">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=tier = "vip"</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="flow-default" name="Standard" sourceRef="gw-route" targetRef="task-standard" />
Validation
| Check | Severity |
|---|---|
default doesn't match an outgoing flow | Error |
Default flow carries a conditionExpression | Error |
Non-default outgoing flow has no condition (when default is set) | Warning |
| Single outgoing flow has no condition and no default | Warning |
Parallel gateway (AND)
Splits into all outgoing flows simultaneously and joins by waiting for a token on every incoming flow.
Behavior
| Scenario | Behavior |
|---|---|
| Split | All outgoing flows are activated. No condition evaluation |
| Join | Waits until a live token has arrived on every incoming flow, then activates a single outgoing token |
Conditions on outgoing flows are ignored (with a warning at deployment).
Example
<bpmn:parallelGateway id="fork" name="Split">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:outgoing>f3</bpmn:outgoing>
</bpmn:parallelGateway>
<!-- ... tasks in parallel branches ... -->
<bpmn:parallelGateway id="join" name="Join">
<bpmn:incoming>f2-done</bpmn:incoming>
<bpmn:incoming>f3-done</bpmn:incoming>
<bpmn:outgoing>f4</bpmn:outgoing>
</bpmn:parallelGateway>
Validation
| Check | Severity |
|---|---|
Outgoing flow carries a conditionExpression | Warning (condition is ignored) |
Inclusive gateway (OR)
Activates every outgoing flow whose condition evaluates to true. The matching join waits for all of those branches to arrive.
Behavior
| Scenario | Behavior |
|---|---|
| Split | All matching conditions activate their flows in parallel, if none match and a default is set, only the default fires, if none match and no default, runtime error |
| Join | Waits for every branch the matching split activated, merges branch variables into the parent scope before advancing |
Example
<bpmn:inclusiveGateway id="gw-or" name="Select Channels" default="flow-email">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>flow-sms</bpmn:outgoing>
<bpmn:outgoing>flow-push</bpmn:outgoing>
<bpmn:outgoing>flow-email</bpmn:outgoing>
</bpmn:inclusiveGateway>
<bpmn:sequenceFlow id="flow-sms" sourceRef="gw-or" targetRef="send-sms">
<bpmn:conditionExpression>=smsEnabled</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="flow-push" sourceRef="gw-or" targetRef="send-push">
<bpmn:conditionExpression>=pushEnabled</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="flow-email" sourceRef="gw-or" targetRef="send-email" />
Validation
| Check | Severity |
|---|---|
default doesn't match an outgoing flow | Error |
Default flow carries a conditionExpression | Error |
Non-default outgoing flow has no condition (when default is set) | Warning |
Event-based gateway
Waits for the first of several events to fire and then takes the corresponding branch. Each outgoing flow must lead directly to an intermediate catch event with a message, timer, or signal trigger.
Attributes
| Attribute | Default | Notes |
|---|---|---|
instantiate | false | Not supported. Deployment fails with an error if set to true |
eventGatewayType | Exclusive | Only Exclusive (first event wins) is implemented. Parallel deploys without error but is treated as Exclusive at runtime |
Behavior
| Scenario | Behavior |
|---|---|
| First event fires | The winning branch advances, all other branches are cancelled |
| No event fires | Token waits indefinitely (use an attached timer to bound the wait) |
To express "every event that fires advances its own branch", use a parallel gateway followed by individual catch events instead of an event-based gateway in parallel mode.
Example
<bpmn:eventBasedGateway id="gw-event" name="Wait for First">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>flow-timer</bpmn:outgoing>
<bpmn:outgoing>flow-msg</bpmn:outgoing>
</bpmn:eventBasedGateway>
<bpmn:sequenceFlow id="flow-timer" sourceRef="gw-event" targetRef="catch-timer" />
<bpmn:intermediateCatchEvent id="catch-timer" name="30s Timeout">
<bpmn:incoming>flow-timer</bpmn:incoming>
<bpmn:outgoing>f-timeout</bpmn:outgoing>
<bpmn:timerEventDefinition>
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">PT30S</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:sequenceFlow id="flow-msg" sourceRef="gw-event" targetRef="catch-msg" />
<bpmn:intermediateCatchEvent id="catch-msg" name="Confirmation Received">
<bpmn:incoming>flow-msg</bpmn:incoming>
<bpmn:outgoing>f-confirmed</bpmn:outgoing>
<bpmn:messageEventDefinition messageRef="Msg_Confirm" />
</bpmn:intermediateCatchEvent>
Validation
| Check | Severity |
|---|---|
| Fewer than 2 outgoing flows | Error |
| No incoming flow | Error |
instantiate="true" set on the gateway | Error |
| Outgoing flow leads to a node that isn't an intermediate catch event | Error |
| Catch event trigger isn't message, signal, or timer | Error |
Outgoing flow carries a conditionExpression | Error |
FAQ
What's the difference between exclusive, parallel, and inclusive gateways in BPMN?
An exclusive (XOR) gateway takes exactly one outgoing flow at a split — the first whose condition returns true, or the default if none match. A parallel (AND) gateway takes every outgoing flow unconditionally, and at a join waits until a token has arrived on every incoming branch. An inclusive (OR) gateway takes every outgoing flow whose condition is true (or the default if none match), and its matching join waits until at least one branch has arrived and no other live token in scope can still reach it.
What happens at an exclusive gateway when no outgoing condition matches?
If the gateway has a default flow configured, the default flow is taken. If no default exists, the process fails at runtime with a routing error. Conditions are evaluated in the order the outgoing flows appear in the BPMN XML, and the first to return true wins.
Can a default flow have a condition expression?
No. A default flow must not carry a conditionExpression. The modeler and the engine both reject this at deployment with a validation error. The same rule applies to default flows on exclusive gateways, inclusive gateways, and on activities with multiple outgoing flows.
What happens if a parallel gateway join receives a token on only some of its incoming flows?
The join blocks. A parallel join waits until a live token has arrived on every incoming sequence flow, then fires exactly once. If one branch never produces a token — for example because an upstream task failed and was not compensated — the join waits indefinitely. Use a timer boundary event on the surrounding scope to bound the wait.
How does an inclusive (OR) join decide when to fire?
The inclusive join fires as soon as at least one branch has arrived AND no other live token elsewhere in the enclosing scope can still reach the join without first passing through it. This reachability check (Völzer-Vanhatalo enablement) handles loops, nested gateways, and asymmetric splits correctly. The join does not need to know which split forked the incoming tokens, and tokens parked in disjoint fragments of the process don't keep it waiting.
What can come after an event-based gateway?
Each outgoing flow must lead directly to an intermediate catch event with a message, signal, or timer trigger. Tasks, gateways, and any other element types are rejected at deployment. Conditional, multiple, and parallel-multiple catch events are also rejected — only message, signal, and timer are wired into the gateway's race selector.
Can an event-based gateway start a process instance (`instantiate="true"`)?
No. The instantiating event gateway from the BPMN 2.0 specification is not supported. Deployment fails with an explicit validation error if the instantiate attribute is set to true. To trigger a new process instance from an event, use a message start event or a signal start event on the process itself.
Does QuantumBPM support the BPMN complex gateway?
No. The complex gateway is rejected at deployment by both the modeler and the engine. Most use cases for the complex gateway — for example, "fire when 2 of 3 branches arrive" — can be expressed with an exclusive or inclusive gateway combined with FEEL conditions on the outgoing flows, or by adding a script task that computes the routing decision explicitly.
Can an exclusive gateway be used as a join to merge multiple branches?
Yes. Used as a join, an exclusive gateway is a simple pass-through merge: every incoming token advances independently with no synchronisation. This is the standard way to reconverge alternative branches (originally split by an XOR gateway) onto a common downstream path without waiting for the other branches.