Skip to content

FAQ

These are the questions people ask first when deciding whether this library is for them, with short, direct answers. Each answer links to the page that explains it fully. If a word is new, the glossary defines it.

What is this, in one paragraph?

Your model or rules propose an action (block this card, discharge this patient). Before that action is carried out, this library asks six yes/no questions: is the policy simple enough to analyse, can the workflow keep going, do the related cases agree strongly enough to be informative, is the evidence clear of the knife-edge around your cut-off, has anything like this case been seen recently, and do the related cases point the same way as the action. If all six say yes, it acts; if any says no, it holds the decision and records what it would have done and why. Start with Why a score isn't enough.

Isn't this just a second threshold?

No. A second threshold looks at the same score again. The checks here look at different things: how many related cases there are and how much linked cases agree (a score of 0.97 backed by two loosely linked cases is not the same as 0.97 backed by eighty tightly linked ones), whether the density of links sits so close to your cut-off that noise flips it, whether this case is unusual compared with recent ones, and whether the related cases carry the outcome the action implies. Each check also writes its reason into the record. See Naive vs grounded for side-by-side runs.

Do I have to know the thresholds up front?

No. The settings of the four data checks (how strong the related cases must be, how wide the knife-edge window is, how unusual a case may look, how many related cases must point the same way) and the calibration set are learned from your labelled history: past decisions with the evidence you had, the action proposed, and whether it turned out right. uv run grounded-decision learn history.jsonl --max-risk 0.05 picks the settings that act most often while certifying, with 95% confidence, that at most 5% of acts are wrong, and writes a provenance record. What stays with you has a plain meaning: the error rate you accept, and what counts as a related case and a correct outcome. The shipped defaults are only starting points. See Learn parameters from your history and Where every number comes from.

How much history do I need?

It depends on the error rate you want to certify and how often the service acts. Even with zero mistakes, certifying at 95% confidence with the default fixed-sequence procedure needs at least 59 acted-on cases in the risk split for a 5% target, 149 for 2% and 299 for 1%; with some mistakes, more (153 at 5% if half the target rate is wrong). The risk split is half the history, and only the cases the service would act on count, so at a 20% act rate a 5% target needs roughly 1,500 labelled cases to leave room for errors. If there is too little, the learner says so, says how many are needed, and returns settings that hold everything. See Learn parameters: step 2.

Does a pass mean the evidence supports my action?

Only if you measure support. Three of the data checks ask whether the related cases are informative, settled and familiar; none of them looks at which way the evidence points. The sixth check, support, does: it requires that at least half (by default) of the related cases carry the outcome the proposed action implies.

In the fraud example (uv run grounded-decision examples fraud_review) the third event is clean, arriving with the same strong evidence as the first: a 16-account device ring, evidence strength dλ² = 1.513 (above the 1.05 bar), 13 of the 16 accounts confirmed fraud. The first event's block acts (support 0.81). The clean event's pass is held: support 0.19 vs minimum 0.50 (does not support the proposed action).

That works because the example tells the library which outcome each action implies (block needs confirmed fraud, pass needs cleared accounts) when it builds the evidence. If your evidence carries no support share, the support check passes with support not measured, and a pass means only that the evidence was strong, settled and familiar; the direction then comes entirely from the event you send. Even with support measured, a pass is not a promise that the action is right; the error rate you can rely on is the one learning certifies. More: Do the related cases support this action?.

Do I need Lean?

No. Lean is a proof assistant used, separately, to check some of the mathematics this library relies on. Nothing in the package runs Lean at build time or decide time, and it is not a dependency. The trust field on each check (lean, paper, operational) only records how much of a proven result that check relies on. See Trust levels and honest limits.

Does it work with my model?

Yes, if you can do two things. First, turn your model's output into an event name (suspicious when the fraud score is above your cut-off, clean otherwise). Second, describe the related cases as numbers: how many there are, how they are linked, whether linked cases share a label, and optionally an unusualness score for this case and for recent good cases (for example 1 − model confidence). The library does not read features or call your model. See From your data to evidence.

Then the neighbour-agreement and knife-edge checks have nothing to measure, and the library is the wrong tool for those decisions. You can still build a graph from things cases share (a device, an address, a symptom, a reporter); Build evidence from your data shows how. Passing made-up numbers to get past the checks defeats the point.

How small can my graph be?

Below about ten related cases, almost every decision is held. The knife-edge check holds a decision when the link density is within a half-width of your cut-off, and that half-width grows as the number of cases shrinks: 0.5 (every density) at 6 or fewer, 0.30 at 10, 0.12 at 40, 0.08 at 80. The neighbour-agreement check also needs each case to have enough links. The full table is on Defaults and limits. If your groups are routinely tiny, expect holds and route them to people.

What's the latency?

Building a module (parsing the policy, compiling it and solving the progress check) happens once, at start-up. Each decision is one step through the policy plus four arithmetic checks. Measured on an Apple M-series laptop with Python 3.12, for a three-state policy and a calibration set of 200 scores:

Step Time
Module.from_expr(...) about 9 ms for the first build in a process (it loads the policy grammar), under 1 ms after that
module.decide(...) about 10 µs
record.to_dict() about 3 µs

Reproduce it with:

import time
from grounded_decision import Event, EvidenceSnapshot, Module, parse

module = Module.from_expr(
    "fraud", parse("test(suspicious) ; act(block) | test(clean) ; act(approve)")
)
evidence = EvidenceSnapshot(
    n=80,
    p_hat=0.35,
    p_threshold=0.08,
    degree=8.0,
    lambda_hat=0.55,
    scores=tuple(i / 200 for i in range(200)),
    label_score=0.3,
)
start = time.perf_counter()
for _ in range(20_000):
    module.reset()
    module.decide(Event("suspicious"), evidence)
print((time.perf_counter() - start) / 20_000 * 1e6, "µs per decide")

In a service, building the evidence (querying your graph) and saving state will dominate. Limits.decide_ms_budget (50 ms) is a target that a test checks on the library itself, not a timer in your service. More: Limits and performance.

Is the Module thread-safe?

No. A Module holds the current state of one entity and decide changes it. Either keep one module per worker, or keep one shared module and, under a lock, restore the entity's saved state, decide, and read record.extras["state"] before releasing the lock. The reference service (make service, source in src/grounded_decision/examples/reference_service.py) shows the second pattern. See Integrate into a service.

What happens to saved states when I edit the policy?

States of a text policy are numbered s0, s1, ... by the compiler, and an edit can renumber them, so a saved s2 may mean something else afterwards. Store a fingerprint of the policy next to each saved state and refuse to restore across a change, as the reference service does (HTTP 409 policy_version_mismatch). See State and policy versions.

What if I have no calibration scores?

The learner builds them from your labelled history, per proposed action (Learn parameters). Without them, by default the unusual-case check then passes (its p-value is 1), so it is effectively switched off. Set Limits(require_conformal_calibration=True) in production so an empty calibration set holds decisions instead. With the default α = 0.10 you need at least 9 calibration scores before the check can ever fail. See Have we seen cases like this?.

Does the "can the workflow keep going?" check catch stuck workflows?

Some of them. It is solved once at build time. A policy written as text fails it when it can loop forever without proposing anything, such as (test(ping))*. A hand-built chart fails it when, from the start, every way it can run ends up circling among states you marked with an odd priority, assuming the worst in states you marked environment=True. It assumes your service sends the event that keeps the workflow moving in the states it controls, and a chart left at priority 0 everywhere passes whatever its shape. An event that arrives out of order is not handled and leaves the entity where it is, so it cannot strand it. Test the workflows you care about with replays as well. See Can the workflow keep going?.

Why did it abstain?

Read record.reason: it joins the one-line summaries of every failed check, or says event '…' is not handled in state '…'; nothing proposed when the event has no arrow out of the current state, or no action marked on current state when the policy moved to a state that proposes nothing. Troubleshoot: why did it abstain? maps each message to a fix.

Can I explain a hold to a non-technical person?

Yes. record.proposed_action says what was about to happen, and each failed check has a plain question: "Do the related cases agree?", "Are we on a knife-edge?", "Have we seen cases like this?", "Do the related cases support this action?". For example: "We were about to discharge this patient, but only five loosely linked past visits resemble them and their outcomes were mixed, so a nurse should decide." Every field you can draw on is listed on The decision record.

Is there an HTTP service I can copy?

Yes. make service runs a reference service with persistence, locking, idempotency and policy fingerprints. The tutorial is Build a decision service; the function to call from your own handler is handle_request(module, body).

What Python versions are supported?

Python 3.12 and newer (requires-python = ">=3.12" in pyproject.toml). See Install and Stability and versioning.

When should I not use this?

  • You have no related cases. The data checks need a graph of linked cases; without one there is nothing to measure.
  • Your groups are tiny. Under about ten related cases, nearly every decision is held.
  • You need the library to choose the direction. It decides whether to act on a proposal, not which proposal is right; the support check can hold a proposal the related cases contradict, but it does not pick another one.
  • You cannot afford holds and have no one to route them to. Holding is the library's answer to weak evidence; if nobody reviews held cases, they pile up.
  • You need hard real-time guarantees. Decisions are fast but not timed or interrupted.
  • You want an error-rate guarantee without labelled history. The certified error rate comes from learning the settings on past decisions with known outcomes; with only the shipped defaults, no error rate is promised. The certificate also assumes future cases come from the same process as the history. See Learn parameters from your history and Trust levels and honest limits.