Skip to main content
Authoring is in beta. The request and response shapes may still change, and the Management API is enabled per account. Contact us to turn it on.
Predict answers with Prelude’s own model. Eval answers with logic you wrote. This page covers the three objects you author to get there, and the language you write them in.

The three objects

Each layer is reusable by the one above: a rule can be scored by several recipes, and a recipe can be referenced by several flows. Build them bottom-up - rule, then recipe, then flow - because each references the layer below by identifier and the reference is checked when you save. Only the flow identifier reaches your application. Everything else can be re-tuned without a deploy on your side.

Scoring

A recipe sums the weights of the rules that triggered and flags when the total reaches its threshold. A rule that did not trigger or that could not run do not affect the score.

Rules that could not run

A rule reading a signal that never arrived reports NOT_EVALUATED rather than being scored as false. This is the distinction the whole model rests on: missing is not false. A recipe with at least one such rule reports partial_evidence, and a PASS carrying it is weaker evidence than a PASS without it. The usual cause is a request that did not carry device or network signals. Passing dispatch_id from a Frontend SDK fills in most of what rules read.

Overriding rules

Set overrides on a rule association to make that rule’s outcome the recipe’s verdict, bypassing the score - for a condition that is conclusive on its own. The alternative, a weight above every threshold the recipe might later be given, is a statement about the recipe’s arithmetic rather than about the rule, and it silently stops being true when the threshold moves. The weight still applies and the score is still reported; an evaluation references the overriding rule in determined_by. That field is the only thing accounting for a recipe reporting a score under its threshold and flagging anyway.

Conflicting overrides

When overriding rules with different verdicts both trigger, PASS wins over FLAG. The order of rules does not change the result, and the score is not consulted. Rules that did not trigger, or that report NOT_EVALUATED, take no part.
determined_by names the rule that set the verdict. If several overriding rules with that same verdict trigger, it names the first one to trigger, in the order of rules; rules that need a lookup are run after the others. The override applies to its own recipe only. In a flow, a LIVE recipe that flags still flags the evaluation, whatever another recipe’s PASS override decided.

Status

status decides whether a recipe evaluates at all, and whether its verdict counts. Go through SHADOW. A threshold picked without seeing live traffic is a guess, and shadow scoring is the only way to find out what it would have done before it decides anything. A flow referencing a DRAFT or INACTIVE recipe runs its other recipes and reports nothing for that one. It was not asked, which is not the same as having passed.

Writing an expression

A rule’s expression is CEL and must return a boolean. It is compiled before it is stored, so an undeclared name or a type mismatch is refused at authoring time rather than becoming a rule that never fires. An expression reads four kinds of name, each under its own namespace: The namespace is what tells a name you can read apart from CEL’s own syntax, and it is required: a signal written without it does not resolve.
The target is the only one of the four that is always there in some form, since an evaluation names at least one of the two halves. The half a request omits is absent like any other value: rules reading it report NOT_EVALUATED. Beyond CEL’s own operators, expressions can use the string, list, set, regex and network extensions - split, contains, matches, sets.intersects, net.inCIDR, ip(), and the rest of those libraries. A comparison against a value that never arrived does not evaluate to false; it makes the whole rule NOT_EVALUATED. Write the condition you mean and let the engine report absence - do not try to encode a fallback.

Signals

The signals available to rules you author are served by List signals, each under the identifier you write it as, with its type and what it means. Prelude publishes the catalog progressively, so read it rather than holding a copy. A signal. name the listing does not carry is not a name your rules can read, except for your own signals under signal.ext.. An expression referencing one is refused with Unsupported signal `signal.<name>` - the same answer a misspelling gets, so the error never tells you whether the name exists.

Your own signals and parameters

Rules can read two kinds of value the catalog knows nothing about, each under its own namespace. A parameter is a name and a value, so you set it on the recipe, in parameters. One of your own signals is a name alone: write signal.ext.plan_tier in a rule and that is the whole declaration - nothing to add to the recipe, and nothing to keep in step with it. Send a value for it as plan_tier under signals.ext when you evaluate. To have a rule read your own value for something Prelude also resolves, such as the end user’s IP address, send it as one of your own signals (for example signals.ext.ip) and read it as signal.ext.ip. A value you send for an SDK signal, as signals.ip, is only used when the front-end SDK did not provide one. Keys are lower snake_case, at most 64 characters, written without the namespace, and at most 32 of each per recipe. Values are strings - the only type either supports today, so compare as text or split your way to a list. A parameter needs a value. An empty value is refused when you save the recipe - omit the key instead. The namespaces are separate, so a signal Prelude adds later can never collide with a key of yours. Parameters are what let one rule serve two recipes tuned differently: the same blocked_countries rule, strict at checkout and lenient at signup, is one rule and two recipes.

Rules Prelude maintains

Your recipes can score over managed rules - Prelude’s own, shared by every customer - alongside yours. Name one in rules with a weight, exactly as you would your own. They are returned without expressions, and with a description in place of a name. In an evaluation a managed rule reports its identifier, weight and outcome, and blocked_by: "missing_data" rather than identifying the signal it needed. A managed rule reads Prelude signals and the target only. It can’t reference signal.ext. or param. names, and a recipe referencing one that would is refused - a shared rule pinned to one customer’s recipe is no longer shared.

Changing things safely

References are checked in both directions, which fixes the order you work in. Creating, work upward: a recipe referencing a rule that doesn’t exist is refused, and so is a flow referencing a missing recipe. Deleting, work downward: a rule a recipe scores over answers 409 rule_in_use, and a recipe a flow references answers 409 recipe_in_use. Both list what still refers to them in details. Nothing refers to a flow, so a flow deletes freely. This is refused rather than cascaded because a recipe reads its rules all or nothing. One dangling identifier fails the read, and with it every evaluation of every flow that recipe belongs to - a bad delete would take out the gate, not just the rule. To stop a recipe deciding without unpicking anything, replace it with status: "INACTIVE". The flow keeps working and the recipe stops evaluating.

Putting it together

Watch the shadow verdicts, settle on a threshold, then replace the recipe with status: "LIVE". The flow identifier never changed, so nothing in your application has to.