Run A/B tests from the CLI

The Adapty CLI runs regular A/B tests of paywalls or flows from the terminal, under the adapty ab-tests topic. You can create a test as a draft, start it on a placement for one audience, stop it with a winner, replace a live test with a changed one, and archive old tests.

Use it to let an AI agent run your tests, to script a test across several apps, or to start or stop a test without opening the dashboard.

A regular test splits the users of one audience on one placement between two or more paywalls, or between two or more flows. To run a test across several placements, use a crossplacement test in the dashboard. The ab-tests commands don’t return test results. To see the results, such as the probability to be best, use the dashboard: A/B test results and metrics. To read analytics for the users of one test, filter the analytics commands by the A/B tests dimension from adapty analytics catalog.

Before you start

The ab-tests topic uses the same installation and login as the rest of the CLI. If you have not set that up, follow steps 1 and 2 of the quickstart guide. The topic needs CLI version 0.14.0 or later. To see your version, run adapty --version.

Every command takes the app’s UUID in --app. To find it, run:

adapty apps list

A test uses content that already exists:

  • Flows: A flow joins a test only when it is published. A draft flow, or a flow with unpublished changes, is refused. To publish a flow, run adapty flows publish.
  • Paywalls: To create a paywall from products that are already in Adapty, run adapty paywalls create. To give a paywall a new design, use the Paywall Builder.
  • Placements: A paywall test runs on a paywall placement, and a flow test on a flow placement. To create a placement, run adapty placements create. Your app shows nothing from a new placement until its code fetches that placement.
  • Segments: To run a test for some users only, create a segment first with adapty segments create.

For these commands, see the command reference.

List and read tests

list shows flow tests unless you pass --type paywall. To see every test of an app, run it once for each type:

adapty ab-tests list --app <app-id> --type flow --page-size 100
adapty ab-tests list --app <app-id> --type paywall --page-size 100

A test is in one of these states: draft, live, completed, inactive, or archived.

To read one test with its arms, run:

adapty ab-tests get --app <app-id> <test-id>

In get --json, segment_ids is the test’s audience: [] is All users, and null is no audience planned yet. Each arm has its weight and the paywall or flow ID. To identify an arm, use paywall_title or flow_name. The arm’s title is empty unless you set one.

Create a test

A new test is a draft. No user sees a draft, so you can create and change one at any time.

  1. Find the IDs of the content to test, with adapty paywalls list or adapty flows list.

  2. Create the draft with two or more arms. Each arm is one paywall or flow with a weight. The weight is the percentage of users who see that arm, and the weights must add up to 100:

    adapty ab-tests create --app <app-id> --title "Annual vs monthly" \
      --goal "More revenue per user" --placement <placement-id> \
      --paywalls '[{"paywall_id":"<paywall-a>","weight":50},{"paywall_id":"<paywall-b>","weight":50}]'

    --placement is required. A paywall test needs a paywall placement, and a flow test needs a flow placement. For a flow test, pass --flows with flow_id in each arm instead. An arm can also have a title, such as "title":"A".

  3. Review the draft with ab-tests get.

The placement of a draft is only planned. create can also take a planned audience, with --segment or --all-users. These planned values have no effect on users. When you start the test, pass the placement and audience to start again.

Start a test

Warning

You can’t undo a start. From the moment the test starts, users of the audience you name no longer see their current paywall or flow.

To start a draft, name the placement and one audience: --all-users, or --segment with one segment ID:

adapty ab-tests start --app <app-id> <test-id> --placement <placement-id> --segment <segment-id>

The CLI prints what the start does and asks Apply? [y/N] before it sends anything. Where the test runs depends on the placement’s audiences:

  • The placement has an audience for this segment (or for All users): The test runs on that audience. A test that runs there now stops and moves to the completed state.
  • The placement has no audience for this segment: start adds a new audience with the highest priority on the placement. Users of this segment who currently match another audience move into the test.

To give the test a lower priority than another audience, first add the segment as an audience with that priority. Give it the paywall or flow that those users see now, then start the test on that audience. See Change audience priority.

A change on a segment audience can take up to 20 minutes to reach every user.

Only a draft test starts. To run a test again, create a new one: read the old test with get, then create a test with the same arms.

Change a test

update sends the whole test again. Pass every flag each time, including the required --placement. If you leave out a flag, Adapty clears its value: for example, the goal or the planned audience.

adapty ab-tests update --app <app-id> <test-id> --title "Annual vs monthly, Q4" \
  --goal "More revenue per user" --placement <placement-id> \
  --paywalls '[{"paywall_id":"<paywall-a>","weight":70},{"paywall_id":"<paywall-b>","weight":30}]'

On a live test, update changes only the title and the goal. Pass the arms, the placement, and the audience exactly as get returns them; other arms or another placement are refused. The test keeps its start time, and users keep their arms. You can’t change a completed or archived test.

To change the arms or weights of a live test, replace it. replace is the Modify action in the dashboard:

adapty ab-tests replace --app <app-id> <test-id> --title "Annual vs monthly v2" \
  --placement <placement-id> \
  --paywalls '[{"paywall_id":"<paywall-a>","weight":80},{"paywall_id":"<paywall-b>","weight":20}]'

The live test stops and moves to the completed state, and its results stay with its ID. A new test with a new ID starts on the same audience, and Adapty assigns every user of the audience to an arm again. If you pass a different placement in --placement, Adapty moves the audience to that placement. You can’t move an All users test.

Stop a test

stop ends the test. You choose what users of its audience see afterwards. Pass exactly one of these flags:

FlagAfter the stop
--winner <id>The whole audience sees one arm of the test. Pass the arm’s paywall ID or flow ID.
--other <id>The whole audience sees a paywall or flow that was not in the test, of the same kind as the test.
--no-contentThe audience leaves the placement, and its users get the next audience they match. Refused for an All users test.
adapty ab-tests stop --app <app-id> <test-id> --winner <paywall-b>

The test moves to the completed state. To test the same arms again, create a new test, as described in Start a test. To choose the winner, see A/B test results and metrics.

Archive a test

You can archive any test that isn’t live. To archive a live test, stop it first. You can’t restore an archived test.

adapty ab-tests archive --app <app-id> <test-id>

Confirmation in scripts and agents

start, stop, replace, and archive can’t be undone, so each one shows its effect before it sends anything. How it asks depends on how the command runs:

How the command runsWhat happens
In a terminalThe CLI prints the effect and asks Apply? [y/N]. If you answer y or yes, the CLI sends the request. For any other answer, the CLI exits with code 1 and sends nothing.
With --yesThe CLI sends the request without asking.
With --json, in a pipe, or with no terminalThe CLI sends nothing and exits with code 6. The error message describes the effect.

So a script or an agent can read the effect of a command first: run it without --yes, read the message, and run it again with --yes only after someone has approved that effect.

Errors

Exit codeCauseWhat to do
1You answered something other than y at the prompt. Nothing was sent.Run the command again if you meant to.
2The CLI rejected the input before sending it: an invalid UUID, arms that aren’t a JSON array, an arm without an ID or an integer weight, both or neither of --paywalls and --flows, or two stop options.Fix the flag that the message names.
3You aren’t logged in, or your Adapty user can’t access the app.Log in with adapty auth login, and find the app ID in adapty apps list.
4Adapty refused the request. The message gives the reason.See Messages from Adapty.
5The CLI couldn’t reach Adapty.Make sure you are online, and run the command again.
6A command that can’t be undone ran without --yes and with no one to answer. Nothing was sent.Read the effect in the message, then run the command again with --yes.
130You pressed Ctrl+C at the prompt. Nothing was sent.—

Messages from Adapty

MessageWhat to do
The sum of the weights must equal 100, but has N.Change the weights so they add up to 100.
Flow(<id>) must be published before adding to AB test.Publish the flow, then run the command again.
Paywalls cannot be changed in non-draft ab tests. or Placement cannot be changed in non-draft ab tests.Pass the arms and placement as get returns them, or use replace.
AB Test from default audience can not be stopped.An All users test needs a paywall or flow after the stop: use --winner or --other.
AB Test to stop must be live.Only a live test can be stopped or replaced.
Live Ab Test can not be deleted.Stop the test before you archive it.

Command reference

Every command takes --app, the app UUID from adapty apps list. Every command except list takes the test ID as its argument.

ab-tests list

FlagRequiredDescription
--typeNoflow (default) or paywall.
--stateNolive, draft, completed, inactive, or archived.
--placementNoOnly the tests of this placement (ID). Repeat the flag for several placements.
--segmentNoOnly the tests that run for exactly this segment (ID). Repeat the flag for several segments.
--searchNoOnly the tests whose title contains this text.
--pageNoPage number. Default: 1.
--page-sizeNoTests per page, up to 100. Default: 20.

ab-tests get

Returns the test with its arms. Takes no other flags.

ab-tests create

FlagRequiredDescription
--titleYesTest title.
--paywallsOne of the twoArms of a paywall test, as JSON: [{"paywall_id":"<id>","weight":50,"title":"A"}, …]. title is optional.
--flowsOne of the twoArms of a flow test, as JSON, with flow_id instead of paywall_id.
--goalNoWhat the test is meant to show.
--placementYesPlacement the test is planned for (ID). A paywall test needs a paywall placement, and a flow test needs a flow placement.
--segmentNoPlan the test for one segment (ID). Can’t be used with --all-users.
--all-usersNoPlan the test for All users.

ab-tests update

Takes the same flags as create, and --placement is required. A flag that you leave out is cleared. On a live test, only --title and --goal can change. Pass the same placement and audience again.

ab-tests start

FlagRequiredDescription
--placementYesPlacement to run the test on (ID).
--segmentOne of the twoRun the test for one segment (ID).
--all-usersOne of the twoRun the test for All users.
--yes, -yNoStart without asking. Required with --json or in a pipe.

ab-tests stop

FlagRequiredDescription
--winnerOne of the threeShow this arm’s paywall or flow (ID) to the whole audience.
--otherOne of the threeShow a paywall or flow that was not in the test (ID).
--no-contentOne of the threeRemove the audience from the placement. Refused for an All users test.
--yes, -yNoStop without asking. Required with --json or in a pipe.

ab-tests replace

FlagRequiredDescription
--titleYesTitle of the new test.
--placementYesPlacement of the new test (ID). A different placement moves the audience to that placement.
--paywalls or --flowsYesArms of the new test, as in create.
--goalNoWhat the new test is meant to show.
--yes, -yNoReplace without asking. Required with --json or in a pipe.

The new test takes the audience of the test it replaces, so replace takes no audience flag.

ab-tests archive

FlagRequiredDescription
--yes, -yNoArchive without asking. Required with --json or in a pipe.

What’s next