Agent and AI engineering · Practical technical guide · 26 October 2026
The OpenAI Decisions API reached general availability on 22 October 2026. A practical guide to what its answers contain, how to turn a probability into approve, review or deny with a labelled sample and a cost model, why a score must not be rounded, and how to batch and cache questions safely.
What the API is, and what changed at GA
The Decisions API is a separate endpoint, POST /v1/decisions, for questions with typed answers rather than generated text. The OpenAI changelog for 22 October 2026 lists it as generally available with gpt-6-luna, which the guide says is currently the only supported model. Our research digest of 9 October covered the public beta, which arrived on 6 October; this guide is about using the GA version in an application.
The guide lists what changed since the beta: prompt caching for shared input, images inside individual question instructions, structured evidence passed as JSON with an input_json part, up to 400 questions per request instead of 200, hosted image URLs as well as inline data URLs, conversation history that may include function calls, custom tool calls and their outputs, organisation usage reporting, a native GPT-Live integration and Private Link. The guide also states the boundary clearly: use Decisions when you need one of its answer types, and Structured Outputs or function calling when you need the model to generate an object or request a tool call.
Three question types and what comes back
| Question type | You supply | Answer contains | Use it for |
|---|---|---|---|
| predicate | Instructions | probability, from 0 to 1, that the condition is true | Yes or no checks: is the charge a duplicate |
| choice | Instructions and 2 to 255 unique choices | the chosen value, a confidence, and a probability for every value | Unordered categories: which department |
| score | Instructions and ordered levels | a probability for every level and a score, the probability-weighted average of level indices | Ordered levels: issue severity |
| any of them | type refusal with the question name, instead of an answer | Must be handled for every question |
Answers come back in question order and carry the name you gave each question, so name every question and match by name, not position. A refusal affects only its own question; the other answers in the same response are still valid. Choice values are typed, so a string true and a boolean true are different values. None of these answers is an action. The guide is explicit that your application runs whatever action an answer selects, which is exactly where the engineering work lies.
Turning a probability into approve, review or deny
A single cut at 0.5 treats a probability as if it were already the right decision boundary. It rarely is, for two reasons: the costs of the two kinds of mistake differ, and a model's probabilities are not guaranteed to match how often it is right. Both are checked against a labelled sample of your own traffic before any threshold is trusted.
The companion example uses a synthetic sample of 200 duplicate-charge cases with a deliberately overconfident model, and costs of 40 for wrongly approving a refund, 25 for wrongly denying one, and 3 for a human review. Its reliability table shows the overconfidence: cases the model put in the top bin averaged 0.962 but only 0.894 were real duplicates, and the bottom bin averaged 0.039 with 0.093 real. A single 0.5 cut makes 20 wrong approvals and 13 wrong denials, a cost of 1,125. The cheapest band on the same 200 items denies at or below 0.10, approves at or above 0.90 and sends the 54 cases in between to a person, for a cost of 412.
Python from the companion example, condensed; standard library only.
def band_cost(items, low, high, cost):
"""Approve at or above high, deny at or below low, review the rest."""
t = {"wrong_approve": 0, "wrong_deny": 0, "reviewed": 0}
for x in items:
if x["probability"] >= high:
t["wrong_approve"] += not x["label"]
elif x["probability"] <= low:
t["wrong_deny"] += x["label"]
else:
t["reviewed"] += 1
t["cost"] = (t["wrong_approve"] * cost["wrong_approve"]
+ t["wrong_deny"] * cost["wrong_deny"]
+ t["reviewed"] * cost["review"])
return t
def choose_band(items, cost, step=0.05):
grid = [round(i * step, 2) for i in range(int(1 / step) + 1)]
pairs = [(lo, hi) for lo in grid for hi in grid if hi > lo]
lo, hi = min(pairs, key=lambda b: (band_cost(items, *b, cost)["cost"], b[1] - b[0]))
return lo, hiThe same example also shows why the sample size matters. Fitted on only the first 100 items, the cheapest band never approves automatically and denies at or below 0.05; applied to the other 100, it costs 244 against 720 for the 0.5 cut. With a hundred labels there is not enough evidence to trust any automatic approval, and the search correctly says so. Fit the band on one sample, check it on another, and refit whenever the model, the question instructions or the mix of inputs changes.
Choice versus score, and why a score is not a level
Use choice when the options have no order and score when they do. The distinction matters because a score answer returns a weighted average that can fall between levels. In the example's score answer, the four severity levels have probabilities 0.1, 0.3, 0.5 and 0.1, and the score is 1.6. Rounding that to level 2 and paging on high severity looks reasonable, but the quantity you actually care about is the probability that severity is high or worse, which is 0.6, and that is what should be compared with a paging threshold. For a choice answer, gate on the probability of the chosen value rather than treating the choice as certain: the example sends a choice whose top value has probability 0.71 to review.
Evidence, conversations and untrusted input
The input can be a string or an ordered list of messages, including function calls and their outputs matched by call_id. JSON evidence goes in an input_json part, which is only accepted in a user message; the guide notes that keys such as role or image_url inside that data are ordinary data and do not create messages or fetch images. That is helpful, but it does not make the evidence trustworthy. Text inside a ticket or a tool output can still argue for an answer, so keep the question instructions in the developer message, treat the evidence as data, and keep the action under your own code, which the API's design already encourages.
Failure modes and verification
- Thresholds without labels. A threshold chosen by intuition encodes neither your costs nor the model's calibration. Keep a labelled sample per question and refit on a schedule.
- Refusals mapped to a default. Treating a refusal as no, or as zero, quietly turns a declined answer into a denial. Route refusals to review and count them.
- Silent drift. The model, the instructions or your traffic can change the meaning of a probability. Track the share of items in the review band and the agreement between automatic decisions and later human outcomes.
- Scores rounded to levels. Compare level probabilities with your threshold, as above.
- Assuming one question per request is required. It is not, and it costs more; batch related questions over shared evidence.
The companion example, decision_threshold_calibration in Sandeep-BA/agent-decision-lab, has six tests covering the reliability table, the band, the holdout, the score threshold and refusal routing. It never calls the API; its response fixture was written by hand in the documented shape.
When the Decisions API is the wrong tool
If you need extracted fields, an explanation or any generated object, use Structured Outputs. If a deterministic rule decides the case, such as an amount under a fixed limit, do not ask a model at all. And if no one will ever label outcomes, you cannot fit or check thresholds, so the probabilities are decoration; fix that before automating anything on them.
Sources and scope
Checked against these sources on 26 October 2026. Figures come from the offline companion example with a synthetic labelled sample; no provider call was made.
Treat a Decisions answer as evidence, not an action: check calibration on labelled examples, choose an approve, review and deny band from your own error costs, compare level probabilities rather than a rounded score, route refusals to review, and refit whenever the model, the question or the input mix changes.