Noul (yes/no) questions
TL;DR
{"type": "noul", "instructions": "<yes/no question or statement>"}, with an optionalcriteria: {"true": "...", "false": "..."}. The answer is{"type": "noul", "noul": <0..1>}— the probability that the answer is yes. There is noconfidencefield on a Noul answer. Thresholdnoulin your code when you need a boolean.
When to use / when not to use
Use a Noul when the answer is yes or no: does this message ask for a refund, does this resume mention distributed systems, does this comment contain personal data.
- If the answer is one of several options → Choice questions.
- If it's a position on a spectrum → Score questions.
- Comparison of all three: Choosing between Choice, Score, Noul.
Example questions from raw/docs/primitives__noul.md and raw/docs/primitives.md:
"Is the customer requesting a refund?"
"Does this resume mention experience with distributed systems?"
"Does the message contain personally identifiable information?"
"Is the customer asking for a human agent?"
Probability-that-yes semantics
A Noul answer is a single number, noul, the probability that the answer is yes. It ranges from 0 to 1.
- Near 1 → a strong yes.
- Near 0 → a strong no.
- Near 0.5 → the model gives yes and no similar probability.
Phrase the instruction so that a high probability means "yes", so the returned answer is unambiguous in its meaning. Most often you will threshold noul into a boolean when your code needs a hard decision.
Why there is no confidence
Upstream now states the reason explicitly (raw/docs/primitives__noul.md, 2026-09-21): "A Noul's probability distribution has only two outcomes, yes and no, so the single noul value describes it completely." A Choice or Score spreads probability over several options or levels, and confidence summarizes that spread; there is nothing to summarize for a two-outcome distribution. The number is the answer and the certainty in one.
0.5 is not "medium"
0.5 does not mean a medium amount of the thing you asked about. A Noul value runs 0 to 1, but it is not a scale of the thing asked about — it is the probability the answer is yes. "Is the candidate strong in Python?" asked as a Noul, against a four-level Score ("no experience / some familiarity / regular use in a job / deep expertise"), on the same four resumes:
| Candidate | Noul: "Is the candidate strong in Python?" | Score: "How much Python experience does the candidate have?" |
|---|---|---|
| My experience is in Java and Go. I have not used Python. | 0.03 | 0.0 (No experience) |
| I have used Python occasionally for small scripts alongside my main Java work. | 0.14 | 1.0 (Some familiarity) |
| I used Python every day for two years in my last job, mostly data pipelines. | 0.81 | 2.05 (Regular use in a job) |
| I have written Python daily for eight years, including maintaining a large Django codebase. | 0.92 | 2.89 (Deep expertise) |
You can carve the 0–1 range into bands in your code, but the model never sees them, so nothing in the answer was judged against them, and the spacing between candidates is not something you chose. The Score judges each level description on its own, so every candidate lands on or near a level you wrote. If what you want is a measurement rather than a decision, that is a Score questions, not a Noul. See Confidence vs probability for how Choice and Score confidence differs from a Noul probability.
Request contract
| Field | Required | Type | Description |
|---|---|---|---|
type |
Yes | "noul" |
Must be "noul". |
instructions |
Yes | string | object | array |
The yes/no question or statement to evaluate. |
criteria |
No | object with true / false |
Optional { true, false } descriptions clarifying what a yes and a no mean. |
Per raw/docs/api.md, criteria.true is "What a yes (value near 1) means" and criteria.false is "What a no (value near 0) means". As of the 2026-09-21 api.md, both are typed string | object | array (the OpenAPI NoulCriteria schema also allows null) — see Structured instructions, options, levels, criteria. instructions is likewise string | object | array: start with a string, and use an object when the question needs data alongside it, such as a record to compare the state against, or when part of the question is built by your code (see Structured instructions below).
In the JavaScript SDK both parameters are optional: noul(instructions?, criteria?), with instructions defaulting to null (JavaScript/TypeScript SDK: install, client, choice/score/noul).
Example request — one Noul with criteria, one without
{
"state": "I have asked three times now. Can I please just talk to a real person?",
"model": "jev-latest",
"questions": {
"is_human_escalation": {
"type": "noul",
"instructions": "Is the customer asking for a human agent?"
},
"is_repeat_contact": {
"type": "noul",
"instructions": "Has the customer contacted support about this before?",
"criteria": {
"true": "Mentions a prior attempt, ticket, or that they have asked before",
"false": "No sign of any previous contact"
}
}
}
}
Response
{
"model": "jev-1.13.0",
"answers": {
"is_human_escalation": {
"type": "noul",
"noul": 0.99
},
"is_repeat_contact": {
"type": "noul",
"noul": 0.93
}
},
"usage": {
"input_tokens": 360,
"output_tokens": 39
}
}
A Noul answer carries only type and noul. There is no probabilities map and no confidence. Both answers here are close to 1: the customer says "talk to a real person", so is_human_escalation is 0.99; "I have asked three times now" matches the true description of is_repeat_contact, so it is 0.93.
Python SDK
Verbatim from raw/docs/primitives__noul.md (note NoulCriteria, not a plain dict — the upstream page now shows the typed helper):
from typesafe_sdk import Noul, NoulCriteria, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
model="jev-latest",
state="I have asked three times now. Can I please just talk to a real person?",
questions={
"is_human_escalation": Noul(
instructions="Is the customer asking for a human agent?",
),
"is_repeat_contact": Noul(
instructions="Has the customer contacted support about this before?",
criteria=NoulCriteria(
true="Mentions a prior attempt, ticket, or that they have asked before",
false="No sign of any previous contact",
),
),
},
)
print(response.answers["is_human_escalation"].noul)
print(response.answers["is_repeat_contact"].noul)
Reading the number against real messages
Recorded jev-1.13.0 answers to is_human_escalation for different customer messages (raw/docs/primitives__noul.md):
| State | noul |
|---|---|
| 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 (0.26). "Are you a bot?" hints at wanting a human without asking for one, and the model splits almost evenly (0.40). Those are the messages a threshold in your code has to decide.
Writing a Noul question
- One yes/no question per Noul. "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 that reads it later will get it backwards.
- A statement works as well as a question. For "The customer is requesting a refund", a value near 1 means the statement is true. Try both phrasings on your own data.
- Make the yes/no boundary unambiguous. "Does this candidate have any Python experience?" works because "any" leaves no middle ground.
- Optional
criteria. The instruction is enough for most Nouls; when the boundary is subtle, addtrueandfalsedescriptions. Try your questions with and withoutcriteriaand keep whichever gives better answers on your documents. - Keep
truemeaning yes. Per Jev 1.13 jaggedness: known failure modes, a Noul wheretruemaps to "no" andfalsemaps to "yes" performs worse. Treat the criteria as an extension of the instruction and align the two. - Ask many Nouls per call. For a checklist of conditions, ask one Noul per condition in one request and let the code decide what the combination means. Questions are evaluated in parallel, so "adding Nouls barely changes the response time" (raw/docs/primitives__noul.md) — see Speculative fan-out.
Structured true/false objects (a definition plus examples on each side) are shown in Structured instructions, options, levels, criteria and in Writing instructions and criteria that Jev reads correctly.
Structured instructions
instructions can be an object with the question in one field and data it refers to in the others; the question names the data field in backticks. Upstream's worked example compares an arriving resume (in state) against candidate records that might be the same person — one Noul per record, all in one request, with the question ids generated from the database IDs:
{
"type": "noul",
"instructions": {
"potential_duplicate": {
"name": "Jon Smith",
"location": "Oakland, CA",
"last_employer": "Google"
},
"question": "Is the resume for the same person as `potential_duplicate`?"
}
}
Recorded answers: same_as_record_18 (name spelled differently, same location and employer) 0.74; same_as_record_42 (same name, different city and employer) 0.09; same_as_record_77 (similar name, same location, different employer) 0.08. Threshold each value in your code and send the middle values to a person.
Building the questions from records (verbatim, raw/docs/primitives__noul.md):
from typesafe_sdk import Noul, TypeSafeClient
SAME_PERSON = "Is the resume for the same person as `potential_duplicate`?"
def duplicate_questions(candidates: list[dict]) -> dict[str, Noul]:
"""One Noul per candidate record, all asking the same question."""
return {
f"same_as_record_{candidate['id']}": Noul(
instructions={
"potential_duplicate": {
"name": candidate["name"],
"location": candidate["location"],
"last_employer": candidate["last_employer"],
},
"question": SAME_PERSON,
},
)
for candidate in candidates
}
def find_duplicates(resume: dict, candidates: list[dict]) -> list[str]:
with TypeSafeClient() as client:
response = client.system_one(
model="jev-latest",
state={"resume": resume},
questions=duplicate_questions(candidates),
)
return [
question_id
for question_id, answer in response.answers.items()
if answer.noul > 0.7
]
Cookbook: SDE cascade uses the same shape to verify an extracted record: every field gets the same battery of questions, with the question text in main_question and per-field field_spec and extracted_field properties.
Using the number in code
Pick the threshold from the cost of being wrong (raw/docs/primitives__noul.md): 0.5 when yes and no are equally easy to act on; higher when acting on a false yes is expensive (paging someone, issuing a refund); lower when missing a true yes is expensive (failing to flag a safety issue). Values in the middle can go to a person instead of either code path — the same three-way split Confidence vs probability describes for Choice and Score. Upstream's worked routing example uses YES = 0.8 and NO = 0.2, sends anything between them to review, and routes the rest.
The threshold is yours to pick and belongs in your code, not in the prompt. Two cautions from Jev 1.13 jaggedness: known failure modes:
- Don't carry a threshold tuned on a Noul over to a Choice. A Choice over options is relative (which option wins), while each Noul is absolute and can be low for all of them.
- Don't expect arithmetic identities between separate questions. On the ticket "I was charged twice for the same order. Can someone look into this?", the question and its negation asked as two Nouls returned:
refund |
not_refund |
Sum |
|---|---|---|
| 0.72 | 0.47 | 1.19 |
P(noul) and 1 - P(not noul) are not directly comparable.
For a counting use case, ask one Noul per item and add up the thresholded answers in code rather than asking for a count — the worked snippet is in Jev 1.13 jaggedness: known failure modes.
Gotchas
- No
confidencefield exists on a Noul answer; a value near 0.5 is the only "uncertain" signal you get. - 0.5 means split probability, not a medium quantity.
- Vague predicates ("strong", "important", "recent") make the number uninterpretable. State the exact condition ("Does the resume state that the candidate has used Python at work?").
- A Noul and a yes/no Choice on the same text can disagree sharply. Example from Jev 1.13 jaggedness: known failure modes on "I'm not happy with the fit. What are my options here?": Noul
noul0.22 versus Choiceprobabilities["yes"]0.01 withconfidence0.97.
Related
- Primitives: Choice, Score, Noul — the three types and how to batch them
- Choice questions, Score questions — the other two primitives
- Choosing between Choice, Score, Noul — decision table
- Writing instructions and criteria that Jev reads correctly — phrasing and negation pitfalls
- Structured instructions, options, levels, criteria — structured
true/falsecriteria - Confidence vs probability — why Noul has no separate confidence
- HTTP API: POST /v1/systemone and GET /v1/models — wire contract
- Cookbook: Classifying RAG passages — Noul as a relevance filter
Sources
- raw/docs/primitives__noul.md (https://docs.typesafe.ai/primitives/noul)
- raw/docs/api.md (https://docs.typesafe.ai/api)
- raw/docs/primitives.md (https://docs.typesafe.ai/primitives)
- raw/docs/model-jaggedness__jev-1.13.md (https://docs.typesafe.ai/model-jaggedness/jev-1.13)