1. Introduction
In n8n (and visual programming in general), workflows often rely on IF nodes to route data. While IF chains are deterministic—they will always follow the same path given the same input—they can lead to brittle, inflexible logic when multiple conditions overlap. This often results in lost data or “shadowed” conditions.
In this article, we’ll demonstrate:
-
An anti-pattern that drops tickets silently.
-
A recommended decision-table approach with scoring.
-
How to trace decisions for debugging.
2. Anti-Pattern: IF Chains
2.1 Workflow Overview
The typical anti-pattern uses a series of IF nodes like this:
[
{ id: 1, priority: "high", tags: ["payment"] },
{ id: 2, priority: "low", tags: ["auth"] },
{ id: 3, priority: "high", tags: ["payment", "auth"] },
{ id: 4, priority: "low", tags: [] }
]
2.3 IF Node Logic
-
IF Priority High → Team A
-
IF Payment → Team B
-
IF Auth → Team C
Problem: Each IF node only processes its own True path. Tickets that go down False paths may be lost silently.
[
{ "id": 2, "priority": "low", "tags": ["auth"] },
{ "id": 4, "priority": "low", "tags": [] }
]
Explanation:
IDs 2 and 4 are lost because they follow the False path of the IF Payment node, and no catch-all node is connected.
ID 3 is shadowed by the first IF node (Priority High) and never triggers Auth logic.
2.5 Problems With IF Chains
| Problem | Effect |
|---|---|
| Shadowed logic | Some tickets never trigger all applicable rules |
| Lost tickets | Data silently drops if no False-path handling |
| Hard to scale | Adding new rules requires long, complex IF chains |
3. Recommended Pattern: Decision Table With Scoring
Instead of multiple IF nodes, we use:
-
Detect Candidate Rules – for each ticket, collect all matching rules.
-
Score & Sort – assign a score to each rule and pick the highest.
-
Decision Trace – record all candidate rules for debugging.
-
Switch Node Routing – route based on the selected team.
3.1 Detect Candidate Rules Node
// Clone the item
const data = item.json;
// Create candidates container
data.candidates = [];
// VIP rule
if (data.priority === "high" && data.tags.includes("payment")) {
data.candidates.push({ rule: "VIP", team: "senior-support", score: 100 });
}
// Auth rule
if (data.tags.includes("auth")) {
data.candidates.push({ rule: "Auth", team: "security-team", score: 60 });
}
// Billing rule
if (data.tags.includes("payment")) {
data.candidates.push({ rule: "Billing", team: "billing-team", score: 40 });
}
item.json = data;
return item;
3.2 Select Winner Node
const data = item.json;
if (!data.candidates || !data.candidates.length) {
data.team = "default";
data.decisionRule = null;
data.decisionTrace = ["no rules matched"];
} else {
data.candidates.sort((a, b) => b.score - a.score);
data.team = data.candidates[0].team;
data.decisionRule = data.candidates[0].rule;
data.decisionTrace = data.candidates.map(c => `${c.rule}:${c.score}`);
}
item.json = data;
return item;
3.3 Switch Node Routing
| Team | Discord Node |
|---|---|
| senior-support | Send a message to Team A |
| security-team | Send a message to Team B |
| billing-team | Send a message to Team C |
| default | Default Handling |
3.4 Recommended Pattern Output
[
{
"id": 1,
"priority": "high",
"tags": ["payment"],
"candidates": [
{ "rule": "VIP", "team": "senior-support", "score": 100 },
{ "rule": "Billing", "team": "billing-team", "score": 40 }
],
"team": "senior-support",
"decisionRule": "VIP",
"decisionTrace": ["VIP:100", "Billing:40"]
},
{
"id": 2,
"priority": "low",
"tags": ["auth"],
"candidates": [{ "rule": "Auth", "team": "security-team", "score": 60 }],
"team": "security-team",
"decisionRule": "Auth",
"decisionTrace": ["Auth:60"]
},
{
"id": 3,
"priority": "high",
"tags": ["payment", "auth"],
"candidates": [
{ "rule": "VIP", "team": "senior-support", "score": 100 },
{ "rule": "Auth", "team": "security-team", "score": 60 },
{ "rule": "Billing", "team": "billing-team", "score": 40 }
],
"team": "senior-support",
"decisionRule": "VIP",
"decisionTrace": ["VIP:100", "Auth:60", "Billing:40"]
},
{
"id": 4,
"priority": "low",
"tags": [],
"candidates": [],
"team": "default",
"decisionRule": null,
"decisionTrace": ["no rules matched"]
}
]
4. Benefits of the Decision Table Approach
-
No shadowed logic: Every rule is evaluated.
-
All tickets processed: None are lost.
-
Traceable decisions:
decisionTraceallows quick debugging. -
Easily extendable: Add new rules by changing the candidate detection code, without rewriting IF chains.
5. Conclusion
Using a decision-table scoring approach in n8n transforms brittle IF-chain workflows into robust, maintainable, and transparent logic flows. This pattern is essential for real-world automation scenarios where multiple conditions may overlap and tickets should never be silently lost.


