Learning how to use Jev comes down to three question types, one method call, and a state object that holds whatever content you want evaluated. This guide walks a technically literate LMS admin or learning-ops lead through a first real request against typesafe-sdk version 0.7.1 (dated 22 September 2026), using training-content examples rather than generic support-ticket ones. By the end you will have working Python for Choice, Score and Noul, know how to read the response, and know exactly where Jev’s limits show up first.
If you have not installed anything yet, start with the Jev installation and setup guide. This post assumes the SDK is already on your machine and an API key is exported. If you are still deciding whether Jev belongs in your LMS stack at all, the pillar explainer on what Jev and the System One model actually are covers the concept before you write a line of code.
Every example below classifies, scores or checks real training content: course descriptions, module metadata, learning objectives. That is what most LMS integrations actually feed into Jev. We will also flag where it gets awkward, including a 32k input ceiling, unreliable counting, and a model that reads your instructions literally.
What do you need before you can call Jev?
You need three things: a TypeSafe account with an API key, the typesafe-sdk package (0.7.1, requiring Python 3.10+ or the Node 20+ SDK for JavaScript), and a piece of training content to test against, such as a course description or a batch of learner comments. Nothing else is required to send your first request.
Install the Python SDK or the JavaScript SDK, then export your key as an environment variable so it never appears in source code:
pip install typesafe-sdk # Python 3.10+ npm install @typesafe-ai/sdk # Node 20+ export TYPESAFE_API_KEY="sk-..."
The SDK is pre-1.0 (0.7.1 at the time of writing), so method signatures can still change between minor versions. Pin the version in your requirements file rather than tracking latest, especially once you start tuning confidence thresholds against a specific model build. For what a key costs, how seat and usage tiers work, and what free-trial limits look like, see the Jev pricing and access guide before you commit budget to a rollout.
How do you make your first Jev request?
A first Jev request needs a client, a state (the content to evaluate), and at least one question. The smallest complete example below checks whether a course module description contains a hands-on practice activity, using Noul, and prints the probability back to the console.
from typesafe_sdk import TypeSafeClient, Noul
client = TypeSafeClient(model="jev-1.13.0")
course_description = """
Module 4: Handling Escalated Support Tickets.
Learners review three real ticket threads, identify the
escalation trigger, and draft a de-escalation response.
A 10-question knowledge check follows the case studies.
"""
response = client.system_one(
state=course_description,
questions={
"has_practice_activity": Noul(
instructions="Does this module description include a hands-on practice activity, not just reading?"
)
},
)
answer = response.answers["has_practice_activity"]
print(answer.noul) # e.g. 0.93
print(response.model) # jev-1.13.0
print(response.usage.input_tokens, response.usage.output_tokens)
That is the whole shape of a Jev call: a client pinned to a model version, a state, a dictionary of named questions, and a response you index by question name. Everything else in this guide is a variation on that pattern.
What is “state” and how should you structure it?
State is whatever you want Jev to reason against. It accepts a plain string, an object, or an array, and it is text only. The hard limit is 32k tokens for state plus your longest single question, inside a 64k total context budget for the request.
In practice that means a full course outline with objectives, assessment items and a transcript summary usually fits comfortably; a raw SCORM package export or a full learner history dump usually does not. If your state is being assembled from records pulled through an HRIS-to-LMS integration, trim it to the fields the question actually needs before it reaches Jev, not after.
We tested this directly rather than guessing. Using this publication’s own 15 published and drafted posts, classified against a 5-family content taxonomy, we ran the same Choice question against three versions of state: lean (title and objectives only), full (every field), and bloated (every field plus irrelevant padding, such as fake enrollment counts and a generic instructor bio). Accuracy held flat at 73.3% across all three sizes. That is an honest null result on a sample of 15, not proof that padding never hurts. It just means our small test did not catch a penalty for it. TypeSafe’s own documentation warns that irrelevant context degrades decisions, so the safer default is still to structure state around what the specific question needs, not to assume bloat is free because one small test didn’t punish it.
Keep State Lean By Default
Build a per-question state template (objectives plus one or two supporting fields) instead of reusing one giant course object for every question; it is cheaper per call and easier to keep under the 32k ceiling as courses grow.
When do you use Choice?
Use Choice when the answer is exactly one option out of a defined set, such as routing content into a category. Criteria is a dictionary, not a list, mapping each option’s key to a short description of what qualifies for it.
The example below sorts an incoming training content request into one of five content families, mirroring the taxonomy we used in the state-size test above:
from typesafe_sdk import TypeSafeClient, Choice
client = TypeSafeClient(model="jev-1.13.0")
request_text = "We need a 20-minute module on the new expense reporting tool for finance staff before Q1 close."
response = client.system_one(
state=request_text,
questions={
"content_family": Choice(
instructions="Which content family does this training request belong to?",
criteria={
"compliance": "Mandatory, policy-driven, or regulatory training",
"onboarding": "New-hire orientation and role ramp-up content",
"technical_skills": "Software, systems, or tool-specific training",
"sales_enablement": "Product, pitch, or customer-facing skills",
"soft_skills": "Communication, leadership, or interpersonal training",
},
)
},
)
result = response.answers["content_family"]
print(result.choice, result.confidence)
Choice supports up to 255 options. A course catalog with a few hundred skill tags will hit that ceiling; at that scale, run a first-stage Choice to narrow into a handful of broad families, then a second Choice within the winning family, rather than listing every tag in one call.
When do you use Score?
Use Score when the answer sits on an ordered scale rather than in a category, such as how ready a course is to publish. Criteria is a list of 2 to 10 ordered levels, and the response is a fractional position between them, not just a bucket.
from typesafe_sdk import TypeSafeClient, Score
client = TypeSafeClient(model="jev-1.13.0")
module_notes = "SME sign-off received. Storyboard complete. Voiceover recorded but not yet mixed. QA review not started."
response = client.system_one(
state=module_notes,
questions={
"publish_readiness": Score(
instructions="How close is this module to being ready to publish?",
criteria=["Draft", "Needs SME review", "In production", "Ready for QA", "Approved for publish"],
)
},
)
result = response.answers["publish_readiness"]
print(result.score) # e.g. 2.6
print(result.legend) # maps back to the ordered levels
print(result.confidence)
A score of 2.6 tells a learning-ops lead something a bucket label cannot: the module is past “in production” but not yet at “ready for QA,” which is a more useful signal for a release dashboard than a rounded-off category. For more on where this fits into day-to-day L&D workflows, see Jev for learning and development.
When do you use Noul?
Use Noul for a single yes-or-no check, such as whether a module meets one specific requirement. It returns a probability between 0 and 1 and, unlike Choice and Score, has no separate confidence field; the probability itself is the signal.
from typesafe_sdk import TypeSafeClient, Noul
client = TypeSafeClient(model="jev-1.13.0")
objective_text = "By the end of this module, learners will be able to identify three signs of a phishing email and report them through the correct channel."
response = client.system_one(
state=objective_text,
questions={
"is_measurable_objective": Noul(
instructions="Is this written as a measurable learning objective with an observable action, not just a topic?"
)
},
)
print(response.answers["is_measurable_objective"].noul) # e.g. 0.97
Treat anything below roughly 0.6 as worth a human look rather than an automatic pass or fail. Noul is deliberately the simplest of the three primitives; reach for Choice or Score first if the real question has more than two possible answers hiding inside it.
How do you read the response?
Every system_one call returns one response object with an answers dictionary keyed by your question names, plus usage and model metadata. Which fields exist depends on which primitive answered.
r = client.system_one(state=state, questions=questions) r.answers["your_choice_question"].choice # selected option key r.answers["your_choice_question"].confidence r.answers["your_choice_question"].probabilities r.answers["your_score_question"].score # fractional position, e.g. 1.035 r.answers["your_score_question"].confidence r.answers["your_score_question"].probabilities r.answers["your_score_question"].legend r.answers["your_noul_question"].noul # 0-1 probability, no confidence field r.usage.input_tokens r.usage.output_tokens r.model
If you are wiring this into an existing integration, treat r.model as a value worth logging on every call, not just checking once. It is the cheapest way to notice that a console change or a silent default upgrade shifted you off the version your confidence thresholds were tuned against. Teams already comfortable with general API conventions can lean on the same habits covered in the LMS API documentation guide: log request and response metadata, not just the payload you care about.
Can you ask more than one question at once?
Yes. Pass multiple named questions in the same system_one call and Jev evaluates each one in parallel and in isolation against the same state, so one question’s answer never leans on another’s.
response = client.system_one(
state=course_description,
questions={
"content_family": Choice(instructions="...", criteria={...}),
"publish_readiness": Score(instructions="...", criteria=[...]),
"has_practice_activity": Noul(instructions="..."),
},
)
This is the pattern to reach for whenever a webhook event, such as a new module being submitted for review, needs several checks run at once instead of one call per check. If Jev is triggered from your LMS’s own event system, the LMS webhook integration guide covers the receiving side of that pattern. TypeSafe’s own documentation reports batching 13 questions in one call ran 12.2x cheaper and 10x faster than sending them sequentially, because the state (the expensive part of the request) is transmitted once instead of once per question. That is a vendor-run benchmark, not an independent one, so treat the multiplier as directional and re-check it against your own question count and state size.
Batch By State, Not By Course
Group every question you need answered about one piece of content into a single call before you group by course or by day; the savings come from not re-sending the state, so splitting questions across calls quietly erases most of the benefit.
What are the limits you will hit first?
The limits below are the ones that show up earliest in a real LMS integration, not edge cases. Most teams hit the state-size ceiling and the no-counting limitation within their first week of testing.
| Limit | What actually happens |
|---|---|
| State + longest question > 32k tokens | Request is rejected; trim state to the fields the question needs, or split into multiple calls. |
| Total context > 64k tokens | Hard ceiling across state plus all questions in the call combined. |
| Choice with more than 255 options | Not supported directly; use a two-stage Choice (broad category, then narrow within it). |
| Score with fewer than 2 or more than 10 levels | Rejected; collapse or split your ordered scale to fit the range. |
| Asking Jev to count items | Unreliable; count in code first, then ask Jev to evaluate the number you already computed. |
| Asking Jev to compare or sequence dates | Unreliable; pre-compute a field like days_since_last_update and enumerate options with Choice instead. |
| Pinning a model version from the console | Not possible; the console UI always points at the latest build, so version pinning has to happen in code, e.g. model=”jev-1.13.0″. |
What does Jev get wrong if you write questions badly?
Jev reads instructions literally. Negations, sarcasm, and implied conditions that a human reviewer would infer are taken at face value, so a question with an unstated exception will get answered as if that exception does not exist.
We measured this rather than asserting it. Running a criteria-wording test (T3) against the same 15-post set used for the state-size test, Jev’s tags agreed with human-assigned tags 73.3% of the time, with a mean confidence of 0.849. A middling agreement rate sitting next to a fairly high average confidence is itself a useful signal: the model was not hedging on the cases it got wrong, which points at ambiguous or overlapping criteria wording as the more likely lever than the model itself. Two categories that could plausibly both apply to the same content, or an instruction with an “unless” buried in the middle of a sentence, will produce confident wrong answers more often than uncertain ones.
The fix is almost always in how the question is written, not in switching primitives or models. For a deeper look at structuring instructions and criteria so overlapping categories and buried conditions stop causing this, see how to write Jev questions.
Conclusion
The fastest way to get value out of Jev in an LMS context is to start with one real question against one real piece of content, not a speculative batch of ten. Pick the check that is currently eating the most manual review time, whether that is routing incoming content requests, scoring modules for publish-readiness, or catching objectives that are not actually measurable, and wire up a single Choice, Score, or Noul call for it first.
Once that one call is reliable, expand it into a batched system_one request that answers several related questions about the same content in one round trip, and watch r.model and r.usage on every call so a version change never surprises your confidence thresholds later.