Start with the parameters, not the rules
A rule can only see fields you declared. If you never add locale, no rule can ask about locale — even if your app sends it. So the Parameters panel is the contract with your app: these are the names and types you promise to send.
Names are dotted paths of up to four segments. locale is one segment. user.plan is two. geo.country.code is three. Each segment starts with a letter and can continue with letters, digits, or underscores, up to 64 characters. You can declare at most 32 parameters on one ruleset.
Each parameter has a type. The type decides which operators you get.
| Type | What you send | Good for |
|---|---|---|
string | Text. Empty strings are treated as missing. | Locale, plan, campaign, path, country code. |
number | A finite number. Non-numbers are ignored. | Cart total, item count, version number. |
boolean | true or false. | Logged in, preview mode, feature already on. |
timestamp | Epoch milliseconds, or an ISO-8601 string the engine can parse. | Sale start, last purchase, “is this still the weekend?” |
String parameters can opt into case-insensitive comparison. Turn that on for locale and plan unless you truly need DE and de to be different. It applies to eq, neq, in, not_in, and prefix.
now is reserved. You never send it. The engine injects the current clock so time comparisons work even when the visitor payload has no timestamp. token and environmentId are reserved too — they belong to the request, not to your schema.
How your app's JSON becomes those names
You send a nested object. The engine flattens it onto the dotted names you declared. This body:
{
"locale": "de-DE",
"user": { "plan": "pro" },
"campaign": "spring-sale"
}becomes locale, user.plan, and campaign. Extra fields your schema does not name are ignored. A value with the wrong type, an empty string, an array, or null is treated as absent — which is why exists / not_exists are useful. Nesting deeper than four levels, or more than 256 leaves, is rejected.
Rules: a label, some conditions, one widget
Each rule has a label you will recognize in a dry-run trace (“Spring sale”), an on/off switch, a list of conditions, and an outcome. The outcome is always “return this widget key.” Schema v1 does not do percentage experiments or multi-variant splits. Those are reserved for a later schema; the editor will not let you author them.
Conditions on one rule are all required. There is no “or” inside a rule. If you need “German or Austrian,” that is two rules with the same outcome, or one in list: de-DE, de-AT.
A rule may have at most 16 conditions. A ruleset may have many rules — the editor warns after 200, and compile refuses after 1,000. You will not hit that on a homepage hero.
Disable a rule when you want it out of the running without deleting it — last week's campaign, a locale you are not ready to ship. Disabled rules are omitted from the published snapshot.Duplicate a rule when the next one is almost the same and you would rather edit a copy. Up and Down change evaluation order. There is no drag handle and no hidden priority number. The list you see is the list the engine walks.
Renaming a parameter updates every condition that used the old name. Deleting a parameter drops the conditions that referenced it. If that empties a rule's condition list, the rule is removed too. That cascade is why you add parameters first and delete them carefully.
Every operator
Not every operator is legal on every type. prefix is strings only. between is numbers and timestamps. Booleans are just “is this true” or “is this present.”
| Operator | Meaning | When you reach for it |
|---|---|---|
eq / neq | Equal / not equal. | campaign is spring-sale. Plan is not free. |
in / not_in | Value is / is not in a list of up to 64 literals. | Plan is pro or business. Locale is one of the DACH tags. |
exists / not_exists | The parameter is present / absent. | Logged-out visitors have no user.plan. A rule that needs a plan should not fire for them. |
gt / gte / lt / lte | Greater / less, with or without equality. | Cart total at least 100. A timestamp is still in the future. |
between | Inclusive range of two values. | Sale window from Friday 00:00 to Sunday 23:59. A score from 10 to 20. |
prefix | String starts with a prefix. | Locale starts with de so de-DE and de-AT share a rule. |
| Type | Operators you can use |
|---|---|
string | eq neq in not_in exists not_exists prefix |
number | eq neq in not_in exists not_exists gt gte lt lte between |
timestamp | eq neq gt gte lt lte between exists not_exists |
boolean | eq exists not_exists |
Time windows against the injected clock use a timestamp parameter named in your schema plus now in a comparison, or a between on a timestamp you send. There is no separate “window” or “older than N days” operator in schema v1. Those names exist in the engine for a later schema and are rejected if they show up today.
First match, worked through
Take the homepage hero from the overview, in this order:
campaignequalsspring-sale→ sale herolocaleprefixde→ German herouser.planinpro,business→ Pro hero
Default: the everyday hero. A German Pro visitor with campaign: "spring-sale" gets the sale hero, not the German one. Swap the first two rules if locale should beat the campaign. Dry run both orders before you publish. That is cheaper than discovering the order on the live site.
If no enabled rule matches — unknown locale, no plan, no campaign — the default fires. Always pick a default you are willing to show everyone.
What the editor allows you to save
Saving a draft is deliberately softer than publishing. A missing default, or a widget key that is not published in this environment, is a warning on save. You can keep typing. The same problems become errors when you publish or promote. A broken shape — a bad parameter name, too many rules, an operator that does not belong on that type — is an error even on save, and the request is rejected.
Dry run in the editor evaluates the draft you are looking at, including unsaved edits. It does not switch to the last published snapshot. If you need to see what Production is serving, open View published or a history version.
When the traces look right, publish, promote, or copy. When you want your app to call this in Development, resolve from your app.