Skip to main content

What this solves

Most A/B tests confuse “which variant won” with “did the engine actually help vs doing nothing?” A holdout group answers the second question: a known percentage of traffic is held out entirely and gets zero offers from the flow. Comparing engaged-rate across variant × (in-experiment vs holdout) gives you causal uplift, not just relative ranking.

Why this works

The platform gives you three complementary mechanisms:
  1. Champion / Challenger on the Score nodechampionChallenger.{champion, challengers[]} routes each customer to one scoring model via a deterministic hash of customerId, so the same customer always lands in the same variant. The assignment is persisted (see Step 3).
  2. Per-flow holdout — a flow-level experiment config block ({ enabled: true, holdoutPercent: N }). Held-out customers get zero offers and the trace carries traceSummary.holdout: true. This is a true causal holdout — the customer sees nothing from this flow, so you can measure lift against “did nothing.”
  3. Always-on control group — an independent ~2% slice (deterministic per customer × day) that still receives the qualified, policy-filtered offer set but with randomized scores/ranking instead of the model’s ranking. Surfaced as controlGroup: true on every response.
Champion-vs-challenger measures which model ranks better; the holdout and control group measure whether ranking helps at all.

Step 1 — Reserve a holdout on the flow

The holdout lives on the flow config as an experiment block — not on tenant settings. The Controlled Experiment (Holdout) flow template ships it pre-wired; the shape is:
At decision time, ~10% of customers (hashed deterministically and scoped to this flow’s id, so each flow draws an independent holdout) short-circuit to an empty result with traceSummary.holdout: true — they get no offers from this flow. holdoutPercent is expressed as a percentage of traffic.

Step 2 — Configure champion/challenger on the Score node

The weights sum to 100. A deterministic hash of the customer picks the bucket, and the assignment is persisted (keyed by experimentId) so the same customer stays in the same variant across sessions until you change the configuration.

Step 3 — Capture the variant on each decision

The recommend response includes:
experimentVariant is the model key the customer was routed to by champion/challenger (null when no experiment is configured on the flow). controlGroup: true marks the always-on ~2% control slice (randomized ranking) — it is independent of the champion/challenger split and of the per-flow holdout. The decision_traces.experimentAssignment JSONB persists the assigned variant as { "variant": "bayesian-v2" } for later analysis.

Step 4 — Measure uplift

GET /api/v1/experiments/{id}/results computes z-tested uplift between the treatment population and the holdout (__holdout__ variant assignments). The path segment accepts the experiment’s UUID or its name:
The response carries treatment and holdout conversion rates, uplift.{absolute, relative}, significance.{zScore, pValue, isSignificant}, and Wilson confidence intervals. The z-test / p-value math lives in platform/src/lib/experimentation/uplift.ts.

Gotchas

  • Holdout is per-flow, not tenant-wide. It lives on the flow’s experiment.holdoutPercent, so different flows can hold out different shares. The always-on control group, by contrast, is tenant-wide: it reads tenant.settings.controlGroupPercent and defaults to 2% when unset. (That default is what applies unless the value is seeded directly on the tenant record — controlGroupPercent is not on the PUT /api/v1/tenant-settings allowlist.)
  • Variant assignment is persistent. The same customer always sees the same variant — a DB-backed assignment (variant_assignments, 30-day TTL) keyed by experimentId, falling back to a deterministic hash on a miss.
  • autoPromote on the Experiment resource (when enabled) automatically promotes a winning challenger to champion once it clears promoteThreshold for promoteAfterDays. Combine with four-eyes approval for governance.

What the trace will show

Proof reference

T11 (bulk respond) + T15 (scoring strategy resolution) + the experiment fixture in T1 of the proof bundle cover this end-to-end.