How to Route Support Tickets in n8n With Confidence Thresholds
Route support tickets in n8n with confidence thresholds: a build-along with the Saina Helm node or plain HTTP, a review queue for fallbacks, and testing.
This is a build-along. By the end you'll have a workflow that routes support tickets in n8n automatically: it reads each ticket, asks a decision model (a small model that scores the options you give it and returns probabilities, instead of writing text) which team should handle it, and sends anything it isn't sure about to a review queue instead of guessing. Two paths are shown: the Saina Helm community node (self-hosted n8n only) and a plain HTTP Request version that works on n8n Cloud.
The routing quality comes from a confidence threshold (how sure the model must be before it acts), a margin (how far the winner must beat the runner-up) and a fallback branch that a human actually watches.
The n8n workflow for routing support tickets
Trigger → Decision step → Switch on the route
↘ Fallback → Review queue (Slack/email/sheet)
You don't have to build it from scratch. Both paths below have a ready-made template in the run-saina/n8n-nodes repository under workflows/: support-routing.json for the community node, and http-request-portable.json for plain HTTP. Import one, then adapt it as you read.
Path A: the Saina Helm node (self-hosted n8n)
Install the community node @run-saina/n8n-nodes-saina. It's a community package that has not been verified by n8n, so it runs on self-hosted n8n only, not on n8n Cloud:
cd ~/.n8n/nodes && npm install @run-saina/n8n-nodes-saina
Then restart n8n. The node wraps Helm's /v1/ask and, in decision mode, gives you two outputs:
- Selected: the answer was accepted (the top option cleared your threshold and margin).
- Fallback: the answer was rejected (below threshold, below the minimum margin, or a tie).
Inference errors (server down, timeout) stop the workflow by default. They reach Fallback only if you set the node's error handling to use the Fallback output, which the support-routing.json template does.
1. Trigger. Whatever starts your flow: email, form, webhook, a new row. The template starts with a manual trigger and an Example input node; replace both with your real trigger and map its fields.
2. Saina Helm node. Configure credentials (your server URL and API key), then fill in three fields:
- Context: the ticket text (subject + body + any metadata worth reading), e.g.
{{ $json.body }}. - Question:
Which team should handle this request? - Answers:
Billing, Technical support, Other(comma-separated or one per line).
Everything else lives under Advanced:
- Answer Descriptions: one plain-language description per answer, e.g. Billing = "Charges, invoices, refunds, subscription changes"; Technical support = "Outages, errors, bugs, account access"; Other = "Anything that does not fit the teams above". Descriptions do most of the disambiguation work.
- Confidence Threshold: default 0.8.
- Minimum Margin: default 0. The template sets 0.05 (a configuration example, not a validated setting; more on margins below).
- Mode: Decision (the default).
- On Inference Error: set it to Use Fallback Output if you want failed calls in the review queue too.
- Result Key: the name the answer is stored under. The template uses
route; the default isanswer.
3. Switch on the route. Read the answer from {{ $json.saina.answers.route.selection }} (with the default Result Key it's saina.answers.answer.selection) and route to your team queues. The template does it in one Switch expression:
{{ ["Billing", "Technical support", "Other"].indexOf($json.saina.answers.route.selection) }}
4. The Fallback output is the feature. Wire it to wherever a human looks: a Slack channel, a Google Sheet, an email digest. Each rejected decision carries the probabilities and a reason code, so the reviewer can see why it was unsure. Error items are different: their saina.reason is inference_error and they carry no probabilities, so give them their own message ("routing failed, please triage").
Path B: HTTP Request version (n8n Cloud, or any decision API)
The http-request-portable.json template needs no community package, so it works on n8n Cloud. It's one HTTP Request node against /v1/ask and one Switch.
- Method: POST to
<your endpoint>/v1/ask, headerAuthorization: Bearer <key>(use n8n's Header Auth credential). - Body:
{
"model": "saina-helm-0.8b",
"mode": "decision",
"state": "{{ $json.message }}",
"questions": {
"team": {
"type": "single_choice",
"question": "Which team should handle this request?",
"options": {
"billing": "Charges, invoices, refunds, subscription changes",
"technical": "Outages, errors, bugs, account access",
"other": "Anything that does not fit the teams above"
}
}
},
"threshold": 0.8,
"min_margin": 0.05
}
- Branch: if
{{ $json.answers.team.reason === "accepted" }}, switch on{{ $json.answers.team.selection }}(billing/technical/other); otherwise send the item to review. The template folds both into one Switch expression with a fourth "review" output. HTTP errors stop the workflow unless you configure the node's error output.
Which endpoint? n8n Cloud can't reach a server on localhost or your internal network. Point it at a public HTTPS endpoint with auth in front (reverse proxy or tunnel). The same HTTP Request slot can also call another hosted decision API, such as OpenAI's GPT-6 Luna Decisions; it has its own request shape and response fields, so adapt the body and the branch expression to its docs.
Setting thresholds and margins
- Start at threshold 0.8 on real traffic for a week.
- Margin only matters at lower thresholds. For a single-choice question the probabilities sum to 1, so if the top answer is at 0.8 or above, the runner-up is at 0.2 or below: the gap is already at least 0.6. A margin of 0.05 (or 0.2) never rejects anything at threshold 0.8. It starts to bite when you lower the threshold, for example threshold 0.5 with margin 0.2 rejects a 0.52 vs. 0.40 split. The node's default margin is 0.
- A close two-way race below the threshold is reported as
below_threshold, notbelow_margin. To tell "overlapping options" from "missing option", look at the logged top-two probabilities, not the reason code alone. - Watch the fallback rate. Too high (as a rule of thumb, above a third): your options overlap or your descriptions are vague; fix the definitions before touching the numbers. Too low (near zero): you may be auto-routing coin flips; raise the threshold.
- Tune per question, not per workflow: a priority question can be strict while a tag question stays loose. For a Multiple Answers (tag) question the threshold applies to each label, and at 0.8 often nothing clears, which sends the whole item to Fallback; start lower, e.g. 0.5 (an example to tune).
Testing before you trust it
- Assemble 50–100 real tickets with known correct routes for a first check (your last weeks of manual routing are the corpus); 200+ before you tune thresholds.
- Run them through the workflow with routing disabled (log-only "shadow mode"); compare selections to the truth.
- Fix option wording where humans and the model disagree; re-run.
- Only then connect the Switch to real actions.
The full test-set and shadow-mode steps live in the automated ticket routing guide.
Logging fallbacks
Log every fallback with: ticket ID, probabilities, reason code, timestamp, and the reviewer's eventual decision. Two payoffs: you can see which categories generate uncertainty (option-design feedback), and reviewed fallbacks become labeled evaluation data.
Checklist
- Started from
support-routing.json(self-hosted) orhttp-request-portable.json(n8n Cloud). - Answers include an explicit
Otherthat routes to review, not to a team. - Decision mode on; threshold and margin set and written down.
- Fallback output wired somewhere a human actually looks; error handling decided.
- Shadow-run against 50–100 labeled tickets before enabling routing.
- Fallback log capturing probabilities + reason + eventual human decision.
- Test set re-run monthly, and after any change to options, thresholds, or model version.