Conditional Split: making routing logic native to the pipeline
The problem was never splitting data. It was expressing the decision. This is how a structural branching step became a conditional routing model, explored across three interaction directions, then designed, implemented, and shipped.
Overview
The Data Integrity Suite already had a Split step. Users could create multiple branches, but a branch itself didn't define what belonged there. To actually route records, they had to keep building logic downstream of it.
The result was technically flexible but conceptually fragmented. Looking at a branch told you nothing about what it was for, why a record went there, or what happened to everything else. The opportunity was to move that decision to where it naturally belongs: the point where the pipeline branches.
Before
-
Split asked
"Where can the pipeline go?"
A structural question: branches existed, but nothing tied a branch to a reason. -
Conditional Split asks
"What determines where a record goes?"
A routing question: the branch itself now carries the logic.
For example
- Country = UK→UK processing
- Country = US→US processing
- Default→Everything else
The capability wasn't fundamentally new. The relationship between the rule and the branch was.
Exploration
Rather than assume the answer was another configuration panel, three different mental models were tested before committing to an interaction direction.
01. Branch first
Each branch owned its condition directly, extending the existing Split model with minimal disruption. The open question: would the condition feel like part of the branch, or just another setting attached to it?
02. Rule first
An explicit IF → THEN → ELSE IF → ELSE routing model. The logic was clear, but the experience started behaving like a rules engine, introducing a second mental model into a product where users already think in nodes, connections, and branches.
03. Rules and outputs as separate concepts
Routing rules were separated entirely from output branches. Conceptually clean, but in practice it pushed the decision away from the canvas, the place users were already using to understand their pipeline.
The Direction
The familiar pipeline model stayed intact. The branch became responsible for its own routing logic.
Branch = destination. Condition = reason.
The canvas shows the paths. The drawer handles detailed configuration. The branch itself can surface a concise condition summary, keeping the new capability powerful without making the pipeline harder to read.
The pipeline should explain itself
A configuration drawer can tell you everything. It just can't tell you everything at a glance. So the condition surfaces on the branch itself. With the drawer closed, the pipeline still communicates the relationship that matters: records matching this condition go down this path. The canvas became part of the explanation, not just the place where nodes are arranged.
The drawer itself stayed close to how users already build conditions elsewhere in the product, so the new capability didn't feel like a separate tool bolted on.
Default Path
A second product decision: what happens to records that match none of the rules? Always exposing an unmatched path would add a branch to every Conditional Split, even when the user didn't need one.
Scenario 1: an empty branch is the default
If every branch above it has a condition, a branch is doing real routing work. If a branch has no condition, it isn't filtering anything, so it naturally catches whatever didn't match elsewhere. No extra setup, no separate concept to learn: an empty branch simply behaves as the default.
Scenario 2: an explicit default, added on purpose
Sometimes every branch genuinely needs a condition, with no branch left empty to absorb the rest. For that case, Use default path for unmatched records is available as a checkbox, and it only becomes enabled once every branch above it already has a condition of its own. Checking it adds a distinct Default branch to the canvas.
Functionally, both scenarios resolve to the exact same outcome: unmatched records always land somewhere predictable. The difference is only in how that destination came to exist. Default is either implicit (a branch left empty) or explicit (a branch added on purpose), never both, and never neither.
Implementation
This wasn't a greenfield feature. The canvas, node behaviour, configuration drawer, connectors, and design system already existed. The challenge was introducing a fundamentally different execution pattern without making the rest of the product feel different, changing existing patterns only where the new capability required it.
Reusing what already existed on the backend
The Default branch didn't need a new backend capability to exist. The pipeline engine already had a fallback mechanism, an ELSE branch, built for an earlier routing feature. Rather than asking the backend team to build something new for this UI, the Default path was implemented by repurposing that existing logic: whether a branch was left empty or added explicitly through the checkbox, both simply resolved to the same underlying ELSE behaviour under the hood. The interface changed; the backend contract didn't.
Taking the design into the codebase
After PM approval, the experience was implemented directly in the existing React/TypeScript codebase, using VS Code Copilot and Claude Code. This is where the design met reality. A seemingly isolated change could affect unrelated behaviour, requiring real precision about the scope of each change.
Design
Define the interaction and the condition-on-branch model.
Implement
Build it directly in the existing codebase, not a disconnected prototype.
Inspect
Check how the change actually behaves against real component and canvas state.
Refine
Adjust based on what the real product revealed. The product itself became part of the prototyping environment.
Results
Validation happened in the real, implemented UI rather than treating the approved mockup as the finish line. It meant checking whether the model held together in practice:
- Can a user create the branches?
- Can they define conditions without friction?
- Is the routing logic still understandable once the drawer closes?
- Does Default behave exactly as intended?
- Does the new step still feel native to the pipeline?
The feature shipped.
What changed
Inferred routing
Create branches → add downstream filters → infer routing logic from what's connected where.
Explicit routing
Define routing conditions → create clear paths → process each path independently.
The result is a pipeline that can represent not just what happens to data, but why data takes a particular path.
I didn't need to redesign the pipeline. I needed to make the pipeline's branching decision explicit.