Skip to content

Anatomy of a decision

A risk model flags a card payment as suspicious. Should the system block the card right now, or send the case to a person? This page follows that one decision through the library from start to finish: what goes in, what is checked, what comes out, and what your service does with the result.

It is written for someone deciding whether to adopt the library and for the engineer who will wire it in. It assumes you know Python and your own service, and nothing about the mathematics behind the checks. Every number on the page comes from uv run grounded-decision examples fraud_review. Each section ends with a link to the page that goes deeper.

The whole path in one picture

flowchart LR
    E["Event<br/>suspicious"] --> S["Policy state<br/>intake → review"]
    S --> P["Proposed action<br/>block"]
    EV["Evidence<br/>numbers about the<br/>related cases"] --> C
    P --> C{"Five checks<br/>all must pass"}
    C -- "all pass" --> A["Verdict: act<br/>action = block"]
    C -- "any fails" --> H["Verdict: abstain<br/>block is held"]
    A --> R["DecisionRecord<br/>(saved as JSON)"]
    H --> R
    R --> RT["Routing in your service<br/>execute / human review / wait"]

In words:

  1. An event arrives. Your service names what happened, using a name the policy knows: suspicious or clean. The library does not compute this name. Your risk model, rule or upstream system does.
  2. The policy moves. The policy is a short chart of states and arrows (see Write a policy). Each entity, here a card, has a current state. The event moves it along one arrow: intake to review.
  3. The policy proposes an action. The state it lands in names what the policy would do next if nothing else were checked: block. This proposed action is always recorded, whatever happens next.
  4. Evidence is attached. Your service also sends a few numbers that describe the cases related to this one: here, the accounts that logged in from the same phones as the flagged account, and what their past fraud reviews found. This bundle is the evidence (EvidenceSnapshot).
  5. Five checks run. Each answers one yes/no question about the policy or the evidence. If any answer is no, the proposal is held.
  6. A verdict is returned. act (carry out the proposal), abstain (do not; hold it), or degrade (carry out a safer fallback the policy names).
  7. A record is produced. The DecisionRecord holds the verdict, the proposed action, the outcome of every check, the numbers each check used, and the entity's new state. You save it as JSON.
  8. Your service routes it. act executes. An abstain with a proposal goes to a person. An abstain with nothing proposed means the policy is waiting for its next event.

One worked example: two identical flags, two different answers

The fraud_review example sends the same event, suspicious on an $840 electronics purchase, twice. Only the evidence differs.

  • Dense ring. The flagged account sits among 16 accounts that cycle through 8 phones. 13 of them were confirmed as fraud in past reviews, and linked accounts mostly share the same outcome.
  • Thin chain. The flagged account is one of 4 accounts linked through a chain of 3 shared laptops, such as an internet cafe. Linked accounts barely predict each other's outcomes.

Here is what the library computed for each. The evidence numbers are filled in from raw (account, device) login rows by snapshot_from_graph (see Build evidence from your data).

Quantity Dense ring Thin chain What it means
Accounts in the neighbourhood (n) 16 4 How many related cases there are
Shared-device links 40 3 Lines between cases that share a device
Average links per account (degree, \(d\)) 5.0 1.5 How connected each case is
Neighbour agreement (lambda_hat, λ) 0.55 0.333 How much more often linked accounts share an outcome than chance; 0 means no better than chance
Evidence strength (\(d\lambda^2\)) 1.5125 0.1667 Must be above 1.05 for the related cases to count as informative
Link density (p_hat) 0.333 0.5 Fraction of possible links that exist
Density cut-off (p_threshold) 0.08 0.08 The density at which rings and innocent clusters are hardest to tell apart
Knife-edge half-width 0.209 0.5 How far from the cut-off the density must be to trust it; wider for small graphs
Unusualness p-value 1.0 1.0 Must be above 0.10; low means unlike recent confirmed cases

And the decisions:

Dense ring Thin chain
State after the event review review
Proposed action block block
Failed checks none neighbour agreement, knife-edge
Verdict act abstain
Action carried out block none
Routing execute the block human review, with the reason

The thin chain fails two checks. Its evidence strength, 0.1667, is far below 1.05: four accounts that barely agree tell you almost nothing about a fifth. Its density, 0.5, is only 0.42 from the cut-off of 0.08, and a graph of four accounts is so small that the library will not trust any density within 0.5 of the cut-off. A naive rule that blocks every suspicious event freezes this customer's card. The grounded module holds the block and sends it to a reviewer together with the reason:

d λ² = 0.1667 vs 1+0.050 (at or below KS; abstain); evidence density inside operational window (theorem width=4.39e+05, vacuous=True)

The unusualness check passes in both cases here

The example hard-codes the model's score for this payment at 0.10, below every one of the 40 calibration scores (0.40 to 1.18), so its p-value is 1.0 in both runs. That check does real work only when the candidate score is computed from your model. See Have we seen cases like this?.

The six checks, in one line each

Every decision runs all six, in this order. Each has its own page under The six checks.

Check (record field name) Question it answers Input Computed
Policy shape (certificate) Is the policy simple enough to accept? The policy text Once, at build time
Progress (game) Can the policy keep running from its start state? The policy chart Once, at build time
Neighbour agreement (reconstruction) Do related cases agree strongly enough to be informative? degree, lambda_hat Every decision
Knife-edge (threshold) Is the link density far enough from the cut-off to trust? n, p_hat, p_threshold Every decision
Unusual cases (conformal) Does this case look like recent cases where the answer was right? label_score, scores Every decision
Support (support) Do the related cases point the same way as the proposed action? support Every decision

The two policy checks rarely fail at decide time. A policy that is too complex or ambiguous is refused when you build the module, before any decision. The progress check fails policies that can loop forever without proposing anything; its page explains exactly what it does and does not catch. In practice, the four evidence checks decide most holds.

What the verdict does not tell you

Direction is checked only if you measure it

Which action to propose comes from the event your service sends; choose it with your model or rule. Only the support check compares the direction of the evidence with the proposed action, and only when the evidence carries a support share. In fraud_review it does: send clean with the same dense fraud ring and the module holds the pass, because only 3 of the 16 related accounts were cleared (support 0.19 vs minimum 0.50 (does not support the proposed action)). Without a support share, the same clean event acts pass: the related cases are informative and the density is stable, so every other check passes.

An act verdict therefore means "the policy proposed this action, and the evidence around the case was strong, stable, familiar and (if measured) pointing the same way." It does not mean the action is correct or that a theorem guarantees the outcome. See Act, abstain, degrade for what each verdict does and does not establish, and Trust levels and honest limits for how much mathematics stands behind each check.

Built once, decided per request

Some of the work happens once, when your service starts. The rest happens on every event. This split is why a decision is cheap: on the machine used for Limits and performance, the fraud module builds in about 9 µs and decides in about 7 µs.

When What happens Can fail how
Build, once per policy version Parse the policy text, check how deeply it nests, compile it to a chart of states, solve the progress check Raises an error for an illegal or oversized policy; a progress check that cannot be solved is recorded as failed
Per request Restore the entity's saved state, take one step on the event, run the four evidence checks, read the cached policy checks, build the record Never raises for weak evidence or unknown events; it abstains and says why
After the request Save the record and the entity's new state Your database

A Module holds the current entity's state while it decides, so one instance must not serve two requests at the same time. Restore the state under a lock, or give each worker its own module. State and policy versions covers this, and the pitfall that editing a policy renumbers its states.

Where to go next