# Recipe: route and prioritise support tickets

> Pick the team, rate the urgency and check for churn risk in one call, then route the ticket or hand it to a person when an answer abstains.

Every incoming ticket gets a team, a reply deadline and a churn flag before anyone opens it. One request answers all three questions, and the ticket text is billed once.

## The request

- **State.** The ticket as JSON, so each question can point at `{{ticket.body}}` while the model can still read the channel, the subject and the customer fields. Send only the fields the decision needs: no email addresses, no account numbers.
- **`team` is a `pick`.** The teams are mutually exclusive, so one pick is better than four checks. The option descriptions say what each team does; `other` needs no description.
- **`urgency` is a `rate`** on four levels from lowest to highest. `rate` is in beta, so its floor is low and the code below only uses it to raise a priority.
- **`churn_risk` is a `check`**: one yes or no fact, answered as the probability of yes.
- **`min_confidence`** on every question your code acts on. Below it the answer abstains, and the ticket goes to a person.

```json
{
  "state": {
    "ticket": {
      "channel": "chat",
      "subject": "App logs me out every few minutes",
      "body": "Since this morning your app logs me out every few minutes and I lose the order I am building. I run a small bakery and must order flour before 14:00 today. If this keeps happening I will move to another supplier."
    },
    "customer": { "plan": "business", "customer_since": "2024-03-02", "open_tickets": 1 }
  },
  "questions": {
    "team": {
      "type": "pick",
      "instructions": "Which team should handle {{ticket.body}}?",
      "options": {
        "billing": "Payments, refunds and invoices",
        "technical": "Bugs, outages and sign-in problems",
        "shipping": "Delivery and returns",
        "other": null
      },
      "min_confidence": 0.5
    },
    "urgency": {
      "type": "rate",
      "instructions": "How fast must we reply to {{ticket.body}}?",
      "levels": ["Can wait", "This week", "Within two working days", "Today"],
      "min_confidence": 0.3
    },
    "churn_risk": {
      "type": "check",
      "instructions": "Does the writer of {{ticket.body}} say they may cancel or move to another supplier?",
      "min_confidence": 0.6
    }
  }
}
```

## Run it

Put a test key in `DEX_API_KEY` (see the [Quickstart](/docs/quickstart/#get-a-key-in-60-seconds)). Save the Python code as `support_routing.py` and run `python support_routing.py`. Save the TypeScript code as `support-routing.mts` and run `npx tsx support-routing.mts`: the code uses `await` at the top level, and the `.mts` ending makes the file an ES module. Install the SDKs from [Downloads](/docs/reference/sdks/#downloads).

```bash tab="curl"
curl https://api.thinqit.ai/v1/decide \
  -H "authorization: Bearer $DEX_API_KEY" \
  -H "content-type: application/json" \
  --data-binary @- <<'DEX_REQUEST'
{
  "state": {
    "ticket": {
      "channel": "chat",
      "subject": "App logs me out every few minutes",
      "body": "Since this morning your app logs me out every few minutes and I lose the order I am building. I run a small bakery and must order flour before 14:00 today. If this keeps happening I will move to another supplier."
    },
    "customer": { "plan": "business", "customer_since": "2024-03-02", "open_tickets": 1 }
  },
  "questions": {
    "team": {
      "type": "pick",
      "instructions": "Which team should handle {{ticket.body}}?",
      "options": {
        "billing": "Payments, refunds and invoices",
        "technical": "Bugs, outages and sign-in problems",
        "shipping": "Delivery and returns",
        "other": null
      },
      "min_confidence": 0.5
    },
    "urgency": {
      "type": "rate",
      "instructions": "How fast must we reply to {{ticket.body}}?",
      "levels": ["Can wait", "This week", "Within two working days", "Today"],
      "min_confidence": 0.3
    },
    "churn_risk": {
      "type": "check",
      "instructions": "Does the writer of {{ticket.body}} say they may cancel or move to another supplier?",
      "min_confidence": 0.6
    }
  }
}
DEX_REQUEST
```

```python tab="Python"
# Save as dex_request.py, then run: python dex_request.py
import json

from thinqit_dex import Client

client = Client()  # reads DEX_API_KEY from the environment

request = json.loads(r'''
{
  "state": {
    "ticket": {
      "channel": "chat",
      "subject": "App logs me out every few minutes",
      "body": "Since this morning your app logs me out every few minutes and I lose the order I am building. I run a small bakery and must order flour before 14:00 today. If this keeps happening I will move to another supplier."
    },
    "customer": { "plan": "business", "customer_since": "2024-03-02", "open_tickets": 1 }
  },
  "questions": {
    "team": {
      "type": "pick",
      "instructions": "Which team should handle {{ticket.body}}?",
      "options": {
        "billing": "Payments, refunds and invoices",
        "technical": "Bugs, outages and sign-in problems",
        "shipping": "Delivery and returns",
        "other": null
      },
      "min_confidence": 0.5
    },
    "urgency": {
      "type": "rate",
      "instructions": "How fast must we reply to {{ticket.body}}?",
      "levels": ["Can wait", "This week", "Within two working days", "Today"],
      "min_confidence": 0.3
    },
    "churn_risk": {
      "type": "check",
      "instructions": "Does the writer of {{ticket.body}} say they may cancel or move to another supplier?",
      "min_confidence": 0.6
    }
  }
}
''')

decision = client.decide(
    request["state"],
    request["questions"],
)
for question_id, answer in decision.answers.items():
    print(question_id, answer)
```

```ts tab="TypeScript"
// Save as dex-request.mts, then run: npx tsx dex-request.mts (Node.js 18 or newer)
import { Client, parseRequest } from "@thinqit/dex";

const client = new Client(); // reads DEX_API_KEY from the environment

// parseRequest keeps the key order of the text (JSON.parse would move labels such as "1" to the front).
const request = parseRequest(`{
  "state": {
    "ticket": {
      "channel": "chat",
      "subject": "App logs me out every few minutes",
      "body": "Since this morning your app logs me out every few minutes and I lose the order I am building. I run a small bakery and must order flour before 14:00 today. If this keeps happening I will move to another supplier."
    },
    "customer": { "plan": "business", "customer_since": "2024-03-02", "open_tickets": 1 }
  },
  "questions": {
    "team": {
      "type": "pick",
      "instructions": "Which team should handle {{ticket.body}}?",
      "options": {
        "billing": "Payments, refunds and invoices",
        "technical": "Bugs, outages and sign-in problems",
        "shipping": "Delivery and returns",
        "other": null
      },
      "min_confidence": 0.5
    },
    "urgency": {
      "type": "rate",
      "instructions": "How fast must we reply to {{ticket.body}}?",
      "levels": ["Can wait", "This week", "Within two working days", "Today"],
      "min_confidence": 0.3
    },
    "churn_risk": {
      "type": "check",
      "instructions": "Does the writer of {{ticket.body}} say they may cancel or move to another supplier?",
      "min_confidence": 0.6
    }
  }
}`);

const decision = await client.decide(request);
console.log(decision.answers);
```

## Expected output

```json
{
  "id": "req_01M3JE2D3792VXG4CSFWBBBC68",
  "object": "decision",
  "created": 1790546359,
  "model": "dex-1.0.1",
  "served_by": "gpu",
  "calibration": "cal-20260926-1",
  "answers": {
    "team": {
      "type": "pick",
      "choice": "technical",
      "probabilities": {
        "billing": 0.0392,
        "technical": 0.9066,
        "shipping": 0.0299,
        "other": 0.0243
      },
      "confidence": 0.8673,
      "abstained": false
    },
    "urgency": {
      "type": "rate",
      "rating": 2.6751,
      "levels": ["Can wait", "This week", "Within two working days", "Today"],
      "probabilities": [0.0335, 0.058, 0.1083, 0.8002],
      "confidence": 0.5117,
      "abstained": false
    },
    "churn_risk": {
      "type": "check",
      "probability": 0.9604,
      "confidence": 0.9209,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 185,
    "state_tokens": 99,
    "question_tokens": 86,
    "allowance_tokens": 0,
    "paid_tokens": 0,
    "charge_micro_cents": 0,
    "unit_price_micro_cents": 0,
    "tier": "test"
  }
}
```

Captured from the live API on 2026-09-27 with a test key: model `dex-1.0.1`, calibration `cal-20260926-1`, `served_by: gpu`, 185 input tokens (99 for the state, 86 for the questions). A test key is charged nothing, so `tier` is `test` and the charge is 0. On this exact version the same request always returns these answers.

| Question | Type | Answer | Confidence | `min_confidence` | Abstained |
| --- | --- | --- | --- | --- | --- |
| `team` | pick | `technical` (0.9066) | 0.8673 | 0.5 | no |
| `urgency` | rate | rating 2.6751, most likely `Today` (0.8002) | 0.5117 | 0.3 | no |
| `churn_risk` | check | yes with probability 0.9604 | 0.9209 | 0.6 | no |

The ticket goes to `technical` with probability 0.9066. The urgency rating of 2.6751 sits close to `Today`, and the writer says they may move to another supplier (0.9604). No answer abstained, so the code below can act on all three.

## Act on it

The functions `route_to_person`, `route_to_team`, `set_priority` and `flag_for_retention` stand for your own code.

```python tab="Python"
team = decision.pick("team")
if team.abstained:
    route_to_person(ticket_id)  # two teams are close: a person decides
else:
    route_to_team(ticket_id, team.choice)

urgency = decision.rate("urgency")
if not urgency.abstained and urgency.rating >= 2.5:
    set_priority(ticket_id, "high")

churn = decision.check("churn_risk")
if not churn.abstained and churn.probability >= 0.5:
    flag_for_retention(ticket_id)
```

```ts tab="TypeScript"
const { team, urgency, churn_risk: churn } = decision.answers;

if (team?.type === "pick") {
  if (team.abstained) routeToPerson(ticketId); // two teams are close: a person decides
  else routeToTeam(ticketId, team.choice);
}
if (urgency?.type === "rate" && !urgency.abstained && urgency.rating >= 2.5) setPriority(ticketId, "high");
if (churn?.type === "check" && !churn.abstained && churn.probability >= 0.5) flagForRetention(ticketId);
```

`parseRequest` keeps the request as text, so the TypeScript answers have the general `Answer` type and are narrowed on `type`. Build the questions with the SDK's `pick`, `rate` and `check` helpers instead (as in the [Quickstart](/docs/quickstart/#make-your-first-call)) and `decision.answers.team.choice` is typed as your labels.

## Adapt it

- **Your own teams.** Up to 255 options per pick. Describe each team by what it handles, not by its internal name.
- **A deadline, not a feeling.** When you have a service level, compute the deadline in your code and pass it as a state field; Dex reads, it does not calculate. See [Known limits](/docs/concepts/known-limits/#date-and-amount-arithmetic-is-unreliable).
- **Text from customers can try to steer the classifier.** For a high-impact action, add a guardrail check as in the [agent tool gate recipe](/docs/cookbook/agent-guardrails/).
- **Tune the floors on your own tickets.** Run a few hundred past tickets with a test key, compare with the team that handled them, and raise `min_confidence` until the automatic routes are right often enough. See [Abstention](/docs/concepts/abstention/).
