Skip to main content
Experiments list view in the Algorithms module

The Experiments page.

How experiments work

KaireonAI runs experiments using a champion/challenger pattern, not a generic control/treatment A/B test. The vocabulary maps as follows: The full lifecycle is draft → active → paused → archived (the four values accepted by the status field). Status transitions happen via PUT /api/v1/experiments/{id} and are guarded by RBAC (admin/editor).

End-to-end data flow

Champion/challenger mode

This is the default mode. Traffic is split among the champion and one or more challengers. Each model scores the same candidates but on different customers; outcomes feed back through /respond to compare conversion rates head-to-head. To set it up:
  1. Train a candidate model alongside your live champion (see Algorithms & Models). It must reach registryStatus = "production" or "challenger" to be eligible for promotion via auto-promote.
  2. POST /api/v1/experiments with:
    • championModelId: the current live model
    • trafficSplit: { championPct: 50 } — what fraction of non-holdout traffic the champion gets
    • challengers: [{ modelId: "model_xyz", trafficPct: 50 }] — the competing model and its allocation
    • holdoutPercent: 10 — bypass 10% entirely for measurement
    • status: "active"
  3. Validation rule: championPct + sum(challengers[].trafficPct) must equal 100. The endpoint returns 400 otherwise.
  4. Fire traffic. Variant assignment is deterministic per customer — the same customerId lands on the same variant on every call so personalization is consistent.
  5. After enough samples (see requiredSampleSize in the results response), check /experiments/{id}/results for statistical significance.
  6. Decide the winner: manually update the experiment via PUT /api/v1/experiments/{id} setting status: "archived" and (optionally) swap the championModelId. Or let autoPromote: true handle it after promoteAfterDays if the challenger crosses promoteThreshold.

Shadow mode

Shadow mode is a different mechanism that lives on the model registry itself, not on the Experiment resource. Use it when you want to evaluate a candidate model on real production traffic without changing any decision the customer sees. How it works:
  • A model with registryStatus = "shadow" (or any model listed in a ScoreNode.shadowModelKeys array) scores every candidate in parallel with the live champion.
  • Shadow scores are written to the decision trace’s scoringResults[].shadowScores map, keyed by model key.
  • Shadow scores never enter ranking, never enter /recommend responses, never affect what the customer is shown. They are recording-only.
  • After enough traffic, you can compare per-customer ranking similarity (Kendall tau, top-K overlap, expected lift on observed outcomes) between champion and shadow off-line.
When to use shadow mode vs champion/challenger: To promote a model into shadow mode: Models flow through a strict five-stage registry lifecycle: draft → shadow → challenger → champion → archived. Transitions are made through a dedicated promotion endpoint that enforces transition legality, the “one champion per family” invariant, and an auto-rollback guard, and writes an AuditLog row for every change. There is no direct PATCH on registryStatus — write attempts via PUT /algorithm-models/{id} are ignored.
Body fields: toStatus (required, one of draft/shadow/challenger/champion/archived), family (optional grouping key — the “one champion per family” rule fires here), bypassRollbackGuard (optional, admin escape hatch), metricsSnapshot (optional key-value map recorded with the promotion). Returns 409 if the rollback guard trips, 400 for invalid transitions, 404 if the model isn’t in your tenant. The accompanying read endpoint GET /api/v1/algorithm-models/resolve-lifecycle returns the current champion plus all challengers and shadow models for the tenant — useful for diagnosing which models are wired into which lifecycle slot. Or attach the model to a specific decision flow’s score node:
Shadow scores appear in every decision trace produced by that flow. Read them from decision_traces.scoringResults[].shadowScores or aggregate them via the Decision Traces API.

Holdout group

The holdoutPercent field reserves a slice of customers who bypass the experiment entirely. The variant engine sticky-hashes them to __holdout__ and the platform delivers a baseline (no personalization, or whatever the kill_switch fallback is) for those calls. Outcomes still flow through /respond, which gives you the denominator for incrementality math:
A 10% holdout is the standard. Set it lower (5%) if traffic volume is small and you can’t afford to suppress recommendations; higher (20%) if you want tighter CIs on the holdout-side measurement. The holdout group is the SAME for the entire experiment. Don’t confuse it with per-challenger comparisons — those are champion-vs-challenger; the holdout is treatment-vs-no-treatment.

Statistical methods

The results endpoint uses three textbook procedures:
  1. Two-proportion z-test for significance:
Φ is the standard normal CDF, approximated via Abramowitz–Stegun 7.1.26 with input z/√2 (accurate to ~1e-7). The platform’s implementation has been verified against textbook tables: at z = 1.96 it returns p = 0.0500 (matching the canonical 95% threshold), at z = 2.576 it returns p = 0.0100, etc.
  1. Wilson 95% confidence interval for each variant’s conversion rate. Wilson CIs are preferred over the normal-approximation interval because they remain valid for small samples and rates near 0 or 1.
  2. Required sample size estimate, using baseline rate × minimum detectable effect × number of variants:
Reported as requiredSampleSize so operators know when they have enough power to declare a result.

Mode comparison at a glance


GET /api/v1/experiments

List all experiments with their champion model and challengers. Supports cursor-based pagination.

Response


POST /api/v1/experiments

Create a new experiment. Traffic split must sum to 100%.

Request Body

Validation

  • Traffic split must sum to 100%: championPct + sum(challengers[].trafficPct) must equal exactly 100. Returns 400 if not.
  • Key must be unique per tenant. Duplicate key returns 400.

Example

Response: 201 Created

GET /api/v1/experiments/

Get experiment details with champion and challenger models.

PUT /api/v1/experiments/

Update an experiment. Challengers are replaced entirely when the challengers array is provided.

Request Body

All fields optional. Accepts the same fields as POST except key (which is immutable) — including status, championModelId, trafficSplit, autoPromote, promoteThreshold, promoteAfterDays, holdoutPercent, challengers, and results (object, for stored outcome data). When both trafficSplit and challengers are supplied, their percentages must still sum to 100.

DELETE /api/v1/experiments/

Delete an experiment and its challengers. Response: 204 No Content
DELETE also works at the collection level: DELETE /api/v1/experiments?id={experimentId}. Both the path parameter and query parameter forms are supported.

GET /api/v1/experiments//results

Returns uplift analysis and statistical significance for treatment vs holdout. The endpoint first checks for live variant assignment data. If no assignments exist, it falls back to stored JSON results. The dataSource field reports which was used — "live" (aggregated from variant_assignments + interaction_history) or "stored" (from the experiment’s results JSON). If neither is available, it returns { "experimentId", "hasResults": false, "message": "…" } instead of the full analysis below.

Response

The per-variant variants[] breakdown is derived from the experiment’s stored results JSON, so it is [] when no per-model results have been recorded (each entry carries auc; challenger entries also carry pValue).

Statistical Methods

  • Two-proportion z-test for significance testing (p < 0.05)
  • Wilson confidence intervals for per-variant conversion rates
  • Required sample size estimation based on baseline rate and minimum detectable effect

Roles

See also: Algorithms & Models