1 — Introduction: From Binary Logic to Weighted Decisions
Most of us start our n8n journey with the humble IF Node — the “gateway drug” of automation. But as business logic grows, that simple binary branch mutates into sprawling IF-chain spaghetti logic.
In high-stakes workflows, IF-chains fail in three predictable ways:
- Shadowed Rules — a “High Priority” check at the start can accidentally swallow a “Billing Error” ticket meant for another team.
- The Ghost Item — unconnected False paths can let items vanish silently.
- Zero Explainability — when a ticket goes to the wrong team, tracing 10–15 IF nodes tells you nothing about why.
For more details, see the official IF Node Documentation
The Decision Dispatcher workflow solves this by transforming binary Yes/No logic into a weighted decision pipeline. Instead of just picking a top-scoring rule, we calculate decisionConfidence, record decisionTrace, and route low-confidence items for human review.
High-confidence items proceed automatically, while uncertain cases trigger sub-workflows like Slack notifications or email requests for manual approval. This ensures no item is lost, and every decision is fully auditable.
The Decision Dispatcher pattern consists of four stages:
Stage 1: Detect Candidate Rules
For each item, gather all applicable rules from various services, along with the corresponding team and base score.
Stage 2: Normalize & Aggregate
Convert all scores to a common scale (0–100) and combine votes for each candidate. This allows heterogeneous services to feed into a unified decision framework.
Stage 3: Decide Winner & Compute Confidence
Sort candidates by score, select the winner, and compute decisionConfidence:
- 1.0 → no disagreement among rules (unanimous top choice).
- <1.0 → internal conflict exists; closer scores reduce confidence.
Stage 4: Route by Confidence & Dispatch
Use an IF/Switch node to compare decisionConfidence against a threshold. High-confidence items follow the automated path; low-confidence items trigger human-in-the-loop workflows.
3 — Why Split Nodes: The Modular Approach
You’ll notice the workflow uses multiple Code Nodes instead of one giant script. This is intentional:
- Atomic Observability — inspect each node’s output to see candidates grow, scores normalize, and winners emerge. Debugging becomes transparent and fast.
- Plugin-and-Play — swap a Regex check for a Python ML classifier or an HTTP API call without touching other nodes.
- Workflow as Documentation — each node tells part of the story: “Detect → Normalize → Aggregate → Route → Dispatch.” This makes onboarding teammates easier.
4 — Workflow Implementation: Paste-Ready JavaScript Nodes
Tip: In “Run Once For Each Item” mode, n8n handles the iteration. Simply return the updated
item.json. Beginners often try their own loops, but the platform does it for you.
For more details, see the official Code Node Documentation
4.1 Data Generator
return [
{ json: { id: 1, text: "Payment failed", priority: "high", tags: ["payment"] } },
{ json: { id: 2, text: "Login blocked", priority: "low", tags: ["auth"] } },
{ json: { id: 3, text: "Payment + Auth issue", priority: "high", tags: ["payment","auth"] } },
{ json: { id: 4, text: "General question", priority: "low", tags: [] } }
];
For more details see, see the official Code Node Documentation
4.2 Simulate Services
const data = item.json;
const svcResponses = [
{
name: 'svcA',
verdict: (data.tags.includes('payment') || data.priority === 'high') ? 'VIP' : null,
score: data.tags.includes('payment') ? 0.9 : 0.1,
scale: '0-1'
},
{
name: 'svcB',
verdict: data.tags.includes('auth') ? 'Auth' : (data.tags.includes('payment') ? 'Billing' : null),
score: data.tags.includes('auth') ? 60 : (data.tags.includes('payment') ? 40 : 10),
scale: '0-100'
},
{
name: 'svcC',
verdict: data.priority === 'high' ? 'VIP' : null,
score: data.priority === 'high' ? 4 : 1,
scale: '0-5'
}
];
item.json.services = svcResponses;
return item;
Tip: heterogeneous services feed a unified candidate scoring system.
4.3 Detect & Normalize Candidates
const data = item.json;
const services = Array.isArray(data.services) ? data.services : [];
function normalize(value, scale) {
if (scale === '0-1') return Math.round(value * 100);
if (scale === '0-5') return Math.round((value / 5) * 100);
if (scale === '0-100') return Math.round(value);
return Math.round(value);
}
const map = {};
services.forEach(s => {
if (!s.verdict) return;
const score = normalize(s.score, s.scale);
const rule = s.verdict;
const team = rule === 'VIP' ? 'senior-support' :
(rule === 'Auth' ? 'security-team' : 'billing-team');
if (!map[rule]) map[rule] = { rule, team, scores: [] };
map[rule].scores.push({ svc: s.name, raw: s.score, norm: score });
});
const candidates = Object.values(map).map(r => {
const total = r.scores.reduce((s,v) => s+v.norm,0);
const avg = Math.round(total / r.scores.length);
return { rule: r.rule, team: r.team, avgScore: avg, votes: r.scores.length, details: r.scores };
});
data.candidates = candidates;
item.json = data;
return item;
4.4 Aggregate & Select Winner
const data = item.json;
const candidates = Array.isArray(data.candidates) ? data.candidates : [];
if (candidates.length === 0) {
data.team = 'default';
data.decisionRule = null;
data.decisionTrace = ['no rules matched'];
data.decisionConfidence = 0;
item.json = data;
return item;
}
candidates.sort((a,b) => b.avgScore - a.avgScore);
const precedence = ['VIP','Auth','Billing'];
if (candidates.length > 1 && candidates[0].avgScore === candidates[1].avgScore) {
candidates.sort((a,b) => b.avgScore - a.avgScore || precedence.indexOf(a.rule) - precedence.indexOf(b.rule));
}
const winner = candidates[0];
const totalScore = candidates.reduce((s,c) => s + c.avgScore,0);
data.team = winner.team || 'default';
data.decisionRule = winner.rule || null;
data.decisionTrace = candidates.map(c => `${c.rule}:${c.avgScore}(${c.votes})`);
data.decisionConfidence = totalScore ? Number((winner.avgScore/totalScore).toFixed(3)) : 0;
item.json = data;
return item;
Pro-Tip: Confidence is conflict-aware: a 1.0 means total agreement among rules, not “absolute truth.”
4.5 Set Route Type
const data = item.json;
const THRESHOLD = 0.9;
data.routeType = data.decisionConfidence >= THRESHOLD ? 'auto' : 'human';
item.json = data;
return item;
4.6 Decision Dispatcher Sub-Workflow
- Receives
id,team,decisionRule,decisionTrace,decisionConfidence,routeType. - Performs side-effects: Slack/Discord notifications, emails, Google Sheets logging, or queueing (Redis, RabbitMQ, SQS).
- Keeps the decision engine modular and auditable.
For more details, see the official Execute Sub-workflow Documentation
see also Execute Sub-workflow Trigger Documentation
and Splitting with conditionals Documentation
5 — Final JSON Output (After Set Route Type)
[
{
"id": 1,
"text": "Payment failed",
"priority": "high",
"tags": ["payment"],
"candidates": [
{ "rule": "VIP", "team": "senior-support", "avgScore": 85, "votes": 2 },
{ "rule": "Billing", "team": "billing-team", "avgScore": 40, "votes": 1 }
],
"team": "senior-support",
"decisionRule": "VIP",
"decisionTrace": ["VIP:85(2)", "Billing:40(1)"],
"decisionConfidence": 0.68,
"routeType": "human"
},
{
"id": 2,
"text": "Login blocked",
"priority": "low",
"tags": ["auth"],
"candidates": [
{ "rule": "Auth", "team": "security-team", "avgScore": 60, "votes": 1 }
],
"team": "security-team",
"decisionRule": "Auth",
"decisionTrace": ["Auth:60(1)"],
"decisionConfidence": 1.0,
"routeType": "auto"
}
]
6 — Conclusion
The Decision Dispatcher pattern elevates your automation from fragile IF-chains to a robust, explainable, confidence-aware decision engine:
- Normalize multiple heterogeneous signals into a unified candidate score.
- Aggregate & compute confidence to quantify decision reliability.
- Route intelligently: auto-run high-confidence items, human-review low-confidence items.
- Modular & auditable: each node represents a clear step in the pipeline; the dispatcher sub-workflow handles side-effects cleanly.
This approach balances speed, safety, and transparency, enabling enterprise-grade automation that grows with your confidence in the machine’s decisions.
If you’d like to explore the foundational version of this pattern, I cover the earlier architecture in my related article, “Decision Patterns in n8n.” It walks through the base scoring model before introducing confidence-based routing.



