Decision Patterns in n8n – The IF Chain Anti-Pattern and Its Fix

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:

  1. Detect Candidate Rules – for each ticket, collect all matching rules.

  2. Score & Sort – assign a score to each rule and pick the highest.

  3. Decision Trace – record all candidate rules for debugging.

  4. 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: decisionTrace allows 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.

3 Likes

If you’re ready to go beyond simple rule chains and build confidence-aware, modular routing logic, continue with my next article:
Beyond the IF-Chain: Decision Dispatcher, Advanced Rule Scoring, and Decision Confidence in n8n.
It expands this pattern into a scalable scoring pipeline with explainable decision confidence and modular dispatching.