Targeting Rules
Targeting rules determine which variation of a feature flag is served to a given user.
How rules work
- Rules are evaluated in order (top to bottom).
- Each rule has one or more conditions — all conditions must match (AND logic).
- The first matching rule wins and serves its assigned variation (or one variation chosen from a weighted distribution).
- If no rules match, the default variation for the environment is served (
reason: "no_rule_matched").
Rules are scoped to a single environment. A rule you add in Development is invisible in Testing and Production — there is no auto-mirroring. To replicate a rule across environments, use the per-rule Copy to environment action (see below).
Targeting ON / OFF (the kill switch)
Each environment card on a feature's Manage page has a Targeting ON / OFF switch to the left of the status label. It's the master switch for that feature in that environment:
- Targeting ON — your rules apply. Users who match a rule receive its variation; users who don't match any rule receive the environment's default variation (
reason: "no_rule_matched"). - Targeting OFF — the platform serves no value at all. The API responds with
variation: null,variables: {}, andreason: "targeting_disabled", and the SDK falls back to the default value you hard-coded in your application.
This is the code-default model: the toggle is a true kill switch — flip it OFF and your app instantly reverts to whatever default you ship in source, with no rule edits, deploys, or rollbacks required. No metric rows are written for evaluations served while targeting is OFF, so dashboards stay clean.
Other platforms treat the master switch as "serve the default rule" or "serve the configured fallback variation". RedPennon does not — when targeting is OFF, the API returns null and the SDK uses your code default. If you want a server-controlled fallback variation instead, leave targeting ON and rely on the per-environment default variation that fires under reason: "no_rule_matched".
Self-Targeting lets you pin a variation for your own user id without writing a rule. Overrides are evaluated before targeting rules, so they're ideal for QA, demos, and reproducing customer issues.
The rule card
Each rule on the Manage page renders as a single card with four labelled rows:
- Name — free-text label, can be left blank for in-progress rules. The drag-free reorder (move ▲ / ▼ buttons), copy-to-environment, audience-from-rule, and delete actions live on this row.
- Definition — one row per condition (property + operator + value), AND-joined. A trailing + Add Definition button appends another condition.
- Serve — choose either a single variation OR Random Distribution (revealing weighted split inputs).
- Schedule — defaults to None. Switching to Specific Date & Time reveals a single datetime input that flips the rule on at that moment.
Below the last rule, a read-only Default Rule placeholder card always appears. It explains what the evaluator does when nothing above it matches (serves the environment's default variation).
Condition properties
| Property | Description | Sourced from |
|---|---|---|
all_users | Always matches — use as the catch-all rule at the bottom of the list | (none — operator/value ignored) |
user_id | The user's stable identifier | user.id |
email | The user's email address | user.email |
organisation_id | The user's organisation/tenant id | user.organisation_id |
ip_address | Client IP address | user.ip |
audience | Membership in a named audience | user.audiences (SDK-tagged) plus any audience whose conditions match the user context |
app_version | The user's app/build version (used with numeric operators for "≥ 2.4.0" rollouts) | user.app_version |
platform | Platform identifier (ios / android / web / etc.) | user.platform |
country | ISO-3166 alpha-2 country code | user.country |
custom_property | Lookup inside user.custom_data by custom_key (anything you want — plan tier, A/B cohort, …) | user.custom_data[custom_key] |
The all_users property has no operator or value. It's how a rule says "serve this to everyone who reaches this point in the rule list", which is what you want for a rollout's first rule or for an experiment's distribution rule.
Operators
The Definition row's operator dropdown filters its options to whatever makes sense for the selected property:
| Operator | Available on | Notes |
|---|---|---|
is, is_not | All string properties | String equality |
is_in, is_not_in | All string properties, audience | Value is a list; condition matches when actual is/isn't an element |
contains, does_not_contain | All string properties, audience | Substring match (or list-membership for audience) |
starts_with, does_not_start_with | All string properties | |
ends_with, does_not_end_with | All string properties | |
equals, does_not_equal | app_version | Numeric — both sides parsed as floats; non-numeric values fail the condition |
greater_than, less_than, greater_than_or_equal, less_than_or_equal | app_version | Numeric — same coercion rules as above |
exists, does_not_exist | All optional properties | True when the user-supplied value is non-null and non-empty |
is, not equals, for string equalityequals and does_not_equal are numeric operators by design. They coerce both sides to floats; comparing two strings (platform equals ios) will silently fail to match. Use is / is_not for string equality and reserve the numeric operators for app_version-style comparisons.
Random distribution (splits)
Instead of serving a single variation, a rule can serve a weighted random distribution across multiple variations. Pick Random Distribution in the Serve row to reveal the percent inputs; each split entry pins a variation to a percent and the percents on a rule must sum to 100.
When a rule with splits matches, the evaluator buckets the user deterministically: bucket = sha1(f"{user_id}:{rule.id}") mod 100. This means:
- The same user always lands in the same arm for the same rule.
- Renumbering or recreating a rule reshuffles users (the rule id is part of the hash), which is the expected behaviour for a "fresh experiment".
- Splits and
serve_variationare mutually exclusive on a rule. The editor clears one when you pick the other — switching from Random Distribution to a single variation wipes any stale split rows, and vice versa. - Anonymous users (no
user_id) bucket as"0"so they still hash deterministically to a single arm.
Example. A 3-arm experiment seeded by the create-feature modal sets up a single rule with an all_users condition and three splits: control 34%, variation-a 33%, variation-b 33%.
Per-rule schedule
A rule can be time-gated. In the Schedule row, pick Specific Date & Time to reveal a datetime input. Until that moment passes, the rule's conditions are treated as not-matching (control falls through to the next rule); after that moment, the rule fires normally.
Schedules are deterministic per (rule.id, user.id) — the same user's bucketing position relative to the schedule is stable across requests, so you don't see users bouncing in and out as the rollout progresses.
Reordering rules
Use the ▲ / ▼ buttons on the right of each rule card to swap a rule with its neighbour. The new order is persisted immediately and takes effect on the very next evaluation.
If you need a brand-new rule at the very top of the list (a higher-priority rule than anything you currently have), click the toolbar + Add Rule above the panel — it inserts at position 0 and bumps every existing rule down by one. The inline + Add Rule button between the last rule and the Default Rule placeholder appends to the bottom.
Copy a rule to another environment
The Copy to environment button on each rule card opens a multi-select modal listing every other environment in the project. Picking one or more and submitting deep-copies the rule (with its conditions and any random-distribution splits) into the chosen environment(s) and refreshes their panels in place.
The source rule stays where it is. Each clone is an independent rule from then on — editing one does not affect the others.
Audience from a rule
If you find yourself reusing the same condition set across rules, click Create audience from this rule on the rule card. The conditions are promoted into a new Audience at the organisation level, and the source rule is rewritten to a single (audience, contains, <new-slug>) reference.
If an audience with the chosen key already exists, the modal will:
- show an inline error and let you pick a different key, or
- if the Replace current targeting rule switch is on, overwrite the existing audience in place (same key, conditions replaced) so any other rules referencing that audience pick up the new definition automatically.
Why didn't my rule match?
A few common gotchas:
equalson a string property — see the caution above. Useisinstead.- Wrong order — the evaluator stops at the first matching rule. If a broader rule sits above a more specific one, the specific rule never fires. Move the specific one up using ▲.
- Schedule still in the future — until the scheduled datetime passes, the rule is treated as not-matching.
- Misconfigured Serve — a rule with neither a serve variation nor any splits is skipped (the evaluator falls through to the next rule). The editor's read-only Default Rule placeholder is always last; if you see your variation coming from there, it means none of your rules matched (or the matching one was misconfigured).