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.
What if I don't have a graph of related cases?¶
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.