A scenario is the set of rules that evaluates one event type. You build rules in the UI; this page covers the building blocks and how they fit together.

Trigger condition

The pre-filter deciding which events the scenario runs on at all. If you only want to evaluate sales and cancellations, everything else is excluded here without writing a rule, and comes back as Unknown.

Rule = condition tree + score

Every rule is a condition tree: fields, comparisons, and nested AND/OR combinations of them. If the tree evaluates true, the rule’s score is added to the total.

Operators

equal, notequal, contains, notcontains, startswith, endswith, isempty, isnotempty, in (in list), notin, similarity.
equal, notequal, lessthan, lessthanorequal, greaterthan, greaterthanorequal, between, notbetween.
Comparisons plus ready-made windows: last24hours, lastnhours, last7days, last90days, lastndays, thismonth, lastmonth and their forward-looking counterparts.
matchesregex, notmatchesregex — matching with a regular expression. For the conditions contains cannot express: a card number shape, a disposable-mail domain family, a phone prefix.Every match runs under a time limit; a pattern that exceeds it counts as not matching and is recorded on a counter. Publishing checks that the pattern is valid.
Word boundaries (\b, \y) are refused. The in-memory and SQL evaluators read them differently, so the same rule would answer differently once aggregated. Write the boundary out instead: (^|[^a-z])word([^a-z]|$).
Instead of a value you can pick another field: billing country against shipping country, cardholder name against account name, order total against line total.Toggled beside the value in the editor. If the referenced field is absent from the payload the condition does not match — including for the negative operators, so a rule does not fire precisely when the data needed to judge it is missing.
ipgeolocation, ipdatacenter, emaildisposable, emailfreeprovider, emailnomx, phoneinvalid, phonecountry, phonelinetype, uabot, uadevicetype, sslcertcheck, whoislookup, sanctionsmatch.More in Signals and Sanctions screening.
in and notin ask whether a value is on a list, and compare exactly. That is the right test for a card BIN, an IBAN, a device id.watchlistmatch is for names: it uses the same normalisation and the same similarity measure sanctions screening uses, so “Muhammet Yilmaz” and “Muhammed Yılmaz” are one person. It is offered only against lists marked as holding names of people or companies.
Comparing a list of names with in leaves a list that never fires in practice: a name reaches you spelled however the customer’s system stored it, and only the entries somebody happened to type identically in both places are caught. The rule reads correctly and does nothing.

List weights

Membership answers one question well: is this value on the list. Risk is rarely a yes or no — gambling is worse than electronics, which is worse than groceries — and saying that with membership means one list per band; two of the three get updated and they drift apart. A list entry can carry an optional weight, and a condition can compare that weight instead of the value. The editor offers it from the same place as an aggregate, because it does the same kind of thing: it replaces what is on the left of the comparison, and the operator and value then work unchanged.
An entry with no weight and an entry weighted zero are not the same thing. A value the list does not weight is compared as zero — skipping it would make “weight below 10” miss every unknown value, which is the population that rule is usually written to catch — and the decision records which of the two it found.
A CSV upload accepts value,weight lines. Lines with no comma are added as plain membership entries, exactly as before.

Velocity rules and aggregates

A condition can look at history rather than at one event: how many accounts this device touched in the last 24 hours, how many different senders paid into this IBAN. Open it with the Aggregate selector beside the field; sub-filters decide which records count.

Our own decision history

Every other aggregate reads what the customer sent us. The last two read what we did about it, and “we have sent this card to review four times this month” is a signal no amount of transaction data contains. They are two aggregates because they are two statements: a customer we have decided on twenty times is usually a frequent customer, and one we have sent to review four times is not. The count is keyed on the record’s pivot value — a decision is recorded against the entity it was about, and cases are grouped by it. On a table with no pivot the count is zero.
The window is 30 days and cannot be changed in the rule. The decision being taken right now is not counted: it has not been recorded yet, and counting it would fire the rule one transaction earlier than its author meant.
These two aggregates take no sub-filter. The count is answered from the platform’s own decision history; there is no query over the record’s own table for a filter to narrow. Publishing refuses one — ignoring it silently would count a wider population than its author wrote.
The event being decided counts towards its own aggregate — but only where that can be done exactly (COUNT, SUM, MIN, MAX). So the answer does not depend on whether the caller ingested the event before asking for the decision.

Impossible travel

TIME_SINCE_LAST and DISTANCE_FROM_LAST in one scenario produce a signal no single field can: a payment from somewhere the cardholder could not have reached in the time available.
DISTANCE_FROM_LAST reads a text field shaped "41.0082,28.9784". The decimal separator is a dot and does not depend on the server’s locale.
With no earlier record — or no readable location — the answer is nothing, not zero. Zero reads as “same place” or “just happened”, and would fire the velocity rule on every customer who has never transacted. A comparison against a missing answer matches in neither direction: neither “more than 500 km” nor “less than 60 seconds” fires.

Scoring

Scores can be positive or negative. A negative score is the most direct way to cut false alarms: giving “customer with more than twelve successful transactions” a -20 stops those customers from crossing a threshold on one weak signal.
Keep scores in multiples of five and decide your total budget up front. Assigning each rule an arbitrary number produces a system where, six months later, nobody remembers why the thresholds are where they are.

Thresholds

Three thresholds, four outcomes. They must ascend — review ≤ block-and-review ≤ decline.
You do not have to guess where to put them: Analytics → Threshold simulation re-reads your past decisions at new thresholds and tells you how many would have landed differently.

Publishing a version

Rule changes accumulate in the draft; the live version is untouched. Publish makes the draft live and keeps the previous one on record. A draft affects no decision until you publish it — a half-built rule set never looks at live traffic.

What publishing checks

Publish validates the scenario before it goes live and reports every problem at once: All at once, because publishing one problem at a time is guess-and-retry against live traffic.

A second approval before publishing

Turned on under Settings → Organization → Publishing rules. With it on, Publish does not publish — it records a request: the draft stays a draft and somebody else reviews it and publishes. Whoever asked cannot approve their own request.
Any edit to the draft after a request retires that request, and the next publish counts as a new request rather than an approval. Without this the control is defeated in one move that looks like ordinary work: Ada asks to publish, Grace edits the rules, Grace approves — two different people, both steps recorded, and what went live is a change nobody but Grace ever read.
Retiring the request rather than refusing the edit is deliberate: a reviewer’s job is to find things that need changing, and a control that punishes them for finding one teaches everybody to approve without reading. Off by default. It is a cost as well as a control, and a two-person team cannot pay it.

Comparing versions

Compare versions on the rules screen shows what was edited between two versions: rules added, removed and changed; the before and after of every field that moved; thresholds, schedule and the trigger condition. This is the other half of the question a shadow run answers. A shadow run measures how two versions behave on live traffic; this screen says what somebody changed. A change with no measurable effect is still one somebody has to account for. Rule conditions are shown as sentences rather than as JSON — two blocks of JSON side by side produce a comparison nobody actually performs; they approve it and move on.
Rules are matched by name. Creating a draft copies every rule into a new row, so there is no rule identity that survives a version. The cost is that renaming a rule shows up as a removal and an addition, which the screen says outright. Guessing at a rename by matching formulas would describe an edit nobody made.

A published version does not change

The version number is assigned at publish and that version can never be edited again. Editing always goes to the draft, which is opened as a copy of the live version if there is not one yet.
Every decision records the version that produced it, and that version does not move. The answer to “which rules decided this” is the same six months later — and rolling back is publishing an earlier version rather than trying to remember what changed.

Is a rule pulling its weight?

Analytics → Rule performance tells you, per rule: how often it fired, and in how many decisions it actually changed the outcome. The second number is computed by removing that rule’s score and re-scoring the rest. A rule that fires constantly but is never decisive is adding noise to every score. Try lowering its score before deleting it.