> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.milliseconds.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.milliseconds.ai/_mcp/server.

# Support ticket triage

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](/capabilities/classify) routes, [rate](/capabilities/rate) prioritises, and [yes-no](/capabilities/yes-no) raises flags. The calls are independent, so run them at the same time.

Every request needs an API key. See [Authentication](/authentication).

## The ticket

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

```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`**

```json title="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`**

```json title="Response"
{
  "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`**

```json title="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`**

```json title="Response"
{ "score": 2.497, "level": 2, "label": "A blocked customer with a deadline", "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. Show `label` in the UI instead of `level`.

> **Note**
>
> **`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`**

```json title="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`**

```json title="Response"
{
  "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.

**`curl`**

```bash title="curl"
# 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" -H "authorization: Bearer test_sk-..." -d @- &
jq -n --rawfile t ticket.txt --argjson s "$S" '{text:$t,scale:$s}'   | curl -s "$BASE/rate"      -H "content-type: application/json" -H "authorization: Bearer test_sk-..." -d @- &
jq -n --rawfile t ticket.txt --argjson f "$F" '{text:$t,statements:$f}' | curl -s "$BASE/yes-no" -H "content-type: application/json" -H "authorization: Bearer test_sk-..." -d @- &
wait
```

**`Python`**

```python title="Python"
from concurrent.futures import ThreadPoolExecutor
import requests

BASE = "https://api.milliseconds.ai/v1/decision-machine-1"

QUEUES = {
    "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",
}
SEVERITY = [
    "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",
]
FLAGS = [
    "The customer states a deadline or expresses urgency.",
    "The customer asks for a refund.",
    "The customer threatens to cancel or leave.",
]

def post(path, body):
    r = requests.post(f"{BASE}/{path}", headers={"authorization": "Bearer test_sk-..."}, json=body, timeout=30)
    r.raise_for_status()
    return r.json()

def triage(ticket):
    with ThreadPoolExecutor(3) as pool:
        route = pool.submit(post, "classify", {"text": ticket, "labels": QUEUES})
        sev = pool.submit(post, "rate", {"text": ticket, "scale": SEVERITY})
        flags = pool.submit(post, "yes-no", {"text": ticket, "statements": FLAGS})
    return {
        "queue": route.result(),
        "severity": sev.result(),
        "flags": {f["statement"]: f["probability"] for f in flags.result()["results"]},
    }
```

**`TypeScript`**

```typescript title="TypeScript"
const BASE = "https://api.milliseconds.ai/v1/decision-machine-1";

const QUEUES = {
  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",
};
const SEVERITY = [
  "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",
];
const FLAGS = [
  "The customer states a deadline or expresses urgency.",
  "The customer asks for a refund.",
  "The customer threatens to cancel or leave.",
];

async function post(path: string, body: unknown) {
  const res = await fetch(`${BASE}/${path}`, {
    method: "POST",
    headers: { "content-type": "application/json", authorization: "Bearer test_sk-..." },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${path}: ${res.status} ${await res.text()}`);
  return res.json();
}

export async function triage(text: string) {
  const [queue, severity, flags] = await Promise.all([
    post("classify", { text, labels: QUEUES }),
    post("rate", { text, scale: SEVERITY }),
    post("yes-no", { text, statements: FLAGS }),
  ]);
  return { queue, severity, flags: flags.results };
}
```

## 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.

| Signal                | Action                                        | Suggested band           |
| --------------------- | --------------------------------------------- | ------------------------ |
| `classify.confidence` | Route automatically                           | ≥ 0.9                    |
| `classify.confidence` | Route, but show the second label to the agent | 0.6 to 0.9               |
| `classify.confidence` | Send to the general queue                     | \< 0.6                   |
| `rate.score`          | Put at the top of the queue                   | ≥ 2.5 on a 4-level scale |
| `yes-no.probability`  | Raise the flag                                | ≥ 0.8                    |
| `yes-no.probability`  | Queue for a human check                       | 0.35 to 0.8              |

Tune these numbers against your own tickets. [Golden sets](/evaluate/golden-sets) and [Tuning thresholds](/evaluate/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)`**

```json title="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](/capabilities/batching) explains.

## Pitfalls

#### Hint text that names the verdict

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.

#### Numbers in a rate scale

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

#### Long ticket threads

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](/concepts/long-text) before you send threads.

#### Retries

A 529 `overloaded` response means every inference slot stayed busy. Retry with backoff, and match the other codes in the [error reference](/reference/errors).

## Next

#### [Confidence routing](/patterns/confidence-routing)

Act, confirm, review, escalate on the second axis.

#### [Writing good statements and labels](/concepts/writing-good-questions)

Why label text decides the answer.

#### [Tuning thresholds](/evaluate/tuning-thresholds)

Turn a labelled set into numbers per action.