Skip to main content

Smart Tools, Faster Teams: How We Fixed BPMN Diffs

At Sonalake, we’re big believers that speed comes from clarity, not chaos. The faster you can understand a change, the faster you can ship it with confidence.

We use Camunda extensively as part of our SwitchedOn Fibre product. It underpins the orchestration layer for provisioning and service activation, coordinating multiple backend systems across dozens of BPMN workflows.

Over time, those workflows have grown in number and complexity - and, like most teams using Camunda Modeler, we’ve hit the same small but persistent annoyance: Git diffs for BPMN files are a mess.

Even the tiniest layout change - nudging a task, adjusting a connector, or aligning a gateway - results in a flood of XML churn. The real logic changes get buried somewhere in the middle, making reviews unnecessarily noisy and slowing down collaboration.

We realised the problem wasn’t with Camunda or BPMN itself, but with how BPMN files are structured. They mix two things that evolve independently: the process logic and the diagram layout.

It’s a bit like putting HTML and CSS in the same file. The HTML defines what the page does - the structure and semantics - while the CSS controls how it looks. In BPMN, those two layers live together in the same XML document, so even a small visual tweak ends up cluttering the diff with layout updates.

Fortunately, Git has a built-in mechanism that lets you control how differences are calculated for specific file types. We used that to build a small Python tool, bpmn-diff-filter, that strips out layout-only elements before Git compares files.

The result? Clean, logic-only diffs that make BPMN workflows as reviewable as any other part of your codebase.

Making Git diff smarter #

Through something called a diff driver, you can tell Git to run a script that transforms a file before it’s compared.

For example, if you work with JSON or YAML configuration files, you might use a diff driver that pretty-prints or sorts keys before comparing, so that changes in formatting don’t show up as real differences.

The same idea works for BPMN. Instead of diffing the raw XML, we can run a filter that removes the layout noise and outputs only the logical process structure.

That’s what bpmn-diff-filter does. It parses the BPMN XML, strips out all the Diagram Interchange (DI) elements - things like coordinates, bounds, labels, and waypoints - and outputs what’s left. Git then runs its normal line-based diff on that cleaner version.

Once configured, it works everywhere. Whether you use git diff in a terminal or view changes in VS Code, Sourcetree, or GitHub Desktop, the result is the same - clean, logic-only diffs that actually make sense.

Before #

Without the filter, even a tiny layout change floods the diff with XML noise. Here, we’ve got a 257-line diff with 82 removed and 86 added lines - all from changing a single name attribute from yes to Yes.

 <?xml version="1.0" encoding="UTF-8"?>
-<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1nwx97f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.28.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.21.0">
+<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bioc="http://bpmn.io/schema/bpmn/biocolor/1.0" xmlns:color="http://www.omg.org/spec/BPMN/non-normative/color/1.0" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1nwx97f" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.31.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.21.0">
   <bpmn:process id="leastCostRouting" name="LCR" isExecutable="true" camunda:historyTimeToLive="7">
     <bpmn:startEvent id="StartEvent_1">
       <bpmn:outgoing>Flow_04zct7x</bpmn:outgoing>
@@ -33,7 +33,7 @@ return taskData.providerItems</camunda:script>
     <bpmn:endEvent id="Event_146x9q7">
       <bpmn:incoming>Flow_0xidytj</bpmn:incoming>
     </bpmn:endEvent>
-    <bpmn:sequenceFlow id="Flow_0xidytj" name="yes" sourceRef="Gateway_0o4m57c" targetRef="Event_146x9q7">
+    <bpmn:sequenceFlow id="Flow_0xidytj" name="Yes" sourceRef="Gateway_0o4m57c" targetRef="Event_146x9q7">
       <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression" language="groovy">return execution.getVariable('providerItems') == null || execution.getVariable('providerItems').size() == 0</bpmn:conditionExpression>
     </bpmn:sequenceFlow>
     <bpmn:sequenceFlow id="Flow_1vs2o0p" sourceRef="Activity_0huek0y" targetRef="Gateway_1dxv4t6" />
@@ -174,153 +174,157 @@ return result</camunda:script>
   </bpmn:category>
   <bpmndi:BPMNDiagram id="BPMNDiagram_1">
     <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="leastCostRouting">
+      <bpmndi:BPMNShape id="Event_1tm3dlj_di" bpmnElement="Event_1tm3dlj" bioc:stroke="#205022" bioc:fill="#c8e6c9" color:background-color="#c8e6c9" color:border-color="#205022">
+        <dc:Bounds x="1962" y="282" width="36" height="36" />
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="BPMNShape_0bdgclf" bpmnElement="Activity_1r2wavw">
+        <dc:Bounds x="1510" y="230" width="100" height="80" />
+        <bpmndi:BPMNLabel />
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_1pru9ye_di" bpmnElement="Activity_1pru9ye">
+        <dc:Bounds x="1510" y="350" width="100" height="80" />
+        <bpmndi:BPMNLabel />
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_144hmtq_di" bpmnElement="Activity_0upmuzm">
+        <dc:Bounds x="1810" y="350" width="100" height="80" />
+      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
         <dc:Bounds x="152" y="292" width="36" height="36" />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Event_1tm3dlj_di" bpmnElement="Event_1tm3dlj">
-        <dc:Bounds x="2102" y="282" width="36" height="36" />
-      </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0cxfwj7_di" bpmnElement="Activity_1a0l2te">
-        <dc:Bounds x="290" y="270" width="100" height="80" />
+        <dc:Bounds x="240" y="270" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Gateway_0o4m57c_di" bpmnElement="Gateway_0o4m57c" isMarkerVisible="true">
-        <dc:Bounds x="445" y="285" width="50" height="50" />
+        <dc:Bounds x="395" y="285" width="50" height="50" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="434" y="248" width="72" height="27" />
+          <dc:Bounds x="385" y="248" width="71" height="27" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Event_146x9q7_di" bpmnElement="Event_146x9q7">
-        <dc:Bounds x="452" y="412" width="36" height="36" />
+      <bpmndi:BPMNShape id="Event_146x9q7_di" bpmnElement="Event_146x9q7" bioc:stroke="#205022" bioc:fill="#c8e6c9" color:background-color="#c8e6c9" color:border-color="#205022">
+        <dc:Bounds x="402" y="412" width="36" height="36" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_17nu26r_di" bpmnElement="Activity_0huek0y" isExpanded="true">
-        <dc:Bounds x="770" y="180" width="700" height="330" />
+        <dc:Bounds x="700" y="200" width="650" height="270" />
+        <bpmndi:BPMNLabel />
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape id="Activity_0y1f91g_di" bpmnElement="Activity_1vqkpz7">
+        <dc:Bounds x="1070" y="360" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Event_0xi3l9a_di" bpmnElement="Event_0xi3l9a">
-        <dc:Bounds x="810.3333333333334" y="262" width="36" height="36" />
+        <dc:Bounds x="720" y="292" width="36" height="36" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0ymuumj_di" bpmnElement="Activity_1cnek1t">
-        <dc:Bounds x="890" y="240" width="100" height="80" />
+        <dc:Bounds x="800" y="270" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Gateway_0ajbcek_di" bpmnElement="Gateway_0ajbcek" isMarkerVisible="true">
-        <dc:Bounds x="1055" y="255" width="50" height="50" />
-      </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Event_1oup444_di" bpmnElement="Event_1oup444">
-        <dc:Bounds x="1352" y="262" width="36" height="36" />
+        <dc:Bounds x="965" y="285" width="50" height="50" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Activity_0kk3v2g_di" bpmnElement="Activity_0h7a0nf">
-        <dc:Bounds x="1160" y="240" width="100" height="80" />
+        <dc:Bounds x="1070" y="270" width="100" height="80" />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_0y1f91g_di" bpmnElement="Activity_1vqkpz7">
-        <dc:Bounds x="1160" y="360" width="100" height="80" />
-        <bpmndi:BPMNLabel />
+      <bpmndi:BPMNShape id="Event_1oup444_di" bpmnElement="Event_1oup444" bioc:stroke="#205022" bioc:fill="#c8e6c9" color:background-color="#c8e6c9" color:border-color="#205022">
+        <dc:Bounds x="1282" y="292" width="36" height="36" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNEdge id="Flow_0ojc89l_di" bpmnElement="Flow_0ojc89l">
-        <di:waypoint x="846" y="280" />
-        <di:waypoint x="890" y="280" />
+        <di:waypoint x="756" y="310" />
+        <di:waypoint x="800" y="310" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0dc04pz_di" bpmnElement="Flow_0dc04pz">
-        <di:waypoint x="990" y="280" />
-        <di:waypoint x="1055" y="280" />
+        <di:waypoint x="900" y="310" />
+        <di:waypoint x="965" y="310" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_17chllh_di" bpmnElement="Flow_17chllh">
-        <di:waypoint x="1105" y="280" />
-        <di:waypoint x="1160" y="280" />
+        <di:waypoint x="1015" y="310" />
+        <di:waypoint x="1070" y="310" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_11mmpi2_di" bpmnElement="Flow_11mmpi2">
-        <di:waypoint x="1080" y="305" />
-        <di:waypoint x="1080" y="400" />
-        <di:waypoint x="1160" y="400" />
+        <di:waypoint x="990" y="335" />
+        <di:waypoint x="990" y="400" />
+        <di:waypoint x="1070" y="400" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1rmy1j7_di" bpmnElement="Flow_1rmy1j7">
-        <di:waypoint x="1260" y="400" />
-        <di:waypoint x="1310" y="400" />
-        <di:waypoint x="1310" y="280" />
-        <di:waypoint x="1352" y="280" />
+        <di:waypoint x="1170" y="400" />
+        <di:waypoint x="1220" y="400" />
+        <di:waypoint x="1220" y="310" />
+        <di:waypoint x="1282" y="310" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1n553wz_di" bpmnElement="Flow_1n553wz">
-        <di:waypoint x="1260" y="280" />
-        <di:waypoint x="1352" y="280" />
+        <di:waypoint x="1170" y="310" />
+        <di:waypoint x="1282" y="310" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNShape id="Activity_0311bi5_di" bpmnElement="Activity_0311bi5">
-        <dc:Bounds x="590" y="270" width="100" height="80" />
+        <dc:Bounds x="500" y="270" width="100" height="80" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="Gateway_1dxv4t6_di" bpmnElement="Gateway_1dxv4t6" isMarkerVisible="true">
-        <dc:Bounds x="1505" y="275" width="50" height="50" />
-      </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="BPMNShape_0bdgclf" bpmnElement="Activity_1r2wavw">
-        <dc:Bounds x="1600" y="260" width="100" height="80" />
-        <bpmndi:BPMNLabel />
-      </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_1pru9ye_di" bpmnElement="Activity_1pru9ye">
-        <dc:Bounds x="1730" y="260" width="100" height="80" />
-        <bpmndi:BPMNLabel />
-      </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Activity_144hmtq_di" bpmnElement="Activity_0upmuzm">
-        <dc:Bounds x="1900" y="260" width="100" height="80" />
+        <dc:Bounds x="1415" y="275" width="50" height="50" />
       </bpmndi:BPMNShape>
       <bpmndi:BPMNShape id="TextAnnotation_0j3lqfx_di" bpmnElement="TextAnnotation_0j3lqfx">
-        <dc:Bounds x="980" y="80" width="220" height="50" />
+        <dc:Bounds x="890" y="80" width="220" height="50" />
         <bpmndi:BPMNLabel />
       </bpmndi:BPMNShape>
-      <bpmndi:BPMNShape id="Group_1p0x73q_di" bpmnElement="Group_1p0x73q">
-        <dc:Bounds x="1570" y="180" width="300" height="210" />
-        <bpmndi:BPMNLabel>
-          <dc:Bounds x="1679" y="187" width="83" height="40" />
-        </bpmndi:BPMNLabel>
-      </bpmndi:BPMNShape>
       <bpmndi:BPMNEdge id="Flow_04zct7x_di" bpmnElement="Flow_04zct7x">
         <di:waypoint x="188" y="310" />
-        <di:waypoint x="290" y="310" />
+        <di:waypoint x="240" y="310" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_191r5nc_di" bpmnElement="Flow_191r5nc">
-        <di:waypoint x="390" y="310" />
-        <di:waypoint x="445" y="310" />
+        <di:waypoint x="340" y="310" />
+        <di:waypoint x="395" y="310" />
       </bpmndi:BPMNEdge>
-      <bpmndi:BPMNEdge id="Flow_0n6yrcm_di" bpmnElement="Flow_0n6yrcm">
-        <di:waypoint x="495" y="310" />
-        <di:waypoint x="590" y="310" />
+      <bpmndi:BPMNEdge id="Flow_0xidytj_di" bpmnElement="Flow_0xidytj">
+        <di:waypoint x="420" y="335" />
+        <di:waypoint x="420" y="412" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="541" y="292" width="15" height="14" />
+          <dc:Bounds x="426" y="369" width="18" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNEdge>
-      <bpmndi:BPMNEdge id="Flow_0xidytj_di" bpmnElement="Flow_0xidytj">
-        <di:waypoint x="470" y="335" />
-        <di:waypoint x="470" y="412" />
+      <bpmndi:BPMNShape id="Group_1p0x73q_di" bpmnElement="Group_1p0x73q">
+        <dc:Bounds x="1480" y="180" width="300" height="290" />
+        <bpmndi:BPMNLabel>
+          <dc:Bounds x="1589" y="187" width="83" height="40" />
+        </bpmndi:BPMNLabel>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge id="Flow_0n6yrcm_di" bpmnElement="Flow_0n6yrcm">
+        <di:waypoint x="445" y="310" />
+        <di:waypoint x="500" y="310" />
         <bpmndi:BPMNLabel>
-          <dc:Bounds x="476" y="369" width="18" height="14" />
+          <dc:Bounds x="468" y="292" width="15" height="14" />
         </bpmndi:BPMNLabel>
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1vs2o0p_di" bpmnElement="Flow_1vs2o0p">
-        <di:waypoint x="1470" y="300" />
-        <di:waypoint x="1505" y="300" />
+        <di:waypoint x="1350" y="300" />
+        <di:waypoint x="1415" y="300" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_18napjn_di" bpmnElement="Flow_18napjn">
-        <di:waypoint x="690" y="310" />
-        <di:waypoint x="770" y="310" />
+        <di:waypoint x="600" y="310" />
+        <di:waypoint x="700" y="310" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_0237tum_di" bpmnElement="Flow_0237tum">
-        <di:waypoint x="2000" y="300" />
-        <di:waypoint x="2102" y="300" />
+        <di:waypoint x="1910" y="390" />
+        <di:waypoint x="1936" y="390" />
+        <di:waypoint x="1936" y="300" />
+        <di:waypoint x="1962" y="300" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_06dob4v_di" bpmnElement="Flow_06dob4v">
-        <di:waypoint x="1555" y="300" />
-        <di:waypoint x="1600" y="300" />
+        <di:waypoint x="1465" y="300" />
+        <di:waypoint x="1488" y="300" />
+        <di:waypoint x="1488" y="270" />
+        <di:waypoint x="1510" y="270" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_1qvsubq_di" bpmnElement="Flow_1qvsubq">
-        <di:waypoint x="1700" y="300" />
-        <di:waypoint x="1730" y="300" />
+        <di:waypoint x="1560" y="310" />
+        <di:waypoint x="1560" y="350" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Flow_082mnmu_di" bpmnElement="Flow_082mnmu">
-        <di:waypoint x="1830" y="300" />
-        <di:waypoint x="1900" y="300" />
+        <di:waypoint x="1610" y="390" />
+        <di:waypoint x="1810" y="390" />
       </bpmndi:BPMNEdge>
       <bpmndi:BPMNEdge id="Association_1872ydb_di" bpmnElement="Association_1872ydb">
-        <di:waypoint x="1056" y="180" />
-        <di:waypoint x="1041" y="130" />
+        <di:waypoint x="973" y="200" />
+        <di:waypoint x="952" y="130" />
       </bpmndi:BPMNEdge>
     </bpmndi:BPMNPlane>
   </bpmndi:BPMNDiagram>

After #

With the filter, the same change becomes a clean 9-line diff showing just the logic-only difference. Win.

@@ -33,7 +33,7 @@ return taskData.providerItems</camunda:script>
     <bpmn:endEvent id="Event_146x9q7">
       <bpmn:incoming>Flow_0xidytj</bpmn:incoming>
     </bpmn:endEvent>
-    <bpmn:sequenceFlow id="Flow_0xidytj" name="yes" sourceRef="Gateway_0o4m57c" targetRef="Event_146x9q7">
+    <bpmn:sequenceFlow id="Flow_0xidytj" name="Yes" sourceRef="Gateway_0o4m57c" targetRef="Event_146x9q7">
       <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression" language="groovy">return execution.getVariable('providerItems') == null || execution.getVariable('providerItems').size() == 0</bpmn:conditionExpression>
     </bpmn:sequenceFlow>
     <bpmn:sequenceFlow id="Flow_1vs2o0p" sourceRef="Activity_0huek0y" targetRef="Gateway_1dxv4t6" />

How to enable it #

Setting it up takes about a minute.

First, grab the Python script from GitHub and drop it somewhere in your project (we tend to keep it under scripts/bpmn-diff-filter.py).

Make it executable so Git can run it:

chmod +x scripts/bpmn-diff-filter.py

Next, tell Git how to use it. Git stores this setting locally, so each developer needs to run this once.

git config diff.bpmn.textconv "python3 scripts/bpmn-diff-filter.py"

That command registers the script as a custom diff driver called bpmn.

Now you just need to tell Git which files it applies to. Create or update a .gitattributes file in your repo:

*.bpmn diff=bpmn

Commit that file so everyone on the team gets the same diff behaviour:

git add .gitattributes
git commit -m "Add BPMN diff configuration"

That’s it. From now on, whenever you run git diff or open a pull request, Git will automatically invoke bpmn-diff-filter for .bpmn files.

The noise disappears and you’ll see only the logical changes that matter.

Small change, big difference #

Before, reviewing a simple process tweak meant scrolling through screens of coordinate noise to find the one line that actually changed logic. Now, the diffs are clear and focused - you can see the intent immediately.

Like most good engineering improvements, it’s not about cleverness - it’s about friction.

Remove a little friction - in this case, the visual clutter in BPMN diffs - and everything else feels smoother: reviews are quicker, conversations stay on the real design decisions, and nobody dreads touching process models anymore.

It’s a small change that’s made a real difference to how we work with BPMN.

You can find the filter and setup instructions on GitHub.