Version 18 refactors parallel gateway convergence and removes sequence flow discards altogether. Most diagrams run unchanged, but hosts that listen for discard events, assert discarded counters, or rely on multiple start events all running need attention.
Definitionrequiresnew- No more flow discards
- Parallel gateways
- Multiple start events are mutually exclusive
- Starting activities that are not start events
- Conditional outbound flows on non-gateway activities
- Shake output has changed
- Distinct task behaviours
- Resuming state saved by v17
- Types
- Notable additions
Definition no longer news itself up when called as a function.
// v17
const definition = Definition(context, options);
// v18
const definition = new Definition(context, options);In v17 an untaken branch was propagated as a chain of discards: the untaken sequence flow published flow.discard, the downstream activity ran a discard run, discarded its own outbound flows, and so on until the tokens met at a join. In v18 an untaken branch is simply left untouched — no discard token travels the diagram.
Consequences for a host:
- sequence flow
discardevents and downstreamactivity.discardcascades from branching decisions no longer occur. Listeners waiting for them never fire. - sequence flow and activity
discardedcounters stay at0for untaken branches. Do not assert on them to detect which path was not taken — trackflow.take/activity.endinstead. - an activity discarded through the api (
api.discard()) still runs a discard and increments its owndiscardedcounter; it is only the flow-driven propagation that is gone. - exclusive and inclusive gateways touch only the taken flow(s); the untaken conditional flows still have their conditions evaluated, but produce no events.
- compensation associations follow the same rule: a compensation target only observes
association.take, and the associationdiscardcounter stays0when a transaction completes without compensating.
The mechanics behind the removal — how parallel joins complete without discard tokens — are described in ParallelGateway.
- a converging parallel gateway enters execution as soon as the first inbound sequence flow is touched, instead of waiting for all inbound flows. It discovers its upstream peers with a shake and completes when all peers have reported. Discovered peers are cached per runtime instance, so loops and stop/resume skip the start-up shake; the cache is rebuilt on recover.
- a new
activity.convergeevent is published when the gateway evaluates convergence. - during a shake a converging parallel gateway publishes
activity.shake.converge(previouslyactivity.shake.join). - new activity readonly property
isParallelGatewayindicates a parallel gateway.
Start events in the same process are now mutually exclusive entry points: the first start event to fire discards the others still armed. Two start branches can never both run.
A v17 diagram that relied on multiple start events running in parallel — e.g. two start events converging into a parallel join, or a joining task taken twice — must be redesigned, typically with a single start event forking through a parallel gateway.
- an
IntermediateCatchEventwithout inbound sequence flows is no longer started by default. Give it an inbound flow, or run it explicitly. - start activities that are not start events (e.g. a starting receive task, or any activity without inbound flows) are no longer auto-discarded when a start event fires. They are armed as genuine tokens and must be signalled, completed, or discarded — otherwise they keep the process running.
When all conditional outbound flows of a non-gateway activity evaluate falsy, the branch now ends quietly instead of the activity throwing. Only exclusive and inclusive gateways still require a taken or default flow and emit an error when none is.
If you relied on the thrown error to detect a dead end, listen for the activity end event and inspect taken outbound flows instead.
The shake sequence differs from v17:
- a throwing link
IntermediateThrowEventis no longer marked as an end (isEnd); it has no outbound sequence flows but continues at its catch, so a shake no longer records it as a dead-end sequence. - converging parallel gateways report
activity.shake.convergeas noted above.
Hosts persisting or diffing shake results should re-run and re-baseline them.
UserTask, ManualTask, SendTask, and BusinessRuleTask (and TextAnnotation, Group, Category) are distinct exports with their own behaviour instead of aliases of SignalTask/ServiceTask/Dummy. Identity checks like UserTask.Behaviour === SignalTask.Behaviour no longer hold, and overriding one behaviour's prototype no longer leaks into its base or siblings. Element type overrides passed via options.types are unaffected — they are keyed per BPMN type as before.
States saved by v17 can be recovered and resumed. v18 stamps saved state with a stateVersion (the package major); a recovered state with an older version — legacy unstamped states are treated as version 0 — triggers migrations on resume:
- start events are reconciled to the mutually-exclusive rule: if one entry point already won, the start events still armed are discarded instead of resumed as live entry points.
- in-flight
flow.discard,flow.looped, andassociation.discardtokens left on process queues by the old discard propagation are acked on sight, so they no longer strand process completion.
No action is required beyond resuming with v18; saving again stamps the current version.
Runtime type declarations are now generated from JSDoc and bundled into types/index.d.ts. The shipped surface is self-contained — it no longer references moddle-context-serializer — and declares the serialized-definition contracts locally (SerializableContext, ActivityDefinition, …). Recompile a TypeScript host against the new declarations; some previously loose (any) spots are now typed and may surface pre-existing mismatches.
Not breaking, but new since v17 and useful when upgrading:
- throwable error classes are exposed via the
bpmn-elements/errorssubpath:import { ActivityError, BpmnError, RunError } from 'bpmn-elements/errors'. - postponed activity content declares
accepts— the api message types the waiting run acts on beyondstopanddiscard(e.g.['signal', 'error']for a user task,['cancel']for a timer). Readable fromgetPostponed()api content and wait events, see SharedApi. - a conditional event and a timer can be cancelled through the api:
cancel()completes the event without the condition being met / the timer timing out. AdHocSubProcesshas a dedicated behaviour honouringorderingandcompletionCondition.- catch, boundary, and start events with
parallelMultiple="true"wait for all their event definitions. - called processes and sub processes are seeded with
environment.variables.input; multi-instance loops pass their loop context as input. ActivityExecution,ProcessExecution,DefinitionExecution,EventDefinitionExecution, andExpressionsare exported from the package root.- new activity readonly properties
isStartEventandisParallelGateway.