Skip to content

@miragon/rules/flow-target-alignment ​

Reports an outgoing sequence flow whose target sits at a different height than its source, so the main path slopes up or down instead of reading as a straight horizontal line. The exception is a flow touching a gateway or a boundary event, since those legitimately branch up and down.

Examples ​

An order-approval process: start event, a review task, a split/merge gateway pair with two branch tasks, an expanded "Archive order" sub-process and an end event. Both sub-processes below are the same tall box; the only difference is where the inner reading line sits:

  • 👎 Invalid: the inner "Store record" row (and its startEvent_archiveStarted) is drawn well below the main row, so flow_archived has to step down to reach it. The reading line is off the neighbours' row, so the flow is reported, even though the box centre happens to sit near the row.
  • 👍 Valid: the inner start event sits on the main row (y=200), so flow_archived reads dead straight and nothing is reported, regardless of how tall the box is. The gateway branches (up to approve, down to reject) are never judged.
flow-target-alignment/invalid.bpmn

Invalid model: the "Archive order" sub-process's inner start event sits below the main row, so the flow into it steps down and is reported

Valid model: the "Archive order" sub-process's inner start event sits on the main row, so the main path reads as one straight horizontal line

1 finding
  • flow_archived Sequence flow connects <subProcess_archiveOrder> to <endEvent_orderHandled> at a different height; an outgoing flow's target should sit at the same height as its source

Invalid: the Archive sub-process's inner reading line is below the row, so the flow steps down and is reported

Valid: the Archive sub-process's inner start event is on the row, so the main path reads straight

xml
<!-- 👎 inner start event centre y=260, off the row, so the flow into the sub-process is reported -->
<bpmndi:BPMNShape bpmnElement="subProcess_archiveOrder" isExpanded="true">
  <dc:Bounds x="760" y="90" width="430" height="240" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="startEvent_archiveStarted">
  <dc:Bounds x="790" y="242" width="36" height="36" />
</bpmndi:BPMNShape>

<!-- 👍 same tall box, inner start event centre y=200 on the row: the flow reads straight and passes -->
<bpmndi:BPMNShape bpmnElement="subProcess_archiveOrder" isExpanded="true">
  <dc:Bounds x="790" y="120" width="360" height="210" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="startEvent_archiveStarted">
  <dc:Bounds x="820" y="182" width="36" height="36" />
</bpmndi:BPMNShape>

Why ​

A BPMN diagram is read far more often than its XML, by modelers, reviewers and business stakeholders alike, and they follow it fastest when the main path is one straight horizontal line: each step drawn on the same row as the one before it, the eye running from start to end without a jump. When a flow's target is drawn higher or lower than its source, that line bends into a diagonal or a zig-zag; the model still means the same thing, but it no longer reads as a single main path.

This is deliberately a separate concern from flow-connection-side: that rule checks the side a flow docks onto (out right, in left, gateway tips). A model can dock every flow on the correct side and still slope, because the target is simply drawn off the row. This rule catches exactly that.

Why this matters for agentic BPMN ​

Agents and auto-layout write DI coordinates directly. They connect the right elements and often dock on the right side, but place each shape wherever the maths lands: one task a hundred pixels lower than the last. The XML review looks clean and even flow-connection-side passes; only the drawn diagram shows the main path staircasing down the canvas.

Typical AI artifact without this rule: a straight sequence of tasks and events where each one is nudged a little lower than the previous, so the "main flow" descends diagonally across the diagram.

What this rule guarantees: every non-branching sequence flow keeps its target on the same row as its source, so the main path renders as one straight horizontal line.

Scope ​

Only the DI coordinates decide: for each sequence flow, the vertical centre of the source shape is compared against the vertical centre of the target shape. They must match within a small tolerance (10px) to count as the same row.

Expanded sub-processes are measured against their inner reading line, not the box centre. A sub-process's height is set by its contents, so its box centre lands in arbitrary empty space rather than where the flow docks (the inner main row, near the top). Instead the rule uses the vertical centre of the sub-process's single inner bpmn:StartEvent, the height the flow actually reads from. A collapsed sub-process is a task-sized box, so its box centre is the reading line and the default applies.

Comparison is scoped per BPMNPlane. Left alone:

  • Flows touching an exempt element type: by default gateways (a gateway is where a process branches, so its paths are meant to leave and arrive above or below the row) and boundary events (they sit on their host's border and typically drop to a handler placed below). The list is configurable, see Configuration.
  • Flows to/from an expanded sub-process with no single inner start event. None, or more than one, is a modelling smell owned by other rules (bpmnlint:recommended's start-event-required / sub-process-blank-start-event), so there is no reading line to measure and this rule stays quiet rather than double-report.
  • Flows with no DI for one of their ends: nothing to compare, so never guessed.

Configuration ​

json
{
  "rules": {
    "@miragon/rules/flow-target-alignment": [
      "error",
      {
        "exemptTypes": [
          "bpmn:Gateway",
          "bpmn:BoundaryEvent",
          "bpmn:SubProcess"
        ]
      }
    ]
  }
}
  • exemptTypes: the element types whose flows are never judged; matched inheritance-aware, so a type covers its subtypes. Given a value it replaces the default ["bpmn:Gateway", "bpmn:BoundaryEvent"] entirely, so keep those unless you want branch and boundary flows reported.

Adding bpmn:SubProcess exempts sub-processes entirely (inheritance-aware, so bpmn:Transaction and bpmn:AdHocSubProcess too). You rarely need it: an expanded sub-process is already measured against its inner start-event row, not the box centre (see Scope), so a horizontal flow into a tall box is not reported.

Further reading ​