Contents

Rules and operators

Declare the fields your app will send, then write ordered rules against them. This page is every lever in the editor: types, operators, first-match order, and what save will tolerate that publish will not.

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.

TypeWhat you sendGood for
stringText. Empty strings are treated as missing.Locale, plan, campaign, path, country code.
numberA finite number. Non-numbers are ignored.Cart total, item count, version number.
booleantrue or false.Logged in, preview mode, feature already on.
timestampEpoch 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.”

OperatorMeaningWhen you reach for it
eq / neqEqual / not equal.campaign is spring-sale. Plan is not free.
in / not_inValue 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_existsThe 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 / lteGreater / less, with or without equality.Cart total at least 100. A timestamp is still in the future.
betweenInclusive range of two values.Sale window from Friday 00:00 to Sunday 23:59. A score from 10 to 20.
prefixString starts with a prefix.Locale starts with de so de-DE and de-AT share a rule.
TypeOperators you can use
stringeq neq in not_in exists not_exists prefix
numbereq neq in not_in exists not_exists gt gte lt lte between
timestampeq neq gt gte lt lte between exists not_exists
booleaneq 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:

  1. campaign equals spring-sale → sale hero
  2. locale prefix de → German hero
  3. user.plan in pro, 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.