Mutually Exclusive Experiments

Prevent experiments from overlapping with the bucketed_into_experience_key audience rule

Sometimes two experiments must never overlap for the same visitor — for example, two competing checkout redesigns whose effects would confound each other if a visitor saw both. Full Stack expresses this with the bucketed_into_experience_key audience rule: it makes a visitor's membership in one experiment a targeting condition for another.

Negated, it reads "the visitor is not bucketed into experiment X" — the building block of mutual exclusion. Non-negated, it reads "the visitor is bucketed into experiment X" — useful for deliberately layering a follow-up experiment on top of an existing one.

Configure the rule

Add the rule to the audience of the experiment you want to gate, in the Convert app:

  • Value — the key of the target experiment (the one whose members you want to include or exclude).
  • Exclude vs. include — toggle negation on for "NOT in that experiment" (mutual exclusion); leave it off for "only visitors IN that experiment" (co-targeting).

That's the whole setup — no code changes. In the served configuration the rule appears as the bucketed_into_experience_key rule type, evaluated by the SDK during audience matching like any other rule.

How it works

When the SDK evaluates the gated experiment's audience, it looks up whether the visitor is already bucketed into the target experiment, reading the visitor's stored bucketing decisions. The check is read-only: it never buckets the visitor into the target, never writes state, and never sends an event. If the visitor is bucketed into the target, the negated rule fails (excluding them); if they are not, it passes.

Because it evaluates as a normal audience rule — before bucketing — it composes cleanly with everything else: it does not change how variations are assigned, and it works identically whether the experiment uses the packed or the anchored bucketing layout.

Order matters

The rule can only see decisions that have already been made and stored. So the gating experiment must run before the one that excludes it:

// Run the gating experiment first so its decision is stored...
context.runExperience('experiment-a');
// ...then the mutually-exclusive one — a visitor already in A is excluded here.
const b = context.runExperience('experiment-b'); // null if excluded
$context->runExperience('experiment-a');
$b = $context->runExperience('experiment-b'); // null if excluded
context.runExperience("experiment-a")
val b = context.runExperience("experiment-b") // null if excluded
context.run_experience("experiment-a")
b = context.run_experience("experiment-b")
# excluded → a frozen Sentinel whose #key is nil (b itself is truthy; test b.key.nil?)
_ = await context.runExperience("experiment-a")
let b = await context.runExperience("experiment-b") // nil if excluded
context.run_experience("experiment-a")
b = context.run_experience("experiment-b")  # None if excluded

If you evaluate every experiment at once with runExperiences, they are processed in configuration order (the order the experiments were created), so create the gating experiment first.

Two consequences to plan for:

  • Order is fixed at creation. Configuration order is the order experiments were created, and it can't be changed later — so if the gating experiment is created after the one meant to exclude it, the exclusion can't take effect and both may run. Create the gating experiment first.
  • Transient audiences re-evaluate on every call. A visitor already counted in experiment B who later buckets into A will fail B's exclusion on B's next evaluation and drop out — mid-experiment eviction after exposure. Sequencing the calls (A before B) is what prevents it.

Persistence across requests and sessions

Exclusion depends on the target experiment's decision being available when the gated one is evaluated. Within a single context that's automatic. To make exclusion hold across requests or sessions, the decision must be durable:

  • JavaScript, PHP, Python, Ruby — configure a persistent DataStore. Without one, exclusion only holds for what a single instance remembers in memory.
  • Android and iOS — bucketing decisions persist automatically in app-private storage; no DataStore setup is needed.

Combine with other targeting

Keep the exclusion rule in its own audience, and put any other targeting (URL, visitor properties, and so on) in separate audiences. Then set the experiment's audience matching to ALL — every audience must match, equivalent to AND in the audience builder — so the exclusion always applies. Under ANY (OR), a visitor who matches one of the other audiences would run the experiment even if they're in the excluded one, silently bypassing the exclusion.

Keep the exclusion rule as the sole rule of its audience and combine at the experiment level — don't mix it with other conditions inside a single audience's rule tree.

Notes

  • Self-reference — pointing an experiment's exclusion at its own key is unsupported; avoid it (there is no guard that stops you).
  • Removed targets — if the target experiment is archived or deleted (no longer in the served config), the visitor is treated as not bucketed into it, so a negated exclusion against it simply stops excluding. Point exclusion rules at experiments that stay live for the duration of the gated one.

See Rule Evaluation & Targeting for how audience rules are evaluated, and Requirements for the full list of Full Stack audience rule types.


Did this page help you?