What Are the TypeSafe Primitives in 2026?
TypeSafe's primitives are small, typed building blocks that come in pairs. A question defines one judgment for the model to make about a state, and its answer is the typed value that comes back. There are three question types in 2026: Choice, which picks one option from a set; Score, which rates the state on ordered levels you write; and Noul, which returns the probability that a statement is true.
Every question has an ID you choose, a type, and instructions. Choice and Score also take criteria, which define the options or the levels. Noul accepts criteria optionally, as a clarification of what yes and no mean. The ID is never sent to the model, so name it for your code.
Explain It Like I Am Five: Three Shapes of Question
Imagine you are making a quiz about a letter someone sent you. There are only three kinds of question you are allowed to write.
The first kind is tick one box. You write the boxes yourself: is this letter for the kitchen, the garage, or the garden? The model ticks exactly one. It also tells you how much it wanted to tick each of the others.
The second kind is where on the ruler. You draw a ruler and you label the marks: 0 is calm, 1 is grumpy, 2 is furious. The model puts a pin on the ruler. The pin can land between two marks, like 1.4, which means "quite a lot more than grumpy".
The third kind is yes or no, but with a twist. Instead of just saying yes, the model says how yes. 0.95 means almost certainly yes. 0.5 means it genuinely cannot tell.
That is all there is. Tick a box, point on a ruler, say how yes. Everything clever people build with this is just lots of those three questions asked at once, and then ordinary computer code adding them up.
The trick is writing small questions. "Is this letter angry?" is a good question. "What should I do about this letter?" is a bad one, because that is not a quiz question, that is a decision, and decisions belong to your program.
How Do You Choose Between Choice, Score and Noul in 2026?
Pick the type that matches the shape of the answer you need, and when two seem to fit, prefer the one your code can act on directly. TypeSafe frames it exactly that way: a Choice between refund, rebook and information maps straight onto three code paths, a Score of customer frustration maps onto a threshold, and a Noul maps onto an if statement.
| Type | Use it when | Documented examples | Maps onto |
|---|---|---|---|
| Choice | The answer is one of a known set with no order between the options | Routing a ticket to a department, classifying a document type, detecting a programming language | A branch per option |
| Score | The answer falls on a spectrum you can describe in steps | Bug severity, customer frustration, skill level | A numeric threshold |
| Noul | A clean yes or no where the probability itself is the signal | Does this contain personal data, is a refund being requested, does the resume mention distributed systems | A boolean after thresholding |
How Does a Choice Question Work?
A Choice takes criteria as a map where each key is an option name and each value describes that option. The answer returns choice, the option with the highest probability, probabilities across every option summing to 1, and confidence for the selected option. Both the option names and their descriptions are sent to the model, so write descriptions that separate the options from each other.
{
"state": "My running shoes arrived in the wrong size. Can I swap them for a size 10?",
"model": "jev-latest",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"returns": "Exchanges, wrong or damaged items",
"shipping": "Delivery status, delays, lost packages",
"billing": "Charges, invoices, payment problems"
}
}
}
}
Three configuration facts worth committing to memory in 2026. A Choice accepts up to 255 options, and adding options costs only a few tokens each, so give the model the full list of teams, categories or products rather than a shortlist. A description can be null when the option name is clear on its own. And you should add an other or none of the above option whenever the list might not cover every input, so the model can say that none of the others fit.
Reading a split Choice
The interesting answers are the ambiguous ones. TypeSafe documents a ticket that involves three teams: the department answer comes back as returns at 0.61 probability with billing at 0.35 because of a double charge, and the confidence lands at 0.42 to reflect the split. The documented code does not just take the top option. It routes to returns, sends billing a copy because its 0.35 share is over a 0.25 threshold, and asks the customer a clarifying question because a separate resolution answer came back at 0.20 confidence.
That is the habit to build. The full distribution is in the response for a reason, and a second option with meaningful probability is often actionable in its own right.
Structured Choice criteria
Start with a one-line description per option. When two options are similar and the model keeps confusing them, TypeSafe's documented fix is to describe each one with an object instead of a string, giving it fields for what the option covers, what belongs to a neighbouring option instead, and a few example inputs.
"criteria": {
"return_policy": {
"what": "Questions about the rules for returning an item",
"not_for": "Questions about an existing return already in progress",
"examples": ["How long do I have to return this?", "Do I pay return postage?"]
},
"return_status": {
"what": "Questions about a specific return already under way",
"not_for": "Questions about what the policy allows",
"examples": ["Where is my refund?", "Has my return been received?"]
}
}
The field names in that object are yours. TypeSafe is explicit that question, focus, what, not_for and examples are not part of the API and none are reserved. The model sees the names alongside the values, so use short names that label what follows.
How Does a Score Question Work?
A Score takes criteria as an ordered array of level descriptions, from the low end of the scale to the high end. A level's number is its position in that array starting at 0, so three entries are levels 0, 1 and 2. TypeSafe states that a Score should have at least two levels and that the API accepts up to 10.
{
"state": "The export button crashes the settings page in Safari. It works in Chrome, but a few of our customers only use Safari.",
"model": "jev-latest",
"questions": {
"bug_severity": {
"type": "score",
"instructions": "How severe is the reported issue?",
"criteria": [
"Cosmetic; no impact to functionality",
"Broken or degraded feature, but workaround exists",
"Blocking issue; no workaround exists"
]
}
}
}
The returned score is a position along your levels and can land between two of them, because it is computed as the probability-weighted sum: multiply each level number by its probability and add the results. TypeSafe's documented example returns a score of 1.43 with confidence 0.35 from probabilities of 0.0, 0.57 and 0.43, and 0 times 0.0 plus 1 times 0.57 plus 2 times 0.43 is indeed about 1.43. The answer also carries legend, which maps each level number back to the description you wrote, which is useful when logging.
Structured Score levels, and a warning about examples
When the model keeps scoring between two neighbouring levels on inputs you consider clear, TypeSafe's fix is to give each level an object with a field for what the level covers and a field of example situations, using the same field names on every level so the model can compare like with like. The documentation then publishes a table that is more honest than most vendor material, and it deserves reproducing.
| Level description style (same ticket) | score | confidence |
|---|---|---|
| Plain string, no examples | 1.43 | 0.35 |
| Object with a matching example: "export fails in one browser but works in another" | 1.03 | 0.96 |
| Object with an unrelated example: "search fails, but browsing categories still works" | 1.43 | 0.35 |
A well-chosen example concentrated nearly all the probability on one level. An unrelated example changed nothing. TypeSafe's own conclusion is the important part: higher confidence does not establish which answer is correct. Choose examples with known expected levels, then test the revised descriptions on separate inputs before keeping them. That is straightforward evaluation discipline, and it is rare to see a vendor write it down.
Splitting a judgment into several Scores
A judgment that depends on several things is best split into one question per thing, then combined in your code with weights you control. TypeSafe's documented ticket-priority example asks three Scores in one request and combines them.
def normalized(answers, question_id: str) -> float:
"""Put a score on 0 to 1 by dividing by its top level number."""
top_level = len(TRIAGE_QUESTIONS[question_id].criteria) - 1
return answers[question_id].score / top_level
severity = normalized(answers, "severity")
frustration = normalized(answers, "frustration")
report_quality = normalized(answers, "report_quality")
# A detailed report helps an engineer investigate, so it raises priority a little.
priority = 0.6 * severity + 0.3 * frustration + 0.1 * report_quality
The weights live in your code, visible and reviewable. TypeSafe's framing is that when the combined result does not match what your team would decide, you change the weights and run again rather than rewriting a prompt. This is the composite scoring pattern.
How Does a Noul Question Work?
A Noul asks a yes or no question and returns a single number from 0 to 1 that is the probability the answer is yes. Near 1 is a strong yes, near 0 a strong no, near 0.5 means the model gives yes and no similar probability. There is no separate confidence field, because a distribution over two outcomes is fully described by one number.
The documentation publishes recorded jev-1.13.0 answers for one question across several messages, and the middle of that table is where the design lesson sits.
| State | noul for "Is the customer asking for a human agent?" |
|---|---|
| Thanks, that fixed it! | 0.02 |
| How do I reset my password? | 0.07 |
| I need this sorted today, whatever it takes. | 0.26 |
| Are you a bot? | 0.40 |
| Is there any way to speak to someone about my invoice? | 0.84 |
| I have asked three times now. Can I please just talk to a real person? | 0.99 |
"I need this sorted today" is urgent but never asks for a person, so it scores 0.26. "Are you a bot?" hints at wanting a human without asking, and the model splits at 0.40. Those are precisely the messages a threshold has to adjudicate, and the number is telling you something real about the input rather than failing.
Where to put the threshold
TypeSafe's documented guidance is to set the threshold by the cost of being wrong. Use 0.5 when yes and no are equally easy to act on. Raise it when acting on a false yes is expensive, such as paging someone or issuing a refund. Lower it when missing a true yes is expensive, such as failing to flag a safety issue. Values in the middle can go to a person rather than to either code path.
wants_human = response.answers["is_human_escalation"].noul > 0.9
if wants_human:
route_to_agent(ticket)
else:
route_to_bot(ticket)
Writing a good Noul question
- One condition per question. "Is the customer angry and asking for a refund?" makes the model judge two things at once and the value means less. Ask two Nouls and combine them in code.
- Phrase it so a high value means yes. "Does the message contain personal data?" is clear. "Is the message free of personal data?" inverts the meaning and code will read it backwards.
- Make the boundary unambiguous. "Does this candidate have any Python experience?" works because "any" leaves no middle ground.
- Try a statement as well as a question. "The customer is requesting a refund" works as well as the interrogative form; test both on your data.
- Add
criteriaonly when the boundary is subtle. Instructions alone are enough for most Nouls. Try with and without and keep whichever performs better on your documents.
Noul is not a scale
This is the misuse TypeSafe warns about most directly. A Noul value runs from 0 to 1 but is not a scale of the thing you asked about; it is the probability that the answer is yes. The documentation compares a Noul and a Score on the same four candidates.
| Candidate | Noul: "Is the candidate strong in Python?" | Score: "How much Python experience?" |
|---|---|---|
| Experience in Java and Go; has not used Python | 0.03 | 0.0 (No experience) |
| Used Python occasionally for small scripts | 0.14 | 1.0 (Some familiarity) |
| Used Python daily for two years on data pipelines | 0.81 | 2.05 (Regular use in a job) |
| Python daily for eight years, maintains a large Django codebase | 0.92 | 2.89 (Deep expertise) |
You could invent bands in the 0 to 1 range in your code, but the model never saw them, so nothing in the answer was judged against them. The Score judged each level description on its own, so every candidate landed on or near a level you wrote. If the question is really about degree, use a Score.
How Do You Ask Several Questions in One Request?
Put every question that uses the same state in one request and mix the types freely. TypeSafe evaluates them in parallel, so adding questions barely changes the response time and costs only the tokens for the extra questions. The documented phrasing is blunt: asking a question you might not need is close to free.
That makes speculative questions the default rather than an optimisation. Ask everything your code might need, including answers that only matter for some inputs, and let the code decide which to use. TypeSafe's parallel questions cookbook measured a 13-question briefing as 12.2 times cheaper and 10.0 times faster in one call than in 13 calls, with no change in the answers.
How Do You Point a Question at Part of the State?
When the state is a JSON object with several parts, name the part in the instructions with a dot-and-index path to its key, including the backtick characters around the path. TypeSafe's documented example points a question at something like support.tickets[0].message. Explicit paths make it clear which part of a structured state should inform each judgment, which matters because Jev's documented weaknesses include indirection and distraction by irrelevant content.
When Should a Question Be Structured Rather Than a String?
TypeSafe allows structure in more places than most people notice, and its advanced page lists them precisely. Both instructions and every kind of criteria description accept a string, an object, an array, or null.
| Field | Applies to | Accepted shape |
|---|---|---|
instructions | Choice, Score, Noul | string, object, array, or null |
criteria values (option descriptions) | Choice | string, object, array, or null |
criteria entries (level descriptions) | Score | string, object, array, or null |
criteria.true and criteria.false | Noul | string, object, array, or null |
TypeSafe gives two reasons to reach for structure. It helps with clarity when a question has several parts, because the keys are labelled. And it helps when a question needs supporting data, because a schema, a taxonomy or a database row is already JSON, so you can pass the relevant subfields instead of serialising them into a string template.
"instructions": {
"potential_duplicate": {
"name": "John Smith",
"location": "Oakland, California",
"last_employer": "Google"
},
"question": "Is the resume for the same person as `potential_duplicate`?"
}
The question sits in one field and the data it refers to sits in others, referenced by name in backticks. Because that data comes from your code, it can change per request without touching the wording of the question.
When Does One Question Genuinely Depend on Another?
Rarely, and TypeSafe says so plainly: two requests are the exception, not the rule. Questions in the same request are independent, and one answer does not become context for another. A real dependency exists only when your code cannot build the second request until it has the first answer, because it needs that answer to fetch more data for the state, to decide what the state is made of, or to pick the next question's options.
The documentation names the three cookbooks that make a second request for a legitimate reason: skill suggestion ranks 182 skills in one request then fetches the full text of the top three and judges them again against better evidence; structure recovery asks whether each line break split a sentence, merges lines into blocks, then classifies blocks that did not exist until the first answer; and hierarchical classification uses each Choice answer to decide which options the next request offers.
If the second request's questions could have been asked against the original state, ask them in the first request and let the code ignore the ones it does not need. A second round trip costs latency that parallel questions do not.
What Are the Common Mistakes With Primitives in 2026?
- Using a Noul for a question about degree. The number is a probability, not a magnitude. Use a Score with levels you wrote.
- Hiding two judgments in one question. "Angry and asking for a refund" is two Nouls.
- Shortlisting Choice options. Up to 255 are allowed and each costs a few tokens. Give the full list.
- Forgetting an
otheroption. Without it the model must force every input into a box that may not fit. - Inverting a Noul. Phrase so that high means yes, always.
- Adding examples that do not look like your inputs. TypeSafe's own table shows an unrelated example changing nothing at all.
- Reading higher confidence as proof of correctness. The documentation says it is not.
- Making a second request out of habit. Unless the second question could not have been asked against the original state, it belongs in the first call.
Key Takeaways for 2026
- Three primitives: Choice for one of a set, Score for a level on your ordered rubric, Noul for the probability a statement is true.
- Choice accepts up to 255 options with optional
nulldescriptions; always include anotherwhen the list might not cover every input. - Score levels are 0-indexed, need at least two and accept up to 10, and the returned score is the probability-weighted sum across them.
- Noul has no confidence field because one number fully describes a two-outcome distribution; threshold it by the cost of being wrong.
- Both
instructionsand every kind ofcriteriaaccept strings, objects, arrays or null. Start with strings and add structure when two options blur. - Point questions at parts of a structured state with a backticked dot-and-index path.
- Ask everything in one request. Two requests are the exception, justified only when the second could not have been built without the first answer.
Distk helps teams write the questions, which is the part that actually determines whether this works, and put them in one reviewable file alongside the thresholds.