Skip to main content
Adaptive policies go beyond static A/B tests. Instead of fixed allocations, the optimizer shifts traffic toward better-performing variants based on observed rewards — automatically, continuously, and without code changes.

When to use adaptive policies

Use adaptive policies when:
  • You want to minimise regret — the cost of showing the inferior variant longer than necessary.
  • The best variant may change over time (seasonality, mix shifts).
  • You want continuous optimization rather than a one-time decision.
  • You want personalized selection based on user context (contextual bandits).
Stick with static A/B tests when:
  • You need a clean point-in-time decision with an unambiguous winner for stakeholders.
  • The cost of the variant is large enough that you want a deliberate ramp afterwards (use rollouts).
  • Your goal metric is delayed or noisy (the engine will still work, but a static test gives you a more interpretable result).

Algorithms

Not sure which to pick — or how the goal event shapes what the optimizer learns? See Choosing an optimization algorithm.

Setting up an adaptive policy

  1. Define the goal event in .traffical/config.yaml or in the dashboard:
  2. Create the policy in the dashboard:
    • Kind: adaptive
    • Algorithm: thompson_bernoulli
    • Goal event: purchase
    • Goal type: conversion_rate
    • Min exposures before shift: 100
    • Window size: 60 minutes
  3. Define allocations. Initial ranges can be uniform; the engine will adjust them.
  4. Implement in code — same as a static policy:
The SDK resolves from the bundle. The engine periodically retrains and republishes the bundle with updated bucket ranges. Your code never changes. As data accrues, the policy page tracks progress through four phases — exploringlearningconvergingconverged. See What to expect while it runs for what each phase means and why “converged” still defers to measured significance.

Tuning knobs

Optimizing with your warehouse

The optimizer’s reward source is the policy’s primary metric — its warehouse aggregates feed the bandit on every tick. On projects with defined events, the goal event you pick simply resolves to that metric for you — and the goal step also lets you switch to picking a metric directly, so you can optimize any fact-backed metric even when native events exist. On BYO-warehouse projects with no event definitions at all, the metric is the only way to declare the goal:
  1. Define a fact over your own tables (SQL in your warehouse’s dialect) — one row per goal action, with the entity key, a timestamp, and an optional value column.
  2. Create a metric on that fact — conversion, count, or sum. Ratio metrics can’t be optimization goals (the reward would use only the numerator).
  3. Create the adaptive policy and pick that metric in the goal step. The goal type follows the metric’s type, and the metric is attached as the policy’s primary — the optimizer and measurement judge the same quantity by construction.
Two things to know about this contract:
  • The primary metric is authoritative. Re-pointing the policy’s primary metric re-points the optimizer on its next tick. A policy without a resolvable primary metric can’t start, and a running one reports a “no goal metric” health failure instead of silently optimizing nothing.
  • Metrics that should go down work too. A goal metric with a decrease desired direction (error rate, time-to-complete, support contacts) makes the bandit minimize: traffic shifts toward the variant with the lowest reward.
Adaptive policies also require the project to actually be served by Traffical. If yours is a measurement-only deployment — something else entirely decides who sees what, and Traffical purely analyzes — mark the project externally served in project settings: adaptive policies are then blocked from starting and the optimizer skips the project, since nothing would read the traffic splits it writes. Note that routing assignment data through your own pipeline (the BYO assignment logging pattern) is not external serving — the SDK still serves the traffic there, and adaptive optimization works end to end.

Contextual bandits

The linear_contextual algorithm personalizes selection: different users get different variants based on context features. The training pipeline learns coefficients per allocation per feature; those coefficients ship in the config bundle; the SDK uses them to score each allocation for the current user at resolution time.

What it looks like in code

The code is identical to any other adaptive policy — you just pass more context:
The model uses the listed context fields. Fields not in the model’s training set are ignored.

Scoring

For each allocation, the SDK computes:
It then applies softmax with temperature gamma to convert scores into selection probabilities, and enforces a minimum probability per allocation (actionProbabilityFloor) so the model keeps exploring.

Context logging allowlist

To train the model, the platform needs to see the context fields with each exposure event. To protect PII, only fields you explicitly allow-list are logged:
Anything outside the allowlist is dropped before storage.

Typing your context fields

A plain string entry lets the pipeline infer the field’s type from the data. You can also declare the type, which makes encoding predictable and guards against the classic foot-guns (a raw ID or timestamp entering the model unbounded). An allow-list entry can be an object:
Declared fields are encoded deterministically regardless of the traffic window, and editing a declaration retrains the model from scratch on its next training run — the change doesn’t wait for a scheduled rebuild. Fields you leave as plain strings keep the inferred behaviour.

When to use contextual bandits

When the optimal variant differs per user, and you can describe “who they are” with a handful of features:
  • Homepage layout by engagement level and device type
  • CTA copy by referral source and purchase history
  • Recommendation style by user segment
The features should be available at decision time. Don’t include features derived from the outcome you’re trying to predict. One expectation to set: a contextual policy never converges on a single winner — winners vary per context — so the dashboard doesn’t show a single-winner runtime estimate for it. Measurement progress still tracks the evidence on the policy’s primary metric.

Per-entity bandits

For optimization at the entity level — each product, each merchant, each category learns independently — use a per-entity adaptive policy:
Each entity gets its own bandit. A product with 1000 views has its own learned weights; a product with 3 views falls back to the global prior. Every entity’s traffic split also keeps an exploration floor — each allocation retains at least a small share (1% by default) of that entity’s traffic — so no option is ever fully starved, even after an entity’s bandit has settled on a favorite.

Resolution modes

bundle is the right choice for high-traffic entities (product pages, search results). edge is for low-traffic, high-stakes entities where you need every decision to use the freshest possible weights.

Dynamic allocations

When each entity has a different number of options (e.g. each product has a different image count), use dynamicAllocations:
If context.imageCount = 5, the SDK creates five allocations on the fly and selects from them based on learned weights. The selected index is reported as the allocation name in the decision metadata.

Next steps

Per-entity adaptive pattern

Full walkthrough with metric setup.

Contextual bandit pattern

Personalized layouts and CTAs.

Warehouse-native

Training contextual bandits from warehouse data.