Events
Events represent something that happens during a process: a process starts, something is waited for, a result is emitted, an exception arises, time passes. Each event combines a position (start, end, intermediate catch, intermediate throw, boundary) with an event definition that says what kind of trigger or effect is involved.
Event definitions
The engine supports the following event definition types. Not every type is valid at every position.
| Definition | XML element | Used for |
|---|---|---|
| None | (no event-definition child) | Plain start/end/throw points, the simplest form |
| Message | messageEventDefinition messageRef="..." | Send or wait for a correlated message |
| Timer | timerEventDefinition with timeDate, timeDuration, or timeCycle child | Schedule, wait, or fire on a cycle |
| Signal | signalEventDefinition signalRef="..." | Broadcast or wait for a named signal |
| Link | linkEventDefinition name="..." | In-process "goto" between throw and catch |
| Error | errorEventDefinition errorRef="..." | Throw or catch a business error |
| Escalation | escalationEventDefinition escalationRef="..." | Throw or catch an escalation up the scope hierarchy |
| Compensation | compensateEventDefinition | Trigger or handle compensation for completed activities |
| Conditional | conditionalEventDefinition with condition child | Fire when a FEEL expression turns true |
| Terminate | terminateEventDefinition | End the enclosing scope immediately |
messageRef, signalRef, errorRef, and escalationRef must reference a top-level <message> / <signal> / <error> / <escalation> declaration in the same <definitions> block — see Global definitions.
Position × definition compatibility
| Start | End | Intermediate catch | Intermediate throw | Boundary | |
|---|---|---|---|---|---|
| None | ✓ | ✓ | — | ✓ | — |
| Message | ✓ | ✓ | ✓ | ✓ | ✓ |
| Timer | ✓ | — | ✓ | — | ✓ |
| Signal | ✓ | — | ✓ | ✓ | ✓ |
| Link | — | — | ✓ | ✓ | — |
| Error | event sub-process only | ✓ | — | — | ✓ (always interrupting) |
| Escalation | event sub-process only | ✓ | — | ✓ | ✓ |
| Compensation | — | — | — | ✓ | ✓ |
| Conditional | event sub-process only | — | — | — | ✓ |
| Terminate | — | ✓ | — | — | — |
Compensation start events and compensation end events are not supported. Trigger compensation with a compensation intermediate throw event followed by a none end event, and handle it with a compensation boundary event attached to the activity to compensate.
Start events
Start events define how a process or sub-process begins.
Attributes
| Attribute | Default | Notes |
|---|---|---|
id | — | Required |
name | — | Optional |
isInterrupting | true | Applies to start events inside event sub-processes only. Ignored on top-level start events. |
Auto-start behavior
When a process is deployed, typed start events are registered so an instance is started automatically when the trigger arrives:
| Definition | Trigger |
|---|---|
| None | Process started explicitly via API |
| Message | A message with the matching name is published |
| Signal | A signal with the matching name is broadcast |
| Timer | The configured time arrives. Recurring expressions (R/PT1H) re-arm after each firing, one-shot (date) expressions fire once and deactivate |
Re-deploying the process under a new version deactivates the previous version's auto-start registrations and arms the new ones.
Examples
<!-- None start event -->
<bpmn:startEvent id="start">
<bpmn:outgoing>f1</bpmn:outgoing>
</bpmn:startEvent>
<!-- Message start event -->
<bpmn:startEvent id="start-msg">
<bpmn:outgoing>f1</bpmn:outgoing>
<bpmn:messageEventDefinition messageRef="Msg_Payment" />
</bpmn:startEvent>
<!-- Recurring timer start (every hour) -->
<bpmn:startEvent id="start-timer">
<bpmn:outgoing>f1</bpmn:outgoing>
<bpmn:timerEventDefinition>
<bpmn:timeCycle xsi:type="bpmn:tFormalExpression">R/PT1H</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</bpmn:startEvent>
<!-- Non-interrupting message start in an event sub-process -->
<bpmn:startEvent id="esp-start" isInterrupting="false">
<bpmn:outgoing>f1</bpmn:outgoing>
<bpmn:messageEventDefinition messageRef="Msg_Notify" />
</bpmn:startEvent>
Validation
| Check | Severity |
|---|---|
| Executable process has no start event | Error |
| Start event has any incoming sequence flow (BPMN 2.0 §10.4.2) | Error |
| Error / escalation start event placed outside an event sub-process (BPMN 2.0 §10.4.2 — these triggers are only legal as the starter of an event sub-process) | Error |
| Compensation start event used in any position | Error (not supported) |
| Conditional start event at top level — the engine does not auto-instantiate top-level conditional starts, use an event sub-process or external trigger | Warning |
messageRef / signalRef / errorRef / escalationRef references an unknown definition | Error |
End events
End events finish a path of execution. A process must have at least one reachable end event (or a node with no outgoing flow, which is treated as an implicit end).
Behavior by definition
| Definition | Effect |
|---|---|
| None | Completes the path normally |
| Message | Publishes a message, then completes. With quantum:taskDefinition set, a worker performs the dispatch, otherwise the engine publishes directly |
| Error | Throws an error, propagates up the scope hierarchy to the nearest matching error boundary or error start event in an event sub-process |
| Escalation | Throws an escalation up the scope chain to the nearest matching listener. With quantum:taskDefinition a worker performs the dispatch |
| Terminate | Cancels the nearest enclosing activity scope (sub-process, ad-hoc sub-process, or root process) and continues from that scope's outgoing flow. At the root, the entire instance terminates. A terminate inside a called process ends only the called process |
Compensation end events are not supported. Use a compensation intermediate throw event followed by a none end event.
Examples
<!-- None end event -->
<bpmn:endEvent id="end">
<bpmn:incoming>f_last</bpmn:incoming>
</bpmn:endEvent>
<!-- Error end event -->
<bpmn:endEvent id="end-error">
<bpmn:incoming>f_err</bpmn:incoming>
<bpmn:errorEventDefinition errorRef="Err_Timeout" />
</bpmn:endEvent>
<!-- Terminate end event -->
<bpmn:endEvent id="end-terminate">
<bpmn:incoming>f_abort</bpmn:incoming>
<bpmn:terminateEventDefinition />
</bpmn:endEvent>
<!-- Message end event dispatched by a worker -->
<bpmn:endEvent id="end-msg">
<bpmn:extensionElements>
<quantum:taskDefinition type="send-notification" />
</bpmn:extensionElements>
<bpmn:incoming>f_notify</bpmn:incoming>
<bpmn:messageEventDefinition messageRef="Msg_Done" />
</bpmn:endEvent>
Validation
| Check | Severity |
|---|---|
| End event has any outgoing sequence flow (BPMN 2.0 §10.4.3) | Error |
| Compensation end event | Error (not supported) |
messageRef / errorRef / escalationRef references an unknown definition | Error |
Message end event with no messageRef and no quantum:taskDefinition | Warning (publishes a nameless message) |
Escalation end event with no escalationRef and no quantum:taskDefinition | Warning (throws a nameless escalation) |
Intermediate catch events
Intermediate catch events pause the token until an external trigger arrives, then advance.
Behavior by definition
| Definition | Behavior |
|---|---|
| Message | Waits for a correlated message matching the referenced name. If a buffered message is already available it fires immediately |
Timer — timeDuration | Waits the specified ISO 8601 duration from the moment the event is reached |
Timer — timeDate | Waits until the specified ISO 8601 datetime |
Timer — timeCycle | Fires repeatedly on the cycle |
| Signal | Waits for a broadcast matching the referenced name, checks the buffer first |
| Link | Receives control from a matching link throw event of the same name in the same scope. Acts as a goto target |
A catch event placed immediately after an event-based gateway must have exactly one incoming flow (from the gateway).
Examples
<!-- Message catch -->
<bpmn:intermediateCatchEvent id="catch-msg" name="Wait for Payment">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:messageEventDefinition messageRef="Msg_Payment" />
</bpmn:intermediateCatchEvent>
<!-- Timer catch (duration) -->
<bpmn:intermediateCatchEvent id="wait-5m" name="Wait 5 min">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:timerEventDefinition>
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">PT5M</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<!-- Signal catch -->
<bpmn:intermediateCatchEvent id="catch-signal" name="Receive Abort">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:signalEventDefinition signalRef="Sig_Abort" />
</bpmn:intermediateCatchEvent>
<!-- Link catch (jump target) -->
<bpmn:intermediateCatchEvent id="catch-link" name="Land">
<bpmn:outgoing>f3</bpmn:outgoing>
<bpmn:linkEventDefinition name="LinkA" />
</bpmn:intermediateCatchEvent>
Validation
| Check | Severity |
|---|---|
| No event definition present | Error |
| Catch event has anything other than exactly one outgoing sequence flow (BPMN 2.0 §10.5.4 — intermediate events have one input, one output) | Error |
| Catch event after an event-based gateway has more than one incoming flow | Error |
messageRef / signalRef references an unknown definition | Error |
Intermediate throw events
Intermediate throw events emit something — a message, signal, escalation, link, or compensation trigger — and then advance immediately. A throw with no event definition is a pass-through.
Behavior by definition
| Definition | Behavior |
|---|---|
| None | Pure pass-through |
| Message | Publishes the message with the current scope variables. If quantum:taskDefinition is also set, switches to external-job mode (see below) |
| Signal | Broadcasts the signal to all subscribers |
| Link | Transfers control to the matching link catch in the same scope. Acts as a goto |
| Escalation | Throws an escalation up to the nearest matching listener |
| Compensation | Triggers compensation, activityRef can target a specific completed activity. When it targets a call activity, it runs the activity's own boundary handler if it has one, otherwise it propagates into the called process |
Execution modes (apply to every throw kind)
A throw event with quantum:taskDefinition type="..." switches into worker-driven mode regardless of which event definition is attached: the engine creates a job, the worker performs the actual publish/throw, and the throw advances when the worker reports completion. In this mode the messageRef / escalationRef / etc. become optional — the worker controls what is dispatched.
| Mode | When active | Behavior |
|---|---|---|
| Direct dispatch | No quantum:taskDefinition | Engine publishes / throws immediately and advances |
| External job | quantum:taskDefinition type="..." is set | Engine creates a persistent job, throw advances when the worker completes the job |
The same applies to message and escalation end events.
Examples
<!-- Signal throw -->
<bpmn:intermediateThrowEvent id="throw-signal" name="Broadcast Abort">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:signalEventDefinition signalRef="Sig_Abort" />
</bpmn:intermediateThrowEvent>
<!-- Escalation throw -->
<bpmn:intermediateThrowEvent id="throw-esc" name="Escalate">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:escalationEventDefinition escalationRef="Esc_Review" />
</bpmn:intermediateThrowEvent>
<!-- Link throw (jump source) -->
<bpmn:intermediateThrowEvent id="throw-link" name="Jump">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:linkEventDefinition name="LinkA" />
</bpmn:intermediateThrowEvent>
<!-- Compensation throw — compensate everything completed in scope -->
<bpmn:intermediateThrowEvent id="throw-comp" name="Compensate All">
<bpmn:incoming>f1</bpmn:incoming>
<bpmn:outgoing>f2</bpmn:outgoing>
<bpmn:compensateEventDefinition />
</bpmn:intermediateThrowEvent>
Validation
| Check | Severity |
|---|---|
messageRef / signalRef / escalationRef references an unknown definition | Error |
Message throw with no messageRef and no quantum:taskDefinition (publish has no name to correlate against) | Error |
Escalation throw with no escalationRef, no inline escalationCode, and no quantum:taskDefinition (no listener can match) | Error |
Link throw has no matching link catch with the same name in the same scope | Error |
Compensation throw activityRef does not resolve to a known node | Error |
Boundary events
Boundary events are attached to an activity (task, sub-process, or call activity) and fire while the activity is running.
Attributes
| Attribute | Default | Notes |
|---|---|---|
id | — | Required |
name | — | Optional |
attachedToRef | — | Required. ID of the host activity (must be in the same scope) |
cancelActivity | true | true = interrupting (host is cancelled, only the boundary path continues), false = non-interrupting (host continues, a parallel token starts from the boundary). Ignored on compensation boundaries |
Behavior by definition
| Definition | Notes |
|---|---|
| Message | Fires when a correlated message arrives while the host is active |
| Timer | timeDuration and timeDate fire once. timeCycle (e.g. R3/PT1M) fires repeatedly and is non-interrupting only |
| Error | Always interrupting. Fires when the host throws a matching error |
| Signal | Fires when the matching signal is broadcast while the host is active |
| Escalation | Fires when the host throws a matching escalation |
| Conditional | Fires when the FEEL condition evaluates to true while the host is active |
| Compensation | Marks this boundary as the trigger for a compensation handler. Linked to the handler activity by an <association> element. Does not use cancelActivity — fires only when compensation is explicitly thrown for the host (after the host has completed), and never cancels the host |
Examples
<!-- Interrupting timer boundary: 30-minute timeout -->
<bpmn:boundaryEvent id="timer-boundary" attachedToRef="long-task" cancelActivity="true">
<bpmn:outgoing>f_timeout</bpmn:outgoing>
<bpmn:timerEventDefinition>
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">PT30M</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Non-interrupting message boundary: send a reminder while waiting -->
<bpmn:boundaryEvent id="msg-boundary" attachedToRef="approval-task" cancelActivity="false">
<bpmn:outgoing>f_notify</bpmn:outgoing>
<bpmn:messageEventDefinition messageRef="Msg_Reminder" />
</bpmn:boundaryEvent>
<!-- Error boundary: handle a known failure type -->
<bpmn:boundaryEvent id="err-boundary" attachedToRef="service-task" cancelActivity="true">
<bpmn:outgoing>f_error</bpmn:outgoing>
<bpmn:errorEventDefinition errorRef="Err_Timeout" />
</bpmn:boundaryEvent>
<!-- Compensation boundary, with handler linked by association -->
<bpmn:boundaryEvent id="comp-boundary" attachedToRef="book-hotel">
<bpmn:compensateEventDefinition />
</bpmn:boundaryEvent>
<bpmn:serviceTask id="cancel-hotel" name="Cancel Hotel" isForCompensation="true">
<bpmn:extensionElements>
<quantum:taskDefinition type="cancel-hotel-worker" />
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:association id="assoc1" sourceRef="comp-boundary" targetRef="cancel-hotel" />
<!-- Cyclic non-interrupting timer: fire every minute, up to 3 times -->
<bpmn:boundaryEvent id="cycle-boundary" attachedToRef="long-task" cancelActivity="false">
<bpmn:outgoing>f_tick</bpmn:outgoing>
<bpmn:timerEventDefinition>
<bpmn:timeCycle>R3/PT1M</bpmn:timeCycle>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
Validation
| Check | Severity |
|---|---|
attachedToRef is empty or doesn't resolve to an activity in the same scope | Error |
| No event definition present | Error |
Error boundary has cancelActivity="false" | Error |
Timer boundary has none of timeDuration/timeDate/timeCycle | Error |
Timer boundary uses timeCycle while interrupting | Error |
Timer boundary mixes timeCycle with timeDuration or timeDate | Warning (timeCycle wins) |
Conditional boundary has empty condition | Error |
| Compensation boundary has no associated handler activity | Error |
Compensation handler is not marked isForCompensation="true" | Error |
messageRef / signalRef / errorRef / escalationRef references an unknown definition | Error |
FAQ
What's the difference between a BPMN message event and a signal event?
A message event is point-to-point and correlated by both message name AND a correlation key — when a publisher's name and key both match a subscriber's, the engine selects exactly one matching subscription and delivers the message to it. A signal event is a broadcast that matches on name only: every subscriber listening on the same signal name receives it. Use messages to route to a specific running instance (for example, the payment confirmation for order #123), and signals when multiple parts of one or more processes need to react to the same external occurrence.
How does message correlation work in QuantumBPM?
A non-start message event declares a correlation key with <quantum:subscription correlationKey="..."/> (or the zeebe:subscription alias), a FEEL expression. When a message catch event, boundary event, or receive task is reached, the engine evaluates that expression against the running instance's scope variables and registers a subscription tagged with the resulting value. A publisher — whether an internal message throw, send task, or message end event, or an external API call — submits the message with its own correlation value. The engine matches by name AND by correlation value using subset/containment semantics: a subscriber listening on {orderId: 123, region: "EU"} matches a publisher narrowing on {orderId: 123}, but not the other way around. Exactly one matching subscriber is selected and signalled per publish (BPMN 2.0 §13.2 exactly-once point-to-point delivery). Matching is type-sensitive — the number 123 never matches the string "123". Process-root message start events are the exception: they correlate by name alone, since they instantiate new processes rather than route to a running one.
What's the difference between an error event and an escalation event?
Error events represent a failure that must abort the current activity scope — they are caught by an interrupting error boundary event or by an error start event in an event sub-process, and they always cancel the throwing activity. Escalation events are softer notifications that travel up the scope hierarchy and can be caught by either interrupting or non-interrupting boundaries, the throwing activity does not need to be cancelled. Rule of thumb: error means an exception that must be handled, escalation means something noteworthy happened that someone should know about.
What is the difference between an interrupting and a non-interrupting boundary event?
An interrupting boundary event (cancelActivity="true", the default) cancels the host activity when it fires, execution continues only down the boundary's outgoing flow. A non-interrupting boundary event (cancelActivity="false") leaves the host running and starts a parallel branch from the boundary — typical for sending reminders or escalations while a long-running task continues. Error boundaries are always interrupting, the engine rejects non-interrupting error boundaries at validation.
Can a BPMN start event have an incoming sequence flow?
No. A start event must not have any incoming sequence flow — the modeler and the engine both reject this at deployment per BPMN 2.0 §10.4.2. If you need to re-enter a flow from elsewhere in the process, use a link catch event as a goto target, or model the entry point as a sub-process called from a parent.
Can a BPMN end event have an outgoing sequence flow?
No. An end event terminates a path of execution and cannot have outgoing flows — both the modeler and the engine reject this at deployment per BPMN 2.0 §10.4.3. If you need follow-up behaviour after a particular branch finishes, place an intermediate throw event (or a regular task) before the end event instead.
What does a terminate end event do?
A terminate end event immediately cancels every still-running token within the nearest enclosing activity scope — the sub-process, ad-hoc sub-process, or root process that contains the terminate. Execution then continues from that scope's outgoing flow. At the root, the entire process instance terminates. A terminate placed inside a called process ends only the called process, not its caller.
What are the different BPMN timer event expressions and what do they do?
timeDuration is an ISO 8601 duration (PT5M, PT30S) and waits the specified amount of time from the moment the event is reached. timeDate is an ISO 8601 datetime (2026-12-31T23:59:00Z) and waits until that exact moment. timeCycle is a recurring expression (R/PT1H for unbounded, R3/PT1M for a fixed number of repeats) that fires repeatedly. Cyclic timers are only valid on non-interrupting timer boundary events and on event-sub-process start events — interrupting boundaries with timeCycle are rejected at deployment.
What happens if a message arrives before its intermediate catch event is reached?
The message is buffered for up to one hour by default. When the token later reaches the catch event, the engine first checks the buffer for a matching message — same name, matching correlation key — and consumes it immediately if found, otherwise the catch registers a subscription and waits for a future publish. The same applies to signals (though signals match by name only). This makes processes robust to network-induced reordering: a downstream catch event correctly receives a message that physically arrived during an upstream task.
What's the difference between an error end event and an error boundary event?
An error end event throws a business error from a path inside an activity. An error boundary event, attached to the enclosing activity (a sub-process or service task), catches an error of a matching type and routes execution down its outgoing flow while cancelling the host activity. They are used together: throw with an error end event inside the activity, catch with an error boundary on the outside. Both reference the same <error> declaration through errorRef.
Can a timer boundary event use a cycle expression?
Only on a non-interrupting boundary. The engine and the modeler reject timeCycle on an interrupting timer boundary at deployment, because a cyclic interrupting boundary would cancel its host after the first tick and leave no host to interrupt on subsequent ticks. Use timeCycle with cancelActivity="false" to fire repeatedly while the host keeps running — the typical use case is sending periodic reminders during a long-running user task.
Can an error boundary event be non-interrupting?
No. Error boundary events are always interrupting — they cancel the host activity when they fire. The modeler rejects cancelActivity="false" on an error boundary at validation. If you need a non-cancelling reaction to an error condition, throw a signal or escalation event from inside the host activity's flow and catch it on the outside with a non-interrupting boundary instead.