Cookbook
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. teamis apick. The teams are mutually exclusive, so one pick is better than four checks. The option descriptions say what each team does;otherneeds no description.urgencyis arateon four levels from lowest to highest.rateis in beta, so its floor is low and the code below only uses it to raise a priority.churn_riskis acheck: one yes or no fact, answered as the probability of yes.min_confidenceon every question your code acts on. Below it the answer abstains, and the ticket goes to a person.
{
"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). 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.
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# 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)// 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
{
"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.
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)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) 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.
- 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.
- 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_confidenceuntil the automatic routes are right often enough. See Abstention.