Support ticket triage

Route, prioritise, and flag urgency for an inbound ticket with three decision calls.

Support triage asks three things of the same text: the right queue, the severity, and the flags that need a human now.

Three capabilities answer them: classify routes, rate prioritises, and yes-no raises flags. The calls are independent, so run them at the same time.

No API key is required during the launch period.

The ticket

Every single-ticket response on this page comes from this text.

Order #88213 still has not arrived and the tracking page has said "label created"
for nine days. I leave the country on Friday and I need this resolved before then.
I have emailed twice with no reply.

Route it with classify

Describe each queue. The API sends each label to the model as name: description. A bare name like shipping scores worse than a sentence about the case.

POST /v1/decision-machine-1/classify
{
"text": "Order #88213 still has not arrived ...",
"labels": {
"shipping": "the customer asks about delivery, tracking or a late or lost order",
"billing": "the customer asks about a charge, refund, invoice or subscription payment",
"account": "the customer cannot sign in or needs a change to their account details",
"product_issue": "the product is broken, defective or does not work as described"
}
}
Response (0.99 s)
{
"label": "shipping",
"probability": 1,
"confidence": 0.997,
"scores": { "shipping": 1, "billing": 0, "account": 0, "product_issue": 0 }
}

label names the queue. confidence near 1 means one label won clearly. Route on confidence, not on probability alone.

Prioritise it with rate

A scale is a list of level descriptions, ordered low to high. Use 2 to 10 levels. Write what each level looks like, never a number.

POST /v1/decision-machine-1/rate
{
"text": "Order #88213 still has not arrived ...",
"scale": [
"A routine question with no deadline",
"An inconvenience the customer can wait on",
"A blocked customer with a deadline",
"A customer threatening to leave or escalate"
]
}
Response (0.94 s)
{ "score": 2.497, "level": 2, "confidence": 0.5, "scores": [0, 0, 0.503, 0.497] }

Read this result honestly. level is 2 by a margin of 0.006, and score 2.497 sits almost exactly between levels 2 and 3. confidence 0.5 reports that split. Sort your queue on score. It keeps the disagreement. Use level only as a label in the UI.

score is a weighted position. It is Σ p_i · i over the levels, so it ranges from 0 to scale.length - 1. A ticket at 2.497 outranks one at 2.05 even though both report level 2.

Flag it with a yes-no batch

Send the flags as statements. All of them go into one inference call, so three flags take about the time of one.

POST /v1/decision-machine-1/yes-no
{
"text": "Order #88213 still has not arrived ...",
"statements": [
"The customer states a deadline or expresses urgency.",
"The customer asks for a refund.",
"The customer threatens to cancel or leave."
]
}
Response (1.18 s)
{
"results": [
{"statement": "The customer states a deadline or expresses urgency.", "answer": true, "probability": 1},
{"statement": "The customer asks for a refund.", "answer": false, "probability": 0.092},
{"statement": "The customer threatens to cancel or leave.", "answer": false, "probability": 0.353}
]
}

The cancel flag scores 0.353. That is a false answer with real doubt behind it. Hand this case to a person.

All three calls together

Fire the three requests concurrently. Each one takes about 1 second, so the whole triage finishes in about the time of the slowest call.

# ticket.txt holds the ticket text above.
BASE=https://api.milliseconds.ai/v1/decision-machine-1
Q='{"shipping":"the customer asks about delivery, tracking or a late or lost order","billing":"the customer asks about a charge, refund, invoice or subscription payment","account":"the customer cannot sign in or needs a change to their account details","product_issue":"the product is broken, defective or does not work as described"}'
S='["A routine question with no deadline","An inconvenience the customer can wait on","A blocked customer with a deadline","A customer threatening to leave or escalate"]'
F='["The customer states a deadline or expresses urgency.","The customer asks for a refund.","The customer threatens to cancel or leave."]'
jq -n --rawfile t ticket.txt --argjson l "$Q" '{text:$t,labels:$l}' | curl -s "$BASE/classify" -H "content-type: application/json" -d @- &
jq -n --rawfile t ticket.txt --argjson s "$S" '{text:$t,scale:$s}' | curl -s "$BASE/rate" -H "content-type: application/json" -d @- &
jq -n --rawfile t ticket.txt --argjson f "$F" '{text:$t,statements:$f}' | curl -s "$BASE/yes-no" -H "content-type: application/json" -d @- &
wait

Turn the numbers into an action

Pick a threshold per action, not one threshold for the whole system. Auto-routing a ticket is cheap to undo; paging an on-call engineer is not.

SignalActionSuggested band
classify.confidenceRoute automatically≥ 0.9
classify.confidenceRoute, but show the second label to the agent0.6 to 0.9
classify.confidenceSend to the general queue< 0.6
rate.scorePut at the top of the queue≥ 2.5 on a 4-level scale
yes-no.probabilityRaise the flag≥ 0.8
yes-no.probabilityQueue for a human check0.35 to 0.8

Tune these numbers against your own tickets. Golden sets and Tuning thresholds show the method.

Triage a backlog

Every capability accepts texts for up to 32 items. The response is {"results": [...]} in input order. The second ticket below reads: “I was charged twice for my subscription this month and support has not replied.”

classify with two tickets (x-input-chars: 279, x-input-tokens: 69)
{
"results": [
{"label": "shipping", "probability": 1, "confidence": 0.997,
"scores": {"shipping": 1, "billing": 0, "account": 0, "product_issue": 0}},
{"label": "billing", "probability": 0.984, "confidence": 0.935,
"scores": {"shipping": 0, "billing": 0.984, "account": 0.012, "product_issue": 0.004}}
]
}

Each text in a texts batch is a separate inference call, so 32 tickets cost 32 calls of work. A batch of statements on one text stays one call, as batching explains.

Pitfalls

On yes-no, when_true and when_false must describe the case. The wording when_true: "yes" flipped a measured answer from true at probability 1 to false at probability 0.004. Write the case, not the verdict.

A scale of ["1", "2", "3", "4"] gives the model nothing to read. Write the level, for example “A blocked customer with a deadline”.

Text over 2,000 characters is chunked, and a label’s score is its maximum over the chunks. A complaint in the last message still scores high. Trim quoted signatures first, and read long text and chunking before you send threads.

A 529 overloaded response means every inference slot stayed busy. Retry with backoff, and match the other codes in the error reference.

Next