About experiment-stop
experiment-stop is a skill for stopping GrowthBook standard A/B tests and optionally declaring a winning variation. It requires an explicit user choice and can route traffic to the selected variation through a temporary rollout.
Key features
- 01Find and verify experiment status
- 02Choose a winner by variation ID
- 03Stop a running experiment
- 04Optionally enable temporary rollout
- 05Explain follow-up for the linked feature flag
Use cases
- 01End an active standard A/B test
- 02Record experiment results and analysis
- 03Stop a test and temporarily route traffic to its winner
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ growthbook/ skills --skill experiment-stopAfter installing, type/experiment-stopin Claude Code to confirm it loaded.
Details
From the project READMEexperiment-stop
Stop a running experiment, optionally declaring a winning variation and ramping it to all eligible traffic via a temporary rollout. The endpoint is POST /api/v1/experiments/<id>/stop (a dedicated endpoint, not the generic update). All variation references are variation ID strings like var_abc123, not 0-based integer indexes.
All API calls go through the bundled helper: ${CLAUDE_PLUGIN_ROOT}/scripts/gb-call. It needs GB_API_KEY — set in your shell, or written to ~/.config/growthbook/.env by /growthbook:gb-setup. If unset or invalid, gb-call's error message points back at /growthbook:gb-setup.
Workflow
-
Fetch the current experiment.
gb-call GET /api/v1/experiments/<experiment-id>If the user gave a name instead of an ID ("stop the checkout test"), resolve it first —
qmatches against name, tracking key, description, and hypothesis, andstatus=runningkeeps the candidates to experiments this skill can act on:gb-call GET '/api/v1/experiments?q=checkout&status=running'If more than one experiment plausibly matches, list the candidates and let the user pick — don't guess. Stopping the wrong experiment is not cleanly reversible.
Check the
statusfield. Onlyrunningexperiments should be stopped via this skill. Ifstatus === "draft", the experiment hasn't started — the user wants to delete it, not stop it (different operation). Ifstatus === "stopped", it's already done.Also capture the
typefield. Iftype === "multi-armed-bandit", halt and tell the user this skill targets standard A/B tests; bandits have their own lifecycle (stop is similar but interpretation and rollout differ — recommend they review in the UI before scripting). -
Show the user the variations table. Surface the variations as the API records them — capture each one's
variationId(the string ID, e.g.var_treatment_a) andname. The user picks by the variation ID string, not by index:The experiment has these variations: var_control 0: Control — <description> (lift: baseline, users: N) var_treatment_a 1: Treatment A — <description> (lift: +2.3%, users: N) var_treatment_b 2: Treatment B — <description> (lift: -0.8%, users: N) Which variation should ship? Reply with the variation ID (e.g. var_treatment_a), or say "no winner" to stop without declaring.If the user has not already run
experiment-analyze, suggest doing that first so the decision is informed. -
Decide on temporary rollout. If the user has a winner and wants to ship now, offer the temporary-rollout path:
"Want me to also enable a temporary rollout? That keeps this experiment in the SDK payload and forces 100% of eligible traffic to
<winnerVariationId>. It's the cleanest 'ship the winner' option — the linked feature flag's rule stays in place but routes everyone to the winner. You can toggle it off later withmodify-temporary-rolloutor clean up the rule viaflag-targeting."If yes, set
enableTemporaryRollout: trueandreleasedVariationId: <winner ID>in the payload. If no, the experiment stops but you leave the flag alone — surface that the user will need to clean up theexperiment-refrule manually. -
Confirm intent. Restate the action in plain English:
"Stopping experiment '', declaring variation
var_treatment_a(Treatment A) as the winner, and enabling temporary rollout to ship it to 100% of eligible traffic."Get explicit confirmation before posting. Stopping itself is reversible (you can restart), but declaring a winner and enabling a rollout produces downstream signals — don't do it on a hunch.
-
Build the payload. The body is flat; there is no nested
resultSummary.Stop without a declared winner:
{ "results": "inconclusive" }Stop with a declared winner, no rollout:
{ "results": "won", "winnerVariationId": "<winner variation ID>", "analysis": "<one-paragraph markdown summary of the decision>" }Stop with a declared winner and ship via temporary rollout:
{ "results": "won", "winnerVariationId": "<winner variation ID>", "releasedVariationId": "<winner variation ID>", "enableTemporaryRollout": true, "analysis": "<one-paragraph markdown summary of the decision>" }Field reference:
results— required. One of"won","lost","inconclusive","dnf"(did not finish).winnerVariationId— string variation ID (e.g.var_abc123). Required whenresults === "won"and the experiment has multiple test variations.releasedVariationId— string variation ID. Required whenenableTemporaryRollout: true. Usually equalswinnerVariationId.enableTemporaryRollout— boolean. Keeps the stopped experiment in the SDK payload and forces traffic to thereleasedVariationId.analysis— markdown summary shown on the experiment results page.reason— optional reason text stored on the latest phase metadata.dateEnded— optional ISO datetime; defaults to now.
-
Post the update.
echo '<payload-json>' | gb-call POST /api/v1/experiments/<experiment-id>/stop - -
State what happens next, and link to the experiment. Tell the user:
- The experiment is now stopped; no more traffic accumulates against the experiment.
- If a winner was declared, the variation ID that "won."
- Direct UI link so they can verify the stopped state, the recorded
analysis, and the rollout status:
Derive<host>/experiment/<experiment-id><host>fromGB_API_URLby swappingapi.→app.(matchesexperiment-launch's convention; on the default cloud host this produceshttps://app.growthbook.io). What happens to the flag? Surface the disposition clearly based on what was sent:
With temporary rollout (
enableTemporaryRollout: true): the winner is live — traffic is already routed to it via the existing experiment-ref rule. No further action required until the team decides to clean up the flag (which can happen days or weeks later). When ready:- Convert to permanent rule: remove the experiment-ref rule via
flag-rules, add a permanent force rule for the winner viaflag-targeting. - Clean up entirely: if the feature will be inlined in code, use
flag-cleanupto walk through code cleanup and archive/delete the flag. - Roll back: turn off the temporary rollout first (
echo '{"enableTemporaryRollout": false}' | gb-call POST /api/v1/experiments/<experiment-id>/modify-temporary-rollout -), then remove the experiment-ref rule viaflag-rules.
Without temporary rollout: the experiment-ref rule is still on the flag, routing traffic to a stopped experiment (users will get the control value). The flag needs attention:
- Option A — Ship the winner: set
defaultValueto the winner's value viaflag-default-value, then remove the experiment-ref rule viaflag-rules. Or useflag-targetingto add a permanent force rule serving the winner, then remove the experiment-ref rule. - Option B — Roll back: the flag's default value already serves the control — just remove the experiment-ref rule via
flag-rulesand the flag returns to its pre-experiment state. - Option C — Full cleanup: use
flag-cleanupto inline the value in code and archive/delete the flag.
Guardrails
winnerVariationIdis a variation ID string (e.g.var_abc123), not an integer index, not a name, not the variation'skey. Get this wrong and the request 400s or the wrong variation is recorded as the winner.- The endpoint is
POST /api/v1/experiments/<id>/stop, notPOST /api/v1/experiments/<id>. The body shape is flat — there is noresultSummarywrapper. The generic update endpoint exists but takes different fields; use the dedicated stop endpoint here. - Never declare a winner the user didn't pick. Even if the results look obvious, force the user to choose the variation ID. Surface results, but don't pre-fill. Skill convention, not GrowthBook policy: the API accepts any variation ID as
winnerVariationId; the safety is enforced here, not server-side. - Don't stop drafts. A
draftexperiment isn't running — what the user wants there isDELETE /api/v1/experiments/<id>(separate skill, not covered here). Surface the confusion if they ask to stop a draft. - Don't stop already-stopped experiments. The API may accept the call but it's effectively a no-op; tell the user it's already done. To change the results metadata on an already-stopped experiment, post again with the new
results/winnerVariationId/analysis. - Bandits are out of scope.
type === "multi-armed-bandit"experiments need different handling — halt and tell the user. releasedVariationIdis required whenenableTemporaryRollout: true. The API rejects the combination otherwise. They're usually the same aswinnerVariationIdbut don't have to be — e.g., a "lost" result that rolls everyone back to control would setreleasedVariationId: <control variation ID>withresults: "lost".- Always remind about the linked flag. Stopping the experiment does not remove the
experiment-refrule from the linked flag. Without a temporary rollout, the flag keeps routing to a stale experiment until the user cleans the rule up. analysisshould explain the decision in plain English (markdown). Future readers (including future-self) will want context. Don't leave it blank when declaring a winner.- Run
experiment-analyzefirst if the user hasn't. Stopping based on a glance at the dashboard is a common mistake — interim numbers can flip, and the data-quality checks inexperiment-analyzecan flag results that look conclusive but aren't. qrejects negation and operators with a 400. The list endpoint'sqparam takes the app's search syntax (status:running tag:checkoutplus free text) but hard-rejects!,~,^,>,<,=. Send plainfield:valuetokens and free text only.- When resolving by name, filter bandits with
bandits, nottype. The response'stypefield (standard/multi-armed-bandit) is not a list query param; sendingtype=returns a 400. Addbandits=falseto keep bandits out of the candidate list, since this skill halts on them anyway. (implementationTypeis a different axis — the linked-change kind.)
Endpoints used
GET /api/v1/experiments?q=<text>&status=running— resolve an experiment ID when the user only gave a name or keyword.qmatches name, tracking key, description, and hypothesis.GET /api/v1/experiments/<id>— fetch state, variations, andtypePOST /api/v1/experiments/<id>/stop— stop and optionally declare a winner + temporary rolloutPOST /api/v1/experiments/<id>/modify-temporary-rollout— toggle the temporary rollout off (or on) after stopping, without re-running this skill
Handoffs
experiment-analyze— run first if the user wants to interpret results before deciding.flag-rules— remove the experiment-ref rule after stopping (always needed eventually).flag-targeting— add a permanent force rule serving the winner value (replaces the experiment-ref rule).flag-default-value— set the flag's default to the winner value when shipping without a targeting rule.flag-cleanup— if the feature is fully shipped and the flag should be removed from code and archived.experiment-designandexperiment-launch— for the next test if this one informed a follow-up.