Skip to content

Decision types

Nola provides three types for questions answered with probabilities: Choice, Scale and Prob. They require a model with decision support, such as the typesafe() provider. These types are built into .tsi files and need no import:

Type The question What the property evaluates to
Choice<{ label: "description"; … }> one of a set of labelled options { choice, probabilities, confidence? }
Scale<["low", …, "high"]> a position on an ordered scale of 2 to 10 levels { score, probabilities, levels, confidence? }
Prob<{ true: "…"; false: "…" }> or bare Prob whether a statement holds the probability of yes, 0 to 1

Use a literal union when you only need a label, or boolean when you only need a yes-or-no answer. Use a decision type when your code also needs probabilities. The choice of type determines the answer’s shape.

main.tsi
type Triage = {
/** Which team should handle this? */
department: Choice<{ billing: "Payments and refunds"; sales: "Pricing and upgrades" }>;
/** How frustrated is the customer? */
frustration: Scale<["Calm", "Frustrated but civil", "Angry"]>;
/** Does the message express urgency? */
urgent: Prob<{ true: "Explicit time pressure"; false: "No urgency expressed" }>;
/** Plain forms, unchanged — any model can answer them */
team: "billing" | "sales";
isUrgent: boolean;
};
const .ticket = "I was charged twice and nobody answers my emails. I want my money back NOW.";
const t = ask `Evaluate this support ticket`<Triage>;
console.log(t.department.choice, t.department.probabilities.billing, t.department.confidence);
console.log(t.frustration.score, t.frustration.levels[Math.round(t.frustration.score)]);
console.log(t.urgent > 0.8, t.team, t.isUrgent);

A property’s JSDoc comment gives the model instructions for that question. Its type argument defines the options, ordered levels or criteria for true and false.

Choice accepts a type literal with string-literal descriptions (or null for labels that need no description), or a label union such as Choice<"a" | "b">. It supports 2 to 255 labels. The answer includes the selected label in choice, a probability for each label in probabilities, and an optional confidence between 0 and 1. The probabilities sum to 1.

Labels can be strings, numbers or both: Choice<1 | 2 | 3>, Choice<{ 1: "Low"; 2: "High" }> and Choice<1 | "other"> are valid. Numeric labels stay numeric in choice, while probability keys are always strings (probabilities["2"]). Labels such as 1 and "1" cannot coexist because they share the same JSON key (NOLA2015).

Scale accepts a tuple of 2 to 10 string literals, ordered from low to high. Its score is the probability-weighted average of the level indices, from 0 to levels.length - 1. probabilities lists the probabilities in level order, and levels contains your original tuple. You can use levels[Math.round(score)] to select the label nearest the average; it does not necessarily have the highest probability.

Prob accepts an optional { true: "…"; false: "…" } literal describing the two outcomes. Its result is a number from 0 to 1 representing the probability that the statement holds. It has no separate confidence field.

Invalid criteria, such as a non-literal description, a single label, eleven levels or an extra Prob member, raise NOLA2015 at the type.

Nola requires decision support whenever an output type contains Choice, Scale or Prob. A model without that capability is rejected before the request is sent (NOLA3018). This prevents ordinary generated numbers from being treated as decision-model probabilities.

Use typesafe() for decision requests, mockProvider(replies, { decisions: true }) for tests, or a replay() ledger for recorded responses. You can route decision asks by name while using a chat model for other asks:

nola.config.ts
import { defineConfig } from "@nola-lang/runtime";
import { openai, typesafe } from "@nola-lang/providers";
export default defineConfig({
model: { default: openai("gpt-5-mini"), decision: typesafe() },
});
triage.tsi
type Triage = { department: Choice<{ billing: "Payments"; sales: "Pricing" }> };
const .ticket = "Charged twice for order #88.";
const t = ask with decision `Evaluate this support ticket`<Triage>;
console.log(t.department.choice);

fallback([typesafe(), openai("…")]) supports decisions because one of its models does. For a decision ask, it skips models without that capability. Plain unions and booleans do not require decision support.

For a plain boolean, a decision response becomes true only when the probability is greater than 0.5; exactly 0.5 becomes false. Change the threshold with typesafe({ threshold }), or use .withParams({ providerOptions: { threshold: 0.8 } }) for one ask. To apply your own decision rule, ask for Prob and inspect the number.

You can write the decision type immediately after ... These shorthand forms compile to the same requests as their longer equivalents:

sugar.tsi
const .ticket = "Charged twice for order #88.";
const dept = ask ..choice`Which team should handle this?`<{ billing: "Payments"; sales: "Pricing" }>;
const mood = ask ..scale`How frustrated is the customer?`<["Calm", "Civil", "Angry"]>;
const urgent = ask ..prob`Does the message express urgency?`;
const urgentWith = ask ..prob`Does the message express urgency?`<{ true: "Time pressure"; false: "None" }>;
console.log(dept.choice, mood.score, urgent, urgentWith);

The sugar needs the explicit ..: directly after ask, choice`…` reads as a tagged template. Any other word in that slot is NOLA1016; ..choice or ..scale without a type argument is NOLA2015.

A decision schema contains the answer’s structural schema and an x-nola-decision keyword describing the question and its criteria or levels. The provider uses that metadata to interpret the request. Validation checks that the selected label is allowed, the distribution sums to 1 and the score is within the scale. See Schema derivation.