Atomic Questions: The Skill That Decides Whether Jev Works for You
Decomposition is the whole job. Broad questions hide judgments; atomic ones expose them so your code can inspect, tune, and combine them.
One skill, most of the outcome
TypeSafe's own build guide flags decomposition with an unusually strong claim. In the step on decomposing questions, it says this is probably the most important concept in the guide, and gives the reason in one sentence: broad questions hide several judgments behind one answer, while atomic questions expose those judgments so you can inspect, tune, and combine them in code.
That is the entire skill. Teams that get good results with jev are not writing cleverer prompts than teams that get bad results. They are asking smaller questions.
The reason is structural. Jev is a System One model: it makes the kind of judgment a knowledgeable person makes in a second, given the right context, and returns a typed answer. When you ask it something that requires three seconds of deliberation, it does not slow down and deliberate. It returns a confident-looking number produced by the wrong kind of cognition.
The canonical good and bad
The primitives page gives the two examples that anchor everything else:
"Does this message convey urgency?" is a good question. "Analyze this message and determine the best course of action" is not.
Look at what separates them. The first names one property, of one thing, with a yes/no answer. A person reading the message answers it instantly and would agree with other readers most of the time. The second contains an unbounded number of hidden sub-judgments: what the message is about, what options exist, what your business rules are, what the costs of each option are, and how to trade them off.
The second question is not a hard version of the first. It is a different category of work, and the docs name it as such: it needs slow reasoning, and that is a signal to break the task into small questions and compose the answers in code.
A test for whether a question is atomic
Three checks, in order of how often they catch something.
Count the conjunctions. "Is the customer angry and asking for a refund?" is two questions. The Noul docs are explicit: when a question has two conditions, the model has to judge both at once and the value means less. Ask two Nouls and combine them in code with an and. You get a cheaper model call, two inspectable numbers, and the ability to change the combining logic without touching a prompt.
Count the hops. If answering requires finding X, then finding a property of X, then judging that property, you have indirection, which is documented failure mode 4. Resolve the hops in code and point the question at the final value with a backtick path.
Ask whether you could write the rubric. If you cannot describe in a sentence what separates a yes from a no, or what level 2 means versus level 3, the model cannot either. That is not a prompting problem you can fix with more words. It means the judgment has more than one dimension in it and needs splitting.
Interactive
Question builder
match statement · returns choice + probabilities + confidence
sort key · returns an expectation, which can land between levels
if statement · returns one 0–1 value, no confidence field
{
"state": "Hi, I placed an order (#98423) last Thursday and was charged twice. I also can't log in after the site update, and adding Apple Pay would be really helpful. This is getting frustrating.",
"model": "jev-1.13.0",
"questions": {
"category": {
"type": "choice",
"instructions": "Determine the broad category of this support ticket",
"criteria": {
"bug_report": "The user is reporting something that is broken or producing errors",
"billing": "Charges, invoices, refunds, subscriptions",
"feature_request": "The user is requesting new functionality",
"account": "Login, permissions, profile, security"
}
},
"frustration": {
"type": "score",
"instructions": "How frustrated the user appears",
"criteria": [
"Calm, matter-of-fact",
"Frustrated but civil",
"Very angry"
]
},
"refund_requested": {
"type": "noul",
"instructions": "The user is explicitly asking for a refund or credit"
}
}
}Every question here is evaluated in parallel against the same state, so adding one barely moves your response time. That is what makes speculative fan-out cheap.
Worked example: spam detection
The build guide's spam example is the clearest illustration in the docs, because the bad version looks completely reasonable.
The state is a phishing email, structured as an object:
{
"message": {
"sender": {
"display_name": "Acme Payroll",
"email": "rewards@claim-bonus.example"
},
"subject": "Urgent: claim your employee bonus",
"body": "You have been selected for a $1,000 bonus. Confirm your payroll password today to receive it.",
"links": [
{ "text": "Claim bonus", "url": "http://claim-bonus.example/acme" }
]
}
}The broad version:
{
"is_spam": {
"type": "noul",
"instructions": "Is `message` spam?"
}
}This will probably return a high number on this particular email. The problem is not that it is wrong here, it is that you have no idea why it is right, no way to tune it, and no way to find out which signal failed when it is wrong on a harder case.
The decomposed version asks six questions, each about one observable property:
{
"requests_credentials": {
"type": "noul",
"instructions": "Does `message.body` ask the recipient to provide a password or other login credential?"
},
"offers_unexpected_reward": {
"type": "noul",
"instructions": "Does `message.body` claim the recipient received an unexpected prize, payment, or reward?"
},
"creates_time_pressure": {
"type": "noul",
"instructions": "Does `message.subject` or `message.body` pressure the recipient to act quickly?"
},
"sender_identity_mismatch": {
"type": "noul",
"instructions": "Does the organization named in `message.sender.display_name` conflict with the domain in `message.sender.email`?"
},
"link_domain_mismatch": {
"type": "noul",
"instructions": "Does the domain in `message.links[0].url` conflict with the organization named in `message.sender.display_name`?"
},
"disguises_link_destination": {
"type": "noul",
"instructions": "Does `message.links[0].text` conceal or misrepresent the destination in `message.links[0].url`?"
}
}Six properties you can name, each pointed at a specific field by path. Now the policy lives in Python where you can read it:
a = response.answers
signals = [
a["requests_credentials"].noul > 0.7,
a["offers_unexpected_reward"].noul > 0.7,
a["creates_time_pressure"].noul > 0.7,
a["sender_identity_mismatch"].noul > 0.7,
a["link_domain_mismatch"].noul > 0.7,
a["disguises_link_destination"].noul > 0.7,
]
# Credential harvesting plus any impersonation signal is enough on its own.
hard_block = signals[0] and (signals[3] or signals[4] or signals[5])
if hard_block or sum(signals) >= 3:
quarantine(message_id)
elif sum(signals) >= 2:
flag_for_review(message_id)Three things became possible that were not possible with is_spam. You can see which signals fired on any given message, so a false positive is debuggable in seconds. You can change the policy without touching a prompt, because sum(signals) >= 3 is a line of code. And you can weight signals differently for different mailboxes without re-tuning anything the model sees.
Note also what the atomic questions did to the adversarial surface. Is this spam? invites the email to argue that it is not spam, which is failure mode 6. Does message.body ask the recipient to provide a password? is much harder to talk your way out of, because it asks about an observable fact in a named field.
Decomposition is nearly free
The objection to splitting one question into six is cost and latency. Neither survives contact with how the API works.
Every question in a request is evaluated in parallel against the same state. The docs repeat this in nearly every primitive page: adding questions barely changes the response time and costs only the tokens for the extra questions, which are cheap. The parallel questions cookbook measures batching 13 questions into one call at 11.5x cheaper and 9.6x faster than 13 separate calls, with no change in the answers.
The pricing makes the asymmetry sharper. Per the Models page, jev-1.13 charges per input token, and output tokens are free. The state is sent once regardless of how many questions you attach. So the marginal cost of a seventh question is the tokens of that question's own text. Roughly nothing.
This inverts the instinct carried over from LLM prompting, where every extra request is real money and real latency. With jev, asking one broad question to "save a call" is strictly worse on every axis: accuracy, debuggability, cost, and speed.
Choosing the type for an atomic question
Once a judgment is atomic, picking the primitive is mostly mechanical. Prefer the type whose answer your code can act on directly.
Noul for a clean yes/no where the probability itself is the signal. "Does the resume state that the candidate has used Python at work?" Maps onto an if.
Choice for one option from a known set with no order between them. Routing, classification, language detection. Maps onto a branch. Add an other or none of the above option whenever your list might not cover every input, so the model can say none of these fit rather than being forced into the nearest wrong bucket.
Score for a position on a spectrum whose points you can describe. Severity, frustration, skill level. Maps onto a threshold.
The mistake worth naming explicitly, because the docs flag it directly: do not use a Noul to measure degree. "Is this candidate strong in Python?" returns the probability that the proposition "strong" is true. It is not a 0-to-1 skill scale. A value of 0.5 means the model gives yes and no equal probability, not that the candidate is mid-level. You can invent bands in your code, but the model never saw them, so nothing in the answer was judged against them.
The Noul docs make the contrast concrete with recorded jev-1.13.0 values on the same four candidates:
| Candidate | Noul: "strong in Python?" | Score: "how much Python experience?" |
|---|---|---|
| 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) |
The Noul values are not evenly spaced and were never meant to be. The Score landed each candidate on or near a level you wrote, which is what makes it tunable: if you disagree with a placement, reword the level and run it again.
If the judgment is really about degree, use a Score. If you need a yes/no, define the boundary so there is no middle ground. "Does this candidate have any Python experience?" works well precisely because "any" leaves nowhere to sit.
Write the instruction as if the ID does not exist
A small mechanical point that causes real bugs. Question IDs are keys you choose, and they are not sent to the model. The API reference states it plainly, and the primitives page repeats it as a tip: write the complete question in instructions, even when the ID seems self-explanatory.
This is easy to violate without noticing:
# The model sees only "Is it?" — the key tells it nothing.
questions = {"contains_pii": Noul(instructions="Is it?")}Less absurd versions of this show up constantly, where the ID carries half the meaning and the instruction carries the other half. The model only ever gets the second half.
When you actually need a second call
Decomposition means more questions per request, not more requests. The docs are firm that two requests are the exception: if the second request's questions could have been asked against the original state, ask them in the first request and let your code ignore what it does not need.
A second call is justified only when your code cannot build the second request until it has the first answer. Three real reasons: you need the 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 documented examples fit that test. The skill suggestion cookbook ranks 182 skills in one request, then fetches the full text of the top three and judges them again against that better evidence. The structure recovery cookbook asks whether each line break split a sentence, merges lines into blocks from those answers, then classifies blocks that did not exist until the first request had answered.
Everything else goes in one call.
The habit to build
When a judgment feels hard to phrase, that feeling is information. It means the judgment has parts. Name the parts, ask one question about each, and put the combining logic in code where you can read it, test it, and change it on a Tuesday without redeploying a prompt.
The payoff compounds. Weights and thresholds in code are inspectable and version-controlled. A composite built from six named signals tells you why it fired. And when priorities shift, you change a number rather than rewriting a paragraph and hoping.