Blog

Sep 20, 2026

Decision model in the loop: LLM generates, Jev decides

A decision model returns a calibrated probability instead of prose. Four sites in Routines v1.6.0, every threshold named, five live calls for $0.000102.

A post on X where James McDonald asks someone to explain Jev to him like he is five years old, and Aaron Francis answers that if you ask your mom is the sky blue she says yes immediately, that is jev, and if you ask your dad why is the sky blue he talks for ten minutes, that is an LLM

That is a decision model in two sentences, from Aaron Francis replying to James McDonald on X. The rest of this article is the same distinction with the thresholds, the file names and the numbers attached.

A decision model answers a closed question with a calibrated probability and cannot write a sentence. Routines v1.6.0 moves four chat model verdicts onto one, Jev, and a fifth, the meeting folder pick, moved earlier in v1.5.15. Every threshold that acts on those probabilities is named below, and the whole live proof behind the numbers cost $0.000102.

Routines asks a chat model to do the writing. Four times we also asked it for a verdict: continue or stop, pass or fail, keep or drop, file here or nowhere. A fifth verdict, whether a finished output is worth a badge, was never a model call at all, only a keyword test. All five now go to a decision model that returns a calibrated probability and cannot write a sentence: four of them in v1.6.0, out now, and the folder pick earlier, in v1.5.15. The model is Jev, and it is TypeSafe AI's, not ours.

The five places we needed a verdict

  • Flow edge gate: does this flow continue down this edge.
  • Output double check: does this output meet its goal.
  • Meeting to-do owner: is this action item yours or theirs.
  • Meeting folder pick: which folder in your notes do these meeting notes go in.
  • Needs-attention badge: is this output worth a badge.

The first four are chat model calls shaped for parsing: tool-less, history-free, a JSON schema on the request, a salvage parser on the response. The gate and the double check run at the fast tier at temperature: Some(0.0), the to-do owner at 0.1, the folder pick a tier up at 0.2. That folder pick was the first to move: with a key stored and at least two candidate folders it answers from Jev before the chat request is built, and it shipped earlier, in v1.5.15. The fifth was never a model call. The badge is a keyword test, re-run on every React render, checking the text for "high-priority", "high priority" or "urgent".

You are an impartial reviewer. You are given the GOAL of an automated task and
the OUTPUT it produced. Judge ONLY whether the output fulfills the goal and is
internally consistent (dates, numbers, and claims do not contradict each other).
Respond with ONLY a JSON object of the exact shape {"pass": boolean, "reason":
string, "fix": string} where "reason" is one short sentence and "fix" is a
concrete instruction for what to change (empty string when pass is true). No
prose, no markdown.

The gate's older prompt told the model to answer continue when it could not tell, and so laundered every uncertain run into a clean green pass. We fixed it, and the doc comment now states the rule it keeps: "a gate that broke is never recorded as a clean verdict." The same comment names the site that still launders: "verify_output absorbs an unreadable grader result into a plain passed; flows must not." That one is live, deliberate and pinned by a test, because an unreadable verdict must never fail the run.

What a System One decision model is

Jev is TypeSafe AI's System One model, and the launch post is theirs to explain. The wire contract is a POST to https://api.typesafe.ai/v1/systemone under a Bearer key, carrying state, model and a map of questions whose keys are yours. A noul answers yes or no with one probability between 0 and 1. A choice picks one of a map of named options, with a probability for each and a confidence. A score places it on an ordered rubric of at least two levels and returns a score, a distribution, a legend and a confidence.

The needs_attention half of the badge's request, with the two criteria strings elided and the state stood in for. A how_soon score rides in the same call.

{
  "state": {
    "routine_name": "<the routine's name>",
    "output_excerpt": "<the first 800 characters of the output>"
  },
  "model": "jev-latest",
  "questions": {
    "needs_attention": {
      "type": "noul",
      "instructions": "Does this output tell the user about something of their own that needs action soon?",
      "criteria": {
        "true": "[...]",
        "false": "[...] names a company or product whose name contains such a word"
      }
    }
  }
}

This came back on a decoy we wrote to break the keyword test, usage and legend elided.

{
  "model": "jev-1.13.0",
  "answers": {
    "needs_attention": { "type": "noul", "noul": 0.03 },
    "how_soon": {
      "type": "score",
      "score": 0.08,
      "probabilities": { "0": 0.92, "1": 0.08, "2": 0.0 },
      "confidence": 0.88
    }
  }
}

A noul answer has no confidence field. Its only fields are its type and its probability, and the probability is the uncertainty. Jev cannot write text at all, so it replaces the parse and leaves the prose to the chat model. Laravel's AI SDK shipped the same shape on 2026-09-17 in PR laravel/ai#1010, as Boolean, Choice and Score, provider id typesafe, default model jev-latest. We mirrored its names.

The architecture: LLM generates, Jev decides, code controls

The LLM still writes. Jev returns a number. Code owns the threshold, a named constant you can read. When the number falls short, the answer routes somewhere that already existed: the chat path, or a person.

Architecture diagram of the decision model in the loop: an LLM generates, Jev decides, code applies thresholds, and a human catches the uncertain band, with the four shipped call sites as call-outs Figure 1. LLM generates, Jev decides, code controls, and the uncertain band routes to a path that was always there.

One decision, end to endA routine run asks the chat model for the output, then asks Jev one typed question about it, and the code compares the probability to a named constant before anything acts.JevRoutinesChat modelRoutine runJevRoutinesChat modelRoutine runalt[p >= ATTENTION_FLAG_MIN][below the line]alt[a decision key is stored][no key stored]do the workthe output, as prosedoes this need attention?build the state,ask one noulPOST /v1/systemone0.97actthe path that was always therethe path that was always there

Figure 2. One decision, end to end. With no key stored, the run takes the path it always took.

How we chose where to put it

Four blind finders swept the codebase by pattern, by subsystem, by data and by cost. We merged their candidates and measured volume against the app's own SQLite database. The formula is expected win times measured volume, divided by risk, both on a 1 to 5 scale, where risk is how badly and how silently a wrong answer lands, times how much surface the change touches.

Inventory funnel showing four finder angles merging into eighteen ranked candidates, split four implement now, eight later and six never Figure 3. The inventory funnel. The per finder counts were never recorded, so the figure names the four angles and counts only what the review doc actually holds.

The raw count before dedupe was not recorded either, so we put no number on it. What survived: 18 ranked candidates, 4 implement now, 8 later, 6 never.

The two candidates the brief itself seeded, the gate and the double check, both measured zero current volume: zero of 6 flow edges carry a condition, zero of 687 outputs carry a verify status. They shipped as ranks 1 and 2 anyway, each on its own stated forward assumption. The gate's: flow runs ran 2 per day from 2026-08-08 to 2026-09-03 and then stopped, so 2 runs a day on 1 gated edge is 2 evaluations a day. Grounded in history, but an assumption, and the review labels it as one.

On measured volume the order differs: 6.9 for the badge, 3.4 for the to-do owner, 5.0 for the gate on its assumed volume, 0.78 or 17.0 for the grader, both of those on assumed volume as well. The highest measured volume of the four, 10.27 completed routine outputs a day, belonged to the badge, which neither the brief nor half the finders led with, because it was a substring test and not a model call.

The four sites we shipped

Each has a named constant, a route for an uncertain answer, and the same no-key guarantee: the typed path does not exist and the old branch runs.

Flow edge gate. GATE_JEV_CONTINUE_MIN = 0.85. At or above it the flow continues and spends no chat call; below it, the evaluation escalates to the chat gate. Jev may never stop a flow.

Output double check. VERIFY_JEV_PASS_MIN = 0.85 on the score's normalized() value, plus VERIFY_JEV_CONFIDENCE_MIN = 0.6 when a confidence is measured. From VERIFY_JEV_CONCERN_MAX = 0.5 to 0.85 it escalates to the prose grader. Below 0.5 it escalates too, and marks a concern: that is the one band where an unreadable grader verdict is no longer absorbed into a pass.

Meeting to-do owner. TODO_OWNER_USER_MIN = 0.75 keeps the item, TODO_OWNER_DROP_MAX = 0.25 drops it, and between them it falls through to today's string verdict. The keep line sits below 0.85 on purpose: a wrong to-do dies in one gesture, a dropped one is a commitment nobody sees again.

Needs-attention badge. ATTENTION_FLAG_MIN = 0.85 raises the badge. Everything else answers nothing, the column stays NULL, and the keyword test runs as before.

The flow gate, before and afterBefore, a chat model wrote a verdict that had to be parsed and salvaged. After, Jev returns one probability and only a confident yes skips the chat call, so the branch a Mac without a key takes is the old one, unchanged.After, with a keyyesno

Jev returns
one probability

p >= 0.85?

continue, and
no chat call
at all

escalate to
the same
chat gate,
unchanged

Before, and still, with no keyyesno

Chat model writes
verdict plus reason

Parse the JSON

Salvage parser

Readable?

continue,
stop, or
unsure

unparseable,
the edge
policy
decides

Figure 4. The gate before and after. Only a confident yes skips the chat call.

Threshold band chart showing the act, escalate and fallback bands for each decision site on a zero to one probability axis with every named constant marked Figure 5. Every band, every constant. Nothing acts at or below 0.5, because on a two option question chance alone puts 0.5 on the winner.

The fifth band is that meeting folder pick, MIN_FOLDER_PICK_CONFIDENCE = 0.6, which shipped in v1.5.15. The four above it are what v1.6.0 added.

Tests pin the no-key guarantee. The decide tool registers only when the route finder locates a key, and the badge's pin asserts nothing was sent, so the stop happens before a request is built. This one pins the behaviour we criticised above.

#[tokio::test]
async fn with_no_key_an_unreadable_verdict_is_still_absorbed_into_passed() {
    // THE pin. Today an unreadable grader verdict delivers the run marked
    // "passed" with no note, and with no decision key that must not change
    // by a single byte.
    let (result, sends) = run_double_check(
        JevVerdict::Unjudged,
        &verify_long_output(),
        vec![Ok("totally not json".to_string())],
    )
    .await;
    let result = result.expect("a verdict was returned");
    assert_eq!(result.status, "passed");
    assert_eq!(result.content, verify_long_output());
    assert_eq!(result.note, None);
    assert_eq!(sends, 1);
}

The live numbers

The live proof is four ignored probe tests, one per site. The badge's probe loops over two cases, so four probes make five calls: $0.000016, $0.000019, $0.000028, $0.000020 and $0.000019, totalling $0.000102. Our own report said $0.000083, which is the first four of those five: it counted probes, not calls.

Bar chart comparing the per call cost of a chat model verdict against a Jev decision for the flow edge gate and the output double check, about 25 and 27 times cheaper Figure 6. Cost per call, re-derived from the token counts at $0.042 per million input tokens. Latency is not charted: nothing in this codebase times a model call.

At $0.042 per million input tokens with output free, the gate's estimated 3,250 tokens cost $0.000137 against $0.0034 for the chat verdict, 25 times cheaper; the grader's 3,560 tokens cost $0.000150 against $0.0040, 27 times. Those token counts are the finder's estimates, not measured request bodies, and the chat side is priced on the Haiku 4.5 fallback basis: this Mac runs a tier with no price row, so the app stores NULL, never a fabricated zero. At this Mac's measured volume for both sites, zero, the saving per day is exactly $0.00. Jev's published latency is 70 to 500 ms, the vendor's number and not ours.

Two probability bars side by side, 0.03 on a decoy naming a company UrgentIQ and 0.97 on a lease addendum, with the keyword test wrong on both Figure 7. The pair that made the case. The shipped keyword test is wrong in both directions; Jev is right in both.

The pair that made the badge's case ran against jev-1.13.0. The decoy: "UrgentIQ raised a 15M Series A this week. Analysts call the compliance gap it targets large, urgent and under-served." The keyword test badges it. Jev returns needs_attention 0.03 and raises nothing, because the question's own false criterion covers it: an output that "names a company or product whose name contains such a word" is not one that needs you. The second: "The landlord replied and needs your signature on the lease addendum before Friday or the renewal lapses." The keyword test says FALSE; Jev returns 0.97.

The needs attention badge, judged instead of matchedThe shipped keyword test raises a badge on a company called UrgentIQ and misses a lease addendum that needs a signature. Jev scores the first 0.03 and the second 0.97, and is right on both.

Industry news digest
names a company
UrgentIQ

keyword test:
finds urgent,
says YES

Jev: 0.03

a badge that
should not show

below 0.85,
no badge

Inbox triage: sign the
lease addendum
by Friday

keyword test:
no match,
says NO

Jev: 0.97

the badge is
missed entirely

clears 0.85,
badge raised

Figure 8. Both outputs through both paths, the substring test and the judge.

We got one precision wrong in review and caught it before it became a ticket. The same call also asks a how_soon score, and on the decoy that came back at 0.08 with 92 per cent of its mass on "no action is needed" and a confidence of 0.88. The 0.88 belongs to the score, never to the 0.03. A noul carries no confidence field. The score is logged, never acted on: output tokens are free, and a later retune of ATTENTION_FLAG_MIN will want this Mac's own distribution.

The laws we banked

  1. Nothing acts at or below 0.5 on a two option question: chance alone puts 0.5 on the winner.
  2. An "unsure" option inverts calibration: it makes uncertainty an answer and takes mass from the real outcomes, and with three options chance sits at 0.33.
  3. Options come from code, never from the state. A choice's winning option name is logged, which makes this load bearing.
  4. Never gate a tool-enabled agent behind a classifier.
  5. A noul has no confidence field. The probability is the uncertainty.
  6. Log the decision, never the state.
  7. Every uncertain answer needs a written route, and a transport error takes that route too.
  8. Keep arithmetic and hard rules in code. Jev judges meaning.

What we did not do, and what is next

Six candidates were marked never. The meeting chat intent router stays a router: law 4 is quoted from our own project memory, and failing open makes it a gate that fails safe, not a gate that stopped being one. The Slack access allowlist stays a hard rule, because a probabilistic access control is a defect by construction. Device sync conflict auto-merge stays manual: a wrong merge is silent, irreversible loss inside the user's own vault, and the conflict count is zero, so there is no calibration data either. Clipboard classifiers are out, because clipboards carry passwords and tokens. The hard rule matchers compare exact internally generated strings, several in hot loops where a network call is disqualifying. The managed wallet proxy is off limits by the brief's own rule.

Three things are next. The confident negative: the badge's judge never returns a confident no today, so one falls through to the keyword test and the bad badge shows. The decoy produced the calibration datum that was missing, and the column and the component already handle a false. We left it out of this pass, because changing decision semantics after an adversarial approval without a re-verify is how unverified behaviour ships. Thresholds tuned on our own data: every constant above is a judgment call, and the user rating column is NULL on every output row, so there is no labelled corpus yet. A pinned model version: jev-latest tracks stable and runs today; once tuned, the pin moves to jev-1.13.0, so a vendor update cannot move a threshold underneath us.

The routines decide verb and the decide tool shipped in v1.5.15, and the CLI reference documents the verb, including the TypeSafe or OpenRouter key it requires. The four sites above are live in v1.6.0. The TypeSafe key card is in Settings under Connections. An OpenRouter key sits on the AI provider settings, where it already is if you use one for chat. Either way the key lives in the macOS Keychain and never syncs between Macs. Without one, nothing about the app changes. With one, the text Jev judges is sent to that provider, and the decision log keeps the verdict, not the words: never the state text, no excerpt, nothing derived from it, with a test that fails if it does.

Routines is a free download for macOS, with a CLI and its command reference. One of the decisions above is visible from the outside: the needs-attention badge, on any routine you build from the inbox triage recipe.

Frequently asked questions

What is a decision model?

A decision model answers a closed question with a calibrated number instead of prose. Jev, TypeSafe AI's System One model, takes a state and a map of questions and answers in three shapes: a noul, which is a yes or no probability between 0 and 1, a choice, which picks one named option out of at least two and returns a probability for each, and a score, which places the state on an ordered rubric of at least two levels. It cannot produce free text and never returns a value outside the schema, so there is nothing to parse and nothing to salvage when the answer comes back. That is the whole difference from the pattern it replaces. A chat model asked for a verdict returns a sentence that happens to contain a verdict, and every caller ends up with a salvage parser underneath it. Laravel's AI SDK shipped the same three shapes in PR laravel/ai#1010, merged on 2026-09-17, under the names Boolean, Choice and Score, and Routines mirrors those names so the vocabulary matches what Laravel developers already read.

What is Jev, and who makes it?

Jev is a System One decision model made by TypeSafe AI, not by us. Routines is a client of it, the same way it is a client of any chat provider. The current model id is jev-1.13.0, and the aliases jev-latest and jev-preview both resolve there today. You reach it with a POST to https://api.typesafe.ai/v1/systemone under a Bearer key, and the request body carries exactly three required top-level fields: state, model and questions. Pricing is $0.042 per million input tokens with output free, the rate limits are 250,000 tokens per second and 1,200 requests per minute, and the context is 64k per request, of which 32k covers the state plus the single longest question. TypeSafe publishes a latency figure of 70 to 500 ms per answer. That number is the vendor's, and we have never measured it in this codebase, so read it as their claim rather than ours. There is a second route to the same model: OpenRouter exposes it as typesafe/jev-1.13 at POST https://openrouter.ai/api/alpha/decisions, with a 32000 token context.

How is a decision model different from asking a chat model for JSON?

A decision model cannot produce anything except the answer, so no parser sits underneath it. Asking a chat model for JSON gets you prose that is usually JSON. Each of the four verdict sites in Routines was written that way: tool-less, history-free, a JSON schema on the request, temperature pinned at 0.0 for the flow gate and the output grader, and a salvage parser on the response for the runs where the schema was not honoured. Jev returns a typed answer or an error, so the salvage layer has nothing left to do. The second difference matters more. A chat verdict is a word, and a word hides its own uncertainty, which is how the flow gate's older prompt came to instruct the model to answer continue whenever it could not tell, laundering every uncertain run into a clean green pass. A noul returns a probability, and the probability is the uncertainty, so the threshold lives in our code rather than in the model's temperament. GATE_JEV_CONTINUE_MIN is 0.85 and it is written in flows/gate.rs, where anyone can read it.

Does a noul answer carry a confidence score?

No. A noul answer's only fields are its type and its probability, and the probability is the uncertainty: 0.03 is a confident no, 0.97 is a confident yes, and 0.5 is a coin flip. Choice and score answers do carry a confidence field, and Routines reads it as optional, because a provider may not be able to measure how certain a distribution is. The distinction is easy to lose inside a single call. In one live probe of the needs attention question, the decoy output returned needs_attention 0.03 alongside a how_soon score whose confidence was 0.88. The 0.88 belongs to the score, never to the 0.03, and conflating the two was a defect we caught in review before it reached a ticket. The practical rule that falls out of this is that nothing in Routines acts at or below 0.5, because a noul is a two outcome question and chance alone puts 0.5 on the winner. That law is written in attention_flag.rs, and every threshold in the app sits above it.

What happens in Routines when there is no TypeSafe key?

Nothing changes. With no TypeSafe key at all, neither stored nor in the TYPESAFE_API_KEY environment variable, available_route in llm/decision.rs falls through to a stored OpenRouter key, and with neither one the route resolves to nothing, no request is ever built, and the decide tool is not registered at all. Every one of the decision sites then runs exactly the branch it ran before, byte for byte, including the branches we consider wrong. A Rust test pins it at commands/routines.rs:7047, with a name that states what it protects: with_no_key_an_unreadable_verdict_is_still_absorbed_into_passed. Its comment reads that with no decision key this must not change by a single byte. The reason the no key path is that literal is that a decision call is not free in time even when it is nearly free in money. An unbounded decide can cost about three and a half minutes of wall clock, because a 30 second request timeout can be paid four times with a retry backoff of 10, 30 and 60 seconds sitting between the attempts. That is why a typed pre-check is given a PRECHECK_BUDGET of 10 seconds instead, all of these in typesafe.rs. A Mac without a key should never pay any of it, and it never does, because it never enters the path.

Do my notes leave my Mac when Routines uses Jev?

Only if you add a typed decision key yourself, either a TypeSafe key or an OpenRouter key, which the same adapter accepts as a fallback when no TypeSafe key is stored. Without either one the route resolves to nothing, no request is built, the decide tool is not registered, and every decision site runs the branch it ran before. When you do add a key, the text Jev judges goes to whichever of the two providers that key belongs to, TypeSafe at api.typesafe.ai or OpenRouter at openrouter.ai, and nowhere else. The TypeSafe key card sits in Settings under Connections, with the key itself in the macOS Keychain under typesafe, and an OpenRouter key sits on the AI provider settings where it already lives for chat. Keys never sync between Macs. On our side, Routines logs the decision and never the state: llm/decision_log.rs says it must never log the state text, or any excerpt of it, or anything derived from it, and a test reads the source back and fails if a state parameter appears. A choice's winning option name is logged, which is exactly why the options are built in code and not from your content, and a score's label is not logged. The decide tool is read only, and draft mode never queues it.

How much does a Jev call cost compared with a chat model call?

Jev is priced at $0.042 per million input tokens with output free, so one decision costs a fraction of a cent. At the flow gate's estimated 3,250 input tokens that is $0.000137 per call against $0.0034 for the chat verdict it replaces, a ratio of about 25. At the output grader's estimated 3,560 tokens it is $0.000150 against $0.0040, about 27 times. Two honest caveats travel with those ratios. The chat side is priced on the Haiku 4.5 fallback basis in llm/pricing.rs, because the Mac these numbers come from runs a tier with no price row at all, where cost_usd returns nothing and the app stores NULL rather than a fabricated zero. And the saving is a rate, not a bill. At this Mac's measured volume for the gate and the grader, which is zero calls, the saving per day is exactly $0.00. The whole live proof behind this article was four probe tests making five calls, and it cost $0.000102 in total.

Why does the needs attention badge still show on a decoy output?

Because the model is allowed to raise that badge and is not yet allowed to lower it. attention_flag::judge raises the flag when the probability is at or above ATTENTION_FLAG_MIN, which is 0.85 in attention_flag.rs, and answers nothing for every other case. Nothing means the stored column stays NULL, and a NULL column leaves the original behaviour in place: a keyword test in src/components/homescreen/NeedsAttentionSection.tsx that is re-evaluated on every React render and looks for high-priority, high priority and urgent in the text. So a news digest that merely quotes the word urgent still collects the badge. The live probe shows exactly that gap. On the decoy output the model answered needs_attention 0.03 and put 92 per cent of the how_soon mass on the level that means no action is needed, yet the flag came back as nothing and the keyword test still said yes. Suppressing the badge on a confident negative is the named next step, and it was deliberately not shipped in this round, because changing decision semantics after an adversarial approval without a fresh verification is how unverified behaviour ships.

Related posts

Try it on your Mac.

Routines keeps your notes, transcripts, and routine outputs as markdown and SQLite on your machine, where they stay unless you turn on Cloud Sync.

Download