bpmn-auto-layout turns semantic BPMN XML into complete BPMN DI. It recursively
lays out process and sub-process scopes, arranges collaboration participants,
routes connections, and emits shapes, edges, and labels. This document defines
the resulting geometry and the algorithm that produces it.
Generated process flow moves from left to right. The engine prioritizes:
- valid geometry: containment and docking are correct, unrelated shapes do not overlap, and edges do not pass through unrelated shapes;
- narrative: the primary path is continuous, branches are distinguishable, and exception paths remain separate from normal flow;
- polish: fewer crossings, bends, long edges, and unused space.
Layout regenerates geometry. It discards existing coordinates, dimensions, waypoints, and labels; existing DI determines only whether an embedded sub-process is expanded.
BPMN declaration order breaks ties. The same semantic input therefore produces byte-identical output.
BPMN is directed, nested, and lane-constrained. The engine uses a constrained layered layout:
- ranks establish left-to-right progress;
- semantic bands establish vertical narrative roles;
- containers recursively constrain child layouts;
- orthogonal routing connects final shape positions.
This is a BPMN-specific layered algorithm, not a generic graph layout followed by BPMN patches.
flowchart LR
A["Parse, validate, and select root"] --> B["Recursively lay out process scopes"]
B --> C["Assemble collaboration, when present"]
C --> D["Normalize and finalize connections"]
D --> E["Emit BPMN DI and labels"]
Within each process scope, the engine analyzes cycles and narrative structure, places flow nodes and containers, routes sequence flows, and then places artifacts and groups. A collaboration composes those completed process layouts, orders and aligns their participants, and routes message flows.
The layout state for each process, sub-process, or collaboration scope contains shape bounds, edge waypoints, and child layouts. Its fields have one-way ownership:
| Field | Owner | Meaning |
|---|---|---|
scope |
scope extraction | The semantic BPMN scope represented by this layout. |
children |
recursive scope layout | Independently laid-out child scopes. |
shapes |
placement and container stages; message-routing fixed point | Bounds for elements on this scope's plane. |
edges |
routing and connection finalization | Waypoints for connections on this scope's plane. |
emitInParent |
container placement | Whether a child scope's geometry is included on its parent's plane. |
Semantic policy produces placement decisions without mutating geometry. Placement writes shape bounds, routing writes initial edge waypoints, and the message-routing fixed point may enlarge resizable participant bounds. Connection finalization may repair only edge waypoints. External-label layout then computes label bounds from finalized plane geometry. Diagram generation reads that complete geometry without changing it and creates all BPMN DI.
layoutProcess parses XML with bpmn-moddle, selects a
collaboration when one exists or otherwise the first process, removes existing
diagrams, generates new geometry, and resolves with { xml, warnings }.
The engine rejects input when valid geometry would be misleading or undefined.
LayoutError provides stable codes for:
- invalid or cross-scope sequence flows;
- invalid message-flow endpoints;
- invalid boundary-event hosts;
- incompatible lane membership;
- invalid link-event pairs;
- invalid participant process references;
- unsupported visual elements;
- collaborations without at least one participant that references a process;
- routes that cannot avoid unrelated shapes;
- artifact or external-label searches that cannot find collision-free geometry.
Non-fatal omissions produce LayoutWarning
instances. After DI emission, the engine checks every supported visual shape and
connection across generated planes. DI_NOT_CREATED reports a semantic element
without an emitted shape or edge. GROUP_MEMBERS_NOT_FOUND reports a group
whose category value has no visible explicitly referenced members.
An empty definitions document remains valid and receives no invented process.
Each process and sub-process is laid out independently. layoutProcessScope
separates:
- ordinary flow nodes and sequence flows;
- boundary events and their handlers;
- event sub-processes;
- artifacts and associations.
Every extracted process flow node or artifact receives a working record with its BPMN element, declaration index, default size, boundary/artifact classification, expansion state, optional child layout, and eventual bounds. Sub-process contents are laid out before their parent.
Expanded sub-processes contain their child geometry on the parent plane. Collapsed sub-processes remain one parent-level activity and receive a separate plane for their child process.
Weakly connected components include ordinary sequence flows and boundary handlers. A component starts at:
- its earliest declared start event;
- otherwise its earliest node without an incoming sequence flow;
- otherwise its earliest declared node.
Normal disconnected components are laid out independently and stacked vertically in declaration order. Adding a later component does not move an earlier one.
The primary path, or spine, is selected one edge at a time:
- prefer an edge whose target can reach an end event;
- among eligible edges, prefer the BPMN default flow;
- otherwise use declaration order.
This prevents a dead-end alternative from becoming the main narrative merely because it was declared first. The selected edge and deterministic single-outgoing continuations are marked straight and routed before alternatives.
When unobstructed, a spine edge is one horizontal segment from the source's right center to the target's left center.
Band 0 is the spine. Other bands encode branch meaning:
- alternatives without a default alternate below, above, farther below, and farther above;
- alternatives to a default flow fan to one side;
- alternatives from an off-spine gateway fan farther away from the spine;
- error handlers occupy lower bands;
- escalation handlers occupy upper bands;
- paired link events align the catch continuation with the throw until that continuation rejoins other flow.
A band reservation includes the ranks over which its path exists. Disjoint paths may reuse a physical band; overlapping narratives may not.
Boundary events stay attached to their host. Escalation events use the top host edge and other boundary events use the bottom edge. Events sharing a host side are ordered by their handlers' outward destination distance, longest first; declaration order breaks equal-distance ties. Handler flows leave through the outside-facing side and never enter the host interior.
Named events attached to the top edge receive an explicit external label above the event and beside the handler exit, opposite its first horizontal direction.
markBackEdges performs a deterministic depth-first traversal from semantic
starts. The cycle graph includes ordinary sequence flows and an implicit edge
from each boundary-event host into its handler path. Edges that close a cycle
are temporarily excluded from rank assignment and later routed as feedback
edges.
assignRanks computes longest-path ranks over the remaining DAG. Boundary
handlers participate in a bounded fixed-point pass so their targets cannot
precede their hosts.
Rank assignment applies three BPMN-specific refinements:
- Detached, non-cyclic alternatives may reserve a horizontal bay before the spine continues. Boundary-handler paths reserve their complete single-lane span, allowing nearby alternatives to reuse bands.
- A boundary handler with a reserved bay claims the closest available band before a gateway alternative on the same side of the spine.
- Nested joins of the same gateway type may share a rank and connect vertically. Different gateway types retain a forward step.
Each rank becomes an x-position. Its width is the widest node in that rank. Each semantic band becomes a y-position. Nodes sharing a rank and band are separated in declaration order.
Semantic analysis compacts non-overlapping band intervals before any bounds are created. Shape placement then proceeds in this order:
- create initial coordinates from ranks and compacted bands;
- clear boundary-handler exits;
- separate same-rank shape overlaps inside each component;
- pack disconnected components;
- apply lane membership;
- dock boundary events.
Geometry uses these base constants:
| Constraint | Value |
|---|---|
| Horizontal gap | 100 px |
| Vertical gap | 80 px |
| Outer shape margin | 80 px |
| Expanded sub-process padding | 40 px |
| Named expanded sub-process title band | 28 px |
| Group padding | 40 px |
| Routing margin | 20 px |
| Participant header width | 30 px |
| Lane content padding | 40 px |
Layout may add space for containers and routing; it does not reduce these gaps. Bounds and waypoints are normalized to integers before DI emission.
Ad-hoc semantics do not impose an execution order on disconnected children. Using the normal vertical component stack would therefore create long, sparse containers.
The ad-hoc policy compacts without reading labels or authored coordinates:
- for a split whose branches reconverge, normal primary-path semantics choose the horizontal continuation while alternate paths use reduced rank weights and vertical space;
- disconnected components are packed in two dimensions toward a square footprint;
- component footprints include routing gaps, so packing does not create a shape arrangement the router cannot use.
Connected components still preserve their sequence-flow order.
An expanded embedded sub-process is sized around its child layout with the configured padding and a minimum size of 140 x 120 px. A named sub-process reserves the fixed title band inside its top padding; multiline titles do not enlarge it.
If an external label has no close preferred position other than that title band, the complete child layout moves down by its height so the label can remain adjacent to its owner. Parent sequence flows dock at the sub-process perimeter; child flows remain inside.
A collapsed sub-process uses normal activity dimensions in its parent. Its child plane is normalized independently and has no coordinate relationship to the collapsed parent shape.
Event sub-processes are placed after normal flow so they do not claim a normal rank or band.
Lanes are horizontal and may be nested. A node occupies its unique deepest lane. Redundant membership in an ancestor lane is valid; membership in incomparable lanes is not.
Nodes retain their semantic rows inside lanes, and lane bounds expand to contain them. Sequence flows may cross any number of sibling lanes. Lane regions are traversable; flow-node shapes in intervening lanes remain routing obstacles.
Text annotations and data references are decorations: they never influence ranks, semantic bands, or flow-node placement. Process and sub-process artifacts are placed after sequence flows; collaboration artifacts are placed after message flows. Core connections treat artifacts as transparent routing geometry.
- Artifacts with more associations are placed first, followed by larger artifacts and then declaration order.
- Candidates avoid flow-node shapes, routes already present on the plane, previously placed artifacts, boundary-handler exits, and participant headers.
- An artifact must be wholly inside or wholly outside each sub-process, lane, and participant; it may not straddle a container boundary.
- Artifacts emitted on the same BPMN plane share one collision domain, including artifacts from visible expanded child scopes.
- Associations are routed only after both endpoint bounds are available. Parent- and collaboration-scope artifacts may therefore connect to visible elements inside expanded sub-processes or participant processes.
Annotation sizes come from deterministic word wrapping over bounded candidate widths. Placement searches above, below, left, and right of the owner and slides along those sides when blocked. It prefers a short direct association, then a readable aspect ratio, fewer crossings, less diagram expansion, and proximity to the preferred side.
Annotations may sit outside their owner's container, but never across its boundary. Process annotations attached to message endpoints reserve the future vertical message-flow approach. In collaborations they prefer participant sides that do not expand participant rows. A collaboration annotation associated with a single participant instead prefers a centered position above or below it. Participant sizing ignores exterior annotations, while participant spacing still includes their footprint.
Data object and data store references retain their standard dimensions and receive candidates around every distinct owner. Their placement minimizes, in order, flow crossings, insufficient shape clearance, missing owner alignment, association bends, and owner-balanced route length. Repeated read/write associations therefore do not pull a reference toward only one owner.
Data object references remain inside their owner's lane. Their associations, and data store associations, use clear orthogonal routes with normal endpoint legs. Repeated associations receive distributed docking points rather than overlapping one another.
Groups are placed after artifacts and routing. Membership is explicit: a node
or connection belongs to a group when its categoryValueRef contains the
group's category value. Group bounds are the union of member shape bounds and
member connection waypoints, expanded by the configured group padding.
Groups are transparent to routing and hard overlap metrics. Their category value is shown as an external label above the group. Groups without visible explicit members remain semantic-only and are omitted; authored group DI is not used to infer membership.
Sequence-flow routing starts only after ordinary flow-node, lane, boundary-event, and expanded-child coordinates used by sequence flows are final. Event sub-process containers, artifacts, and groups are placed later because they do not participate in parent-scope sequence-flow routing.
Spine edges and other marked straight continuations are routed first, followed by cross-band gateway branches, other detours, and feedback edges. Cross-band gateway branches claim their constrained top or bottom channels before same-band detours choose an outer depth. Later edges treat accepted routes as allocated geometry.
Ports follow semantics:
- same-band forward flows leave right and enter left;
- cross-band gateway branches leave through the top or bottom;
- joins may enter vertically;
- boundary handlers leave through the outside-facing side;
- self-loops route locally around their source;
- feedback and shape-spanning forward edges use nested outer channels;
- U-routed edges try the opposite local side, with matching endpoint ports, before escalating when their preferred side is blocked. Nested routes retain the bottom-side channel order. For an isolated gateway default flow, both local sides are compared at the same constraint level; the shorter route wins and equal routes use the top channel as the deterministic tie-break. Local U-channels include rendered-stroke clearance around unrelated shapes.
The boundary-event placement order described above creates nested vertical exits without weaving.
The router escalates from the simplest candidate to the most general:
flowchart LR
A["Direct segment"] -->|blocked| B["Semantic bend template"]
B -->|blocked| C["Local U-bypass"]
C -->|blocked| D["Rectilinear visibility graph"]
D -->|blocked| E["Outer or perimeter route"]
E -->|none legal| F["ROUTING_FAILED"]
The rectilinear visibility graph uses x- and y-coordinates derived from endpoint ports, shape margins, and outer bounds. Dijkstra-style shortest-path search chooses a legal orthogonal path. Grid construction is capped at 4,096 candidate points. Above that bound the router skips directly to its bounded outer and perimeter fallbacks instead of materializing a potentially quadratic grid.
A segment is legal when it:
- does not enter an unrelated shape;
- does not properly cross an allocated edge;
- does not create a forbidden positive-length overlap.
Shared endpoints, endpoint touches, and intentional shared endpoint channels are not proper crossings. Channels may be shared regardless of whether connections enter or leave their common endpoint.
Every participant with a process reference contains an independently laid-out process. Its pool is sized around that process. Participants without process content are positioned and minimally sized from message-flow anchors, whether they have an empty process or no process reference. An empty process-backed pool keeps its alignment when all anchors already fit with header clearance.
For process participants without lanes, the participant header precedes the normal process-content padding without consuming it. Lane-backed participants start their lane tiles after the header and use the configured lane padding.
Vertical participant order follows message-flow relationships:
- up to eight participants use exhaustive permutation search;
- larger collaborations use deterministic greedy insertion followed by remove-and-reinsert refinement.
With one process-backed participant, connected black-box pools first prefer adjacency and then shorter, straighter message travel. With multiple process-backed participants, connected rows also prefer less vertical separation.
The largest process footprint anchors horizontal alignment. Other process layouts may translate as a unit to align endpoint centers or usable participant edge intervals. Candidate positions are scored using fully rerouted message geometry: crossings, bends, longest route, total distance, and displacement. Large collaborations prioritize bends before crossings; smaller ones use crossings first. Single-process collaborations keep their process position.
Disconnected message-flow components are translated to a common left edge. Consecutive black-box participants may share a row when they retain the normal horizontal gap; process-backed participants always occupy an exclusive row.
Message flows are generated when both endpoints resolve to visible layout geometry. An endpoint inside a collapsed sub-process resolves to its nearest visible collapsed ancestor. Opposing directions receive stable channel offsets. Adjacent pools use their shared gutter; non-adjacent pools may use an outside channel. Large collaborations also consider endpoint-aligned corridors and the sides of intervening pools before either diagram exterior.
Routes avoid process-node obstacles. Horizontal segments claim exclusive y-channels; perpendicular vertical segments may cross them without forming a junction.
Empty pool sizing and message routing form a fixed point:
flowchart LR
A["Route message flows"] --> B["Inspect participant-side docks"]
B --> C{"Every dock inside its pool?"}
C -->|yes| D["Finish"]
C -->|no| E["Expand affected empty pools"]
E --> A
Participant docks remain inside their pool while routing. Empty pools expand around uncovered docks and reroute until every dock fits.
The layout is translated so its minimum shape extents begin at the outer shape
margin. Edge waypoints move by the same offset and may occupy that routing
space. FinalizeConnections
then finalizes connection geometry. DiagramGeneration
computes external-label bounds for every independent plane before
creating any BPMN DI.
Expanded child layouts are emitted on their parent plane. Collapsed sub-process children are normalized and emitted recursively on separate planes. Diagram generation owns that traversal and returns the complete diagram collection.
Before serialization, finalization enforces these endpoint contracts:
- Endpoint segments leave and enter through an unambiguous shape side. Tangent segments are redirected, interior waypoints are removed, and a short dogleg preserves an outward approach when needed.
- Corner dockings move one routing margin inward along the intended side.
- Boundary handlers leave the event's top or bottom center through a vertical stub and approach their target through a facing side.
- Clear three-point sequence-flow elbows are centered on shape sides. Obstructed cross-band elbows try the facing gap, transposed sides, and then global routing.
Crossing-free candidates are preferred, but an unavoidable edge crossing never permits ambiguous corner docking.
Named events, gateways, data references, sequence flows, message flows, and groups receive external label bounds after connection finalization and before BPMN DI creation. Label width is estimated from wrapped text and capped at 90 px.
- Shape labels try below, above, left, and right; group labels try above first.
- Horizontal connection labels try above then below; vertical labels try right then left.
- Unique portions near a connection's center are tried before shared trunks.
- Candidates avoid shapes, connection interiors, other labels, participant headers, sub-process title areas, and container borders. Their direct visual attachment to the owner must also remain clear.
If preferred positions are occupied, a bounded expanding search chooses the nearest clear fallback without changing connection routes. Unknown visual elements never receive task-sized fallback geometry.
The layout entrypoint owns parsing and root
selection, delegating process scopes to process
and collaborations to collaboration.
It passes the complete layout tree to the deep
DiagramGeneration module,
which owns normalization, finalization, external labels, plane traversal, and
DI output behind one interface. Process and collaboration entrypoints build
their contexts and run private immutable phase lists. Nested process scopes
re-enter the same fixed lifecycle.
extractElements
→ layoutChildScopes
→ validateScope
→ analyzeSemantics
→ placeFlowNodes
→ placeExpandedChildren
→ routeSequenceFlows
→ placeEventSubProcesses
→ placeArtifacts
→ placeGroups
Each step receives and returns one context containing the scope and recursive options, extracted elements, graph and semantic state, mutable placement records, layout state, and warnings. Extraction initializes elements and placement; semantic analysis replaces graph and policy state without writing geometry. Placement writes shape bounds, routing writes edge waypoints, and nested scopes re-enter the process entrypoint through a private callback.
validateCollaboration
→ layoutParticipants
→ layoutParticipantGeometry
→ routeMessageFlows
→ placeArtifacts
The collaboration context tracks participant layouts, geometry, routing, and warnings. Participant geometry owns ordering, sizing, horizontal alignment, and row placement behind one phase. Message routing may expand resizable participant bounds until every participant-side dock fits.
Reusable context contracts live in
Types.ts. Runtime modules reference them through
type-only imports.
Process, collaboration, artifact, and connection-finalization routing reuse the shared orthogonal search where their geometric contracts match. BPMN endpoint and shared-channel semantics remain in its adapter.
Entrypoints and major domain operations read top-down as named phases or decisions. Independently meaningful classification, preparation, candidate generation, validation, scoring, fallback, and application steps are extracted.
Decomposition follows concepts rather than line counts. Cohesive graph-search, route-scoring, and candidate-construction kernels remain intact when splitting would obscure shared invariants. Generic utility modules and wrapper-only modules are avoided.
Focused specs use the implementation concept's name and protect boundaries that snapshots do not explain. Snapshot fixtures remain the integration contract for complete generated geometry.
| Concern | Main implementation |
|---|---|
| Layout engine entrypoint | layout/index.ts |
| Process pipeline and stages | process/ |
| Collaboration pipeline and geometry | collaboration/ |
| Spine, components, bands, cycles, and ranks | process/semantics/ |
| Coordinates, component packing, and boundary events | process/placement/ShapePlacement.ts |
| Lane membership, measurement, and placement | process/placement/LanePlacement.ts |
| Participant container bounds and expanded sub-processes | process/placement/ParticipantBounds.ts, process/placement/ExpandedSubProcess.ts |
| Sequence-flow routing | process/routing/ |
| Shared orthogonal search and BPMN routing adapter | routing/ |
| Artifact context and ownership | artifacts/Context.ts, artifacts/Ownership.ts |
| Artifact placement and candidate generation | artifacts/Placement.ts, artifacts/PlacementCandidates.ts |
| Artifact obstacle and association routing | artifacts/ObstacleRoutes.ts, artifacts/AssociationRouting.ts |
| Explicit group bounds | groups/LayoutGroups.ts |
| External label placement | labels/ |
| Layout state and geometry | geometry/ |
| BPMN predicates | bpmn/ |
| Final connection docking | connections/ |
| Diagram generation and DI output | DiagramGeneration.ts |
For an intentional behavior change:
- update the focused spec or a minimal
LayoutSpec.tsfixture; - inspect snapshots and corpus metrics as described in
test/README.md; - update this document when the rule or mechanism changes.
Snapshots record exact geometry; metrics expose quality trends. Wrong-way docking and non-orthogonal connections must remain zero across the fixture corpus. Review visual changes before accepting them.