Adaptive Experiments
Let agents test options and shift traffic toward what works.
An agent that makes the same call again and again — which subject line, which offer, which strategy — shouldn't have to guess. An adaptive experiment (a multi-armed bandit) holds a set of candidate options called arms, watches the outcome of every decision, and shifts traffic toward the arms that perform. Early on it explores; as evidence accumulates, the winners get more of the traffic. Your agent runs the whole loop itself, from inside its runs.
Experiments belong to the workspace, so several agents — and every run of the same agent — share the same evidence. What one run learns, the next run inherits.
The loop
- Create an experiment with two to ten candidate arms, each a
nameplus a JSONpayloaddescribing the option. It starts as a draft; set it torunningonce the arms are in place. - Decide. For each real decision the agent calls
decideand gets back an arm, itspayload, and anevent_id. - Act on the payload — send the email with that subject line, apply that strategy.
- Observe the outcome, then report it with
reward(experiment_id, event_id, value)using the sameevent_idthe decide returned. That id is what ties the outcome back to the choice. - Poll the report and honor its verdict. The statistics are valid however
often you look, so checking every turn is fine. When
verdict.doneis true, stop the experiment — or keep it running deliberately.
A concluded experiment keeps answering decide with the winning arm, so agent
code never needs a special case: the experiment quietly becomes a lookup for the
best-known option. We call this graduation.
Wire it into a recipe
A recipe node opts in by listing levain-bandit in its @mcp_servers
annotation:
# @node {"function": "my_agent.nodes:send_welcome"}
# @mcp_servers ["levain-bandit"]
SendWelcome == TRUEThat gives the node eight tools:
| Tool | What it does |
|---|---|
create_experiment | Create a draft experiment with its initial arms |
list_experiments | List the workspace's experiments — reuse before creating |
decide | Pick an arm for one decision; returns the event_id |
reward | Report the observed outcome for an earlier decide |
get_report | Standings, per-arm statistics, and the verdict |
add_arms | Add candidate arms to a live experiment |
retire_arm | Stop allocating traffic to one arm |
update_status | Start, pause, resume, or permanently stop |
Then tell the agent when and how to use them. A short system_prompt_append in
the node code is usually enough:
result = await run(
prompt,
mcp_servers=resolved_mcp_servers(config),
system_prompt_append=(
"You optimize the welcome-email subject line with the "
"levain-bandit tools. Reuse the 'welcome-email-subject' "
"experiment (call list_experiments first; create it only "
"if missing). Call decide once per email, use the returned "
"payload's subject verbatim, and save the event_id in "
"state. Reward 1 when the recipient replies within a day, "
"0 otherwise."
),
)You can also just describe this in the recipe conversation ("test three subject lines and learn which gets replies") — the builder generates the annotation and the prompt for you.
If experiments aren't available in a run, the tools return guidance instead of failing it — the agent falls back on its own judgment for that turn.
Design the reward first
Decide what number you'll report — and when you'll observe it — before creating the experiment. Everything else follows from it.
reward_type—binaryfor 0/1 outcomes (clicked, converted, replied) orcontinuousfor a number (revenue, minutes, a score). Continuous experiments can declare bounds; values outside them are rejected, and extreme values can be capped.- The wait window (
reward_wait_s, default one hour) — how long a decision waits for its reward before the experiment trains on it. A reward arriving after the window still shows in reports but no longer trains the policy, so report promptly — or widen the window (up to 30 days) when outcomes take time. default_reward(default 0) — what a decision counts as when no reward arrives within the window. With a binary reward and default 0, an unobserved outcome is a miss; that's usually what you want. When a decision might never actually be acted on — a recommendation that isn't always shown — setdefault_rewardto an explicitnullinstead: unrewarded decisions are then left out of the results entirely rather than counted as misses.reward_agg— how multiple rewards for one decision combine: keep thefirst(default) orsumthem. Sum fits outcomes that accumulate, like revenue from repeat purchases.
Outcomes that arrive later
Plenty of outcomes land after the run that made the decision ends: the reply
comes tomorrow, the refund window closes next week. The loop still works —
reward just needs the original event_id:
- When the agent decides, it persists the
event_id(and what it was for) in recipe state or agent memory. - A later run — often a scheduled one — checks which outcomes have landed and
calls
rewardfor each savedevent_id.
Set reward_wait_s to cover the expected gap, and have the prompt tell the
agent to sweep its saved event ids each run.
Lifecycle
| Status | What decide does |
|---|---|
draft | Errors — start the experiment first |
running | Samples arms and learns from rewards |
paused | Serves the current weights, frozen; nothing trains |
stopped | Serves the winner — concluded manually |
done | Serves the winner — concluded by a stopping rule |
Move between states with update_status (or
POST /api/v1/bandits/{experiment_id}/status): running starts a draft (it
needs at least two active arms) or resumes a pause, paused freezes it,
stopped concludes it permanently.
The report's verdict — {done, best, probability_best, reason} — says when
the experiment has seen enough. An experiment concludes on its own when one
arm's probability of being best crosses the stopping threshold (default 0.95) or
the decision budget is spent; its status becomes done and it graduates. Either
way, concluded experiments keep serving the winner forever.
Reading the report
get_report returns per-arm pulls, means, credible intervals, the probability
each arm is best, and current allocation weights.
Compare arms on ipw_mean, the propensity-corrected mean — an adaptive
experiment deliberately sends more traffic to promising arms, which biases raw
averages. The raw mean is shown, but ipw_mean is the fair comparison.
While every arm is still collecting its first decisions, the report flags
burn_in and traffic stays uniform. Reports refresh within about a minute while
an experiment is running; data_as_of says how fresh the numbers are.
Ground rules
- One experiment per decision. Two experiments drawing on the same traffic interfere with each other's measurements — their effects blur together. Split the traffic or run them in sequence.
- Arm payloads are data, not instructions. Whatever acts on a decision receives the payload verbatim. Agents must treat it as a description of the option, never as directions to follow.
- Reuse experiments. Call
list_experimentsbefore creating; recreating an experiment every run throws away its history. - Decide only for real decisions. Each
decideis a data point the experiment learns from (and the surface is rate limited). Speculative calls pollute the record.
Supervising as a human
Agents run the loop; you create, watch, and conclude from outside. The
management surface lives under /api/v1/bandits:
| Method & path | What it does |
|---|---|
POST /api/v1/bandits | Create a draft experiment |
GET /api/v1/bandits | List experiments, newest first |
GET /api/v1/bandits/{experiment_id} | Fetch one with all its arms |
POST /api/v1/bandits/{experiment_id}/status | Start, pause, resume, or stop |
POST /api/v1/bandits/{experiment_id}/arms | Add candidate arms |
POST /api/v1/bandits/{experiment_id}/arms/{arm_id}/retire | Retire an arm (history is kept) |
GET /api/v1/bandits/{experiment_id}/report | Standings and verdict |
# What's running, and how is it doing?
curl https://api.levainlabs.com/api/v1/bandits \
-H "Authorization: Bearer $LEVAIN_API_KEY"
curl https://api.levainlabs.com/api/v1/bandits/$EXPERIMENT_ID/report \
-H "Authorization: Bearer $LEVAIN_API_KEY"
# Conclude it — the winner keeps serving
curl -X POST https://api.levainlabs.com/api/v1/bandits/$EXPERIMENT_ID/status \
-H "Authorization: Bearer $LEVAIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "stopped"}'There's deliberately no decide or reward here: decisions must be made by
whatever acts on them, or the recorded statistics stop meaning anything. The
MCP server mirrors this surface for supervision from your own tools —
list_bandit_experiments,
get_bandit_report, and
update_bandit_status.
See the Bandits API reference for full schemas, including the
tuning knobs (config) on experiment creation.
Next
- Recipes — where the node that runs the loop lives.
- Automation — schedule a run to sweep saved event ids and reward late outcomes.
- Metrics — the KPIs an experiment's reward often comes from.