Blog · October 12, 2026

Automated Ticket Routing: The Complete Setup Guide

Automated ticket routing, start to finish: four approaches compared, defining routes that hold up, building the test set, and wiring a safety review branch.

Saina · October 12, 2026 · 4 min read

Automated ticket routing is the canonical automation decision: read a message, pick from a list you own, send it somewhere. It's also where most teams first feel the difference between a model that answers and a model that decides. This guide covers the four ways to do it, then the setup walkthrough that applies to whichever you pick.

The four approaches

1. Keyword rules

IF body CONTAINS "refund" → billing. Zero ML, instant setup, fully explainable.

2. Chat LLM prompt

Ask GPT-6/Claude "reply with billing, technical or other" and parse.

3. Hosted decision model

A decision model behind an API, such as OpenAI's GPT-6 Luna Decisions (released 2026-10-06; see the Luna Decisions alternatives guide for specs and other hosted options): probabilities per option, many questions per request.

4. Self-hosted decision model / classifier

Open-weights decision models (e.g., Saina Helm) or fine-tuned classifiers on your own box.

No row here settles accuracy; measure on your own tickets.

The automated ticket routing setup walkthrough

Step 1: Define the routes

Write each route as an option with an ID and a plain-language description:

"billing": "Charges, invoices, refunds, subscription pricing changes"

Rules that hold up:

You still maintain this list, whatever approach you use. The difference: with decision models it's data passed per request, rather than prompt prose or rule sprawl.

Step 2: Build the test set

Step 3: Wire the routing step

The Helm shape, one call, route + priority + tags together:

from saina import Saina

client = Saina('https://helm.internal.example', api_key='YOUR_KEY')
result = client.ask(
    model='saina-helm-0.8b',
    state=ticket_text,
    mode='decision',            # required for thresholds and reason codes
    threshold=0.8,              # request-level default
    questions={
        'team': {
            'type': 'single_choice',
            'question': 'Which team should handle this ticket?',
            'options': route_options,   # the data from Step 1
        },
        'priority': {
            'type': 'rating',
            'question': 'How urgent is this?',
            'levels': ['Routine', 'Same-day attention', 'Drop other work'],
        },
        'tags': {
            'type': 'multi_choice',
            'question': 'Which topics apply?',
            'options': {'refund': 'Money back requests',
                        'outage': 'Service down or degraded',
                        'access': 'Login or permission trouble'},
            'threshold': 0.5,   # applies per tag; an example to tune
        },
    },
)

team = result['answers']['team']['selection']          # None if rejected
priority = result['answers']['priority']['level']      # 0, 1 or 2 (zero-based), None if rejected
tags = result['answers']['tags']['selections']         # tags at or above 0.5

The tag question gets its own lower threshold because it applies to each tag independently; at 0.8 a tag question often selects nothing. client.ask returns a plain dict; network or validation failures raise SainaError.

In n8n, the Helm community node (self-hosted n8n) exposes Selected (answer accepted) and Fallback (answer rejected, plus errors if you configure it) as outputs; see Routing support tickets in n8n. The HTTP Request template calls the same /v1/ask endpoint and works on n8n Cloud. A different hosted decision API fits the same slot but needs its own request shape and branch logic.

Step 4: Add the review branch

Step 5: Validate, then enable

Shadow mode first: log decisions without routing on them; compare to the test set and to current behavior. Fix option wording where disagreement clusters. Then enable, and re-run the test set monthly, and after any change to options, thresholds, or model version.

Checklist