Triage support tickets in one request with OpenAI's Decisions API
By Flavio Copes
OpenAI's Decisions API returns a probability, a choice, or a score. Triage a support ticket with several questions in one request.
Yesterday, October 22, OpenAI made the Decisions API generally available.
It’s a dedicated endpoint for small questions with bounded answers. Is this a duplicate charge? Which department gets this ticket? How severe is this bug? You don’t get free text back. You get a probability, a choice from your list, or a score.
OpenAI says it answers about 10x faster than running the same model, gpt-6-luna, through the Responses API. It’s also cheap. Input costs $0.10 per million tokens, cached input costs $0.01 per million, and output tokens aren’t charged at all.
Apps and agents make lots of these small decisions: routing, classifying, filtering. Until now we used a full chat completion for each one and parsed the result. This endpoint does just that job.
The three question types
Every request has a model, an input (the evidence) and a questions array. Each question has a type:
predicatechecks a condition and returns aprobabilityfrom 0 to 1choicepicks one value from the options you give itscorerates the input against 2 to 10 orderedlevels
The only supported model right now is gpt-6-luna.
Your first decision with curl
Let’s route a complaint to a department. Send a choice question to POST /v1/decisions:
curl https://api.openai.com/v1/decisions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-6-luna",
"input": "I was charged twice for my order.",
"questions": [{
"type": "choice",
"name": "department",
"instructions": "Which department should handle this complaint?",
"choices": [
{"value": "billing", "description": "Payments, invoices, and refunds."},
{"value": "technical", "description": "Problems using the product."},
{"value": "shipping", "description": "Delivery and tracking."},
{"value": "other", "description": "Requests outside these categories."}
]
}]
}'
You get back an answers array. Each answer carries the name you gave the question, so you can match them up:
{
"answers": [
{
"type": "choice",
"name": "department",
"choice": "billing",
"probabilities": [
{ "value": "billing", "probability": 0.95 },
{ "value": "technical", "probability": 0.02 },
{ "value": "shipping", "probability": 0.01 },
{ "value": "other", "probability": 0.02 }
],
"confidence": 0.93
}
]
}
That response is the example from OpenAI’s docs, so your numbers will differ.
Notice the other option. OpenAI recommends a fallback like this when your categories don’t cover every input, so you can send those tickets to a general queue.
Ask several questions at once
One request can hold up to 400 questions, and each one can be a different type. Let’s build a small triage script in Node.js that asks three things about the same ticket.
Install the official SDK first:
npm install openai
The client reads OPENAI_API_KEY from your environment. Save this as triage.mjs:
import OpenAI from 'openai'
const client = new OpenAI()
const decision = await client.decisions.create({
model: 'gpt-6-luna',
input: 'Export fails in Safari but works in Chrome. I need the CSV for my invoice today.',
questions: [
{
type: 'predicate',
name: 'urgent',
instructions: 'Does the customer say they need this solved today?'
},
{
type: 'choice',
name: 'department',
instructions: 'Which department should handle this ticket?',
choices: [
{ value: 'billing', description: 'Payments, invoices, and refunds.' },
{ value: 'technical', description: 'Problems using the product.' },
{ value: 'other', description: 'Requests outside these categories.' }
]
},
{
type: 'score',
name: 'severity',
instructions: 'How severe is this issue?',
levels: [
{ label: 'Cosmetic', description: 'Appearance only; no lost functionality.' },
{ label: 'Workaround available', description: 'A task fails, but another way works.' },
{ label: 'Fully blocked', description: 'A task fails with no workaround.' }
]
}
]
})
for (const answer of decision.answers) {
if (answer.type === 'refusal') console.log(`${answer.name}: refused`)
if (answer.type === 'predicate') console.log(`${answer.name}: ${answer.probability}`)
if (answer.type === 'choice') console.log(`${answer.name}: ${answer.choice}`)
if (answer.type === 'score') console.log(`${answer.name}: ${answer.score}`)
}
Run it:
node triage.mjs
Be careful with the refusal type. Any answer can come back as a refusal, so check for it before reading the other fields.
The score can fall between levels. Level indices start at 0, and the score is the probability-weighted average of those indices. In OpenAI’s example for this same Safari bug, probabilities of 0.1, 0.7 and 0.2 give a score of 1.1, just above “Workaround available”.
Pass JSON directly
New in the GA release, you can pass structured data with an input_json content part and skip turning it into a string. It only works in a user message:
const decision = await client.decisions.create({
model: 'gpt-6-luna',
input: [
{
role: 'user',
content: [
{ type: 'input_text', text: 'Evaluate the ticket using the order evidence.' },
{
type: 'input_json',
data: {
ticket: { body: 'Please refund the duplicate charge.' },
order: { charges: [49, 49] }
}
}
]
}
],
questions: [
{
type: 'predicate',
name: 'duplicate_charge',
instructions: 'Does the ticket describe a duplicate charge supported by the order?'
}
]
})
const answer = decision.answers[0]
if (answer.type === 'predicate') {
console.log(answer.probability)
} else {
console.log('Could not evaluate the charge. Send it for review.')
}
Keys like role inside data are treated as plain data, so they won’t create new messages.
You can also send images. Add an input_image part with a base64 data URL or a public HTTPS URL. file_id inputs aren’t supported.
Pick your thresholds
The API gives you probabilities. Deciding what to do with them is your job. My advice is to run 50 or so real tickets you’ve already labeled, look at the numbers, and choose a cutoff based on what a mistake costs you. A missed urgent ticket probably costs more than a false alarm.
If you send the same long context on every call (a policy document, for example), keep it identical at the start of input. Decisions caches a shared prefix of at least 1,024 tokens by default. Questions sit outside that prefix, so you can change them freely and still get the cached price.
You can also try questions in the Decisions Playground before writing any code.
Sources
Want me to talk about your product? You can sponsor this site.
Related posts about ai: