A segment is a set of filters, all of which a user must match. An audience is one entry in
a placement: one segment (or none), one paywall or flow, one priority. The placement checks its
audiences from the lowest priority up and serves the first one the user matches; the default
audience (segment_ids: []) is last and catches everyone else. Every change this skill makes is
live for real users the moment the placement is written.
This skill owns segments and audiences. It does not own:
- The paywall or flow itself →
flow-generatorbuilds and publishes it. An audience can only point at content that exists, and a flow only once it ispublished. - Moving paywall placements to flows →
migrate-placements. - Segments built from Apple Search Ads campaigns, ad groups or keywords →
ads-manager, workflow 13 (asa segments createresolves the Apple ids and keeps the dashboard's naming). Come back here to route the segment it returns. - An attribute the app has to send (custom attributes, age, gender, customer user ID) →
adapty-integrationowns the SDK call. A segment over an attribute nobody sets matches nobody. - A/B tests on an audience — not in the CLI. Send the user to https://adapty.io/docs/add-audience-paywall-ab-test.
How you sound
- "I" for what you do, "you" for what they do. Lead with who will see what, end on one next step.
- Name users, not ids. "Users in Germany and Austria", "users on 2.0.0 or later". Ids appear only inside a command.
- Before a write, show the placement's audiences before and after, as a table in priority order: segment, what it serves. That table is the confirmation.
- Bold at most one thing per paragraph. Reply in the user's language.
The CLI
Resolve $ADAPTY once, as the other Adapty skills do: adapty --version; if it lacks the command
you need, npm i -g adapty@latest, falling back to npx --yes adapty@latest. In zsh a
multi-word $ADAPTY needs setopt shwordsplit. Write commands out for the user with a literal
adapty.
segments list and segments get are old. segments create and segments update ship after
0.9.3 — probe $ADAPTY segments create --help before planning a write. Two refusals are not flag
problems, so do not retry with variations:
unknown command→ the installed CLI predates the command; re-resolve$ADAPTY.405→ the server has not opened segment writes yet. Build the segment in the dashboard (https://adapty.io/docs/segments), read its id withsegments list, and continue at step 5.
| Command | What it does |
|---|---|
segments list --app <app> --page-size 100 |
id, title, description only — no filters |
segments get --app <app> <id> --json |
adds filters[], profile_count, refresh_in_progress |
segments create --app <app> --title <t> --filter <f> [--filter <f>…] [--description <d>] |
filters are ANDed |
segments update --app <app> <id> --title <t> --filter <f> […] |
replaces the whole segment |
placements list --app <app> --page-size 100 --json |
the default page is 20, so a short list hides placements |
placements get --app <app> <id> --json |
title, developer_id, audiences[] |
placements update --app <app> <id> --title <t> --developer-id <d> --audiences '<json>' |
replaces every audience |
--app is the Adapty app UUID from $ADAPTY apps list. A filter is field:OPERATOR:value[,value…]:
IN, NOT IN, >, >=, <, <=, BETWEEN, or field:IS NULL / field:IS NOT NULL with no
value. There is no =: equality is IN with one value (ip_country:IN:DE). For a typed or
nested value, pass one JSON object:
'{"field_name":"age","operator":">","value_list":[18]}'. Numbers go in plain (age:>:18).
Field names come from evidence, never from a guess
The server rejects an unknown field name, which looks like a safety net and is not one: the dangerous mistake is a valid name with the wrong meaning, and that one is accepted silently. Take a field name from one of these, in order:
- Confirmed names:
ip_country,install_date,age,total_revenue_usd, and the attribution fieldscampaign,ad_group,creative. - An existing segment that already filters on it.
segments list, thensegments get --jsonon the candidates, and copyfield_name,operatorand the value format fromfilters[]. This is also how a custom attribute's name and type suffix are found. - Neither: ask the user to build the segment they actually need in the dashboard — not a throwaway one to copy a name from — then reuse it by id. If more segments on that attribute will follow, read the field name off it (step 2) and create the rest through the CLI. Never iterate guesses against the server.
Ambiguous requests — ask, with the options
These look like one attribute and are two, or have a format that fails silently. Ask before building, and say what each option means. Labels are the dashboard's (https://adapty.io/docs/segments).
| The user says | The choice | Why it matters |
|---|---|---|
| "users in Germany" | Country (latest IP, refreshed weekly, moves with VPNs and travel) or Country from store account (iOS 13+ only) | Pricing and legal copy usually want the store country |
| "on version 2.0", "updated to 2.0" | App version (current) or App version (on install) | "On install" silently drops everyone who upgraded |
| any app version | Strict SemVer: 2.0.0, never 2.0 or 02.1.0 |
An invalid version matches nobody, with no error |
| "never paid", "non-payers" | Which signal: total revenue, subscription state, paid access level | Comparisons and NOT IN skip profiles where the attribute is unset, and filters in one segment are ANDed — so "zero or unset" is two segments in two audiences, never one segment with both filters |
| a language | 2-letter ISO 639-1 (pt, not pt-BR) |
Extended locales match nobody |
| two conditions | Both (one segment, two filters) or either (two segments, two audiences) | One audience holds one segment, so "or" is two entries |
Workflow
- Read the placement.
placements list --page-size 100 --json, matchdeveloper_id, thenplacements get --json. Recordtitle,developer_id, and every audience. Note thecontent_typethe entries carry: it is fixed for the placement. Asked to serve a flow at apaywallplacement, or the reverse? The placement cannot; stop and route tomigrate-placements. - Translate the request into filters with the table above. Ask every question it raises in one message.
- Reuse before create.
segments list --page-size 100, thensegments get --jsonon any title that could match, and comparefilters[]. A segment with the same filters is the one to use — say so. Never create a twin. - Create if nothing matches, with a title that says who is in it ("Germany & Austria, store
country").
profile_countis0on creation and fills in later, so it proves nothing yet. - Place the new audience directly before the default. That is the only position where
nobody who matches an existing segment changes what they see. If the request means the new
audience should win over an existing one ("instead of what they see now", a user who could be
in both), name the overlapping segments and ask which should win. Renumber priorities
0…nwith the default last. - Build every entry with its
content_typeand the matching id:{"content_type":"paywall", "paywall_id":…}or{"content_type":"flow","flow_id":…}, plussegment_ids(one id, or[]for the default) andpriority. The CLI refuses an entry without them before sending. - Confirm with the before/after table. Then write all entries back with one
placements update, reusingtitleanddeveloper_idexactly. - Verify with
placements get --json: same entries, your new one where you put it, one default, last. - Close with who sees what now, and two facts: the segment's count fills in after a delay
(new profiles and attribute changes reach Adapty with a delay), and any attribute the app
must send is
adapty-integration's job.
Never
- Never change what a user outside the request sees without their explicit answer — not when the user said "go ahead", not when the order "seems right". Moving a new audience above an existing one is exactly that change.
- Never write a placement without passing back every audience you read. An entry you omit is an entry you deleted.
- Never guess a field name, a custom attribute's spelling, or a value format. See the three sources above.
- Never put a flow id in
paywall_id, or an entry withoutcontent_type. - Never
segments updatewith only the filter you changed — it replaces the segment; read it and pass back every filter you keep. A segment can serve several placements and A/B tests, so name that before editing a shared one; a new segment is often the safer change. - Never use
placements --paywall-id. It rewrites every audience into one default.
Rationalizations
| What the agent told itself | What is actually true |
|---|---|
| "The user said to go ahead without checking back, so I run the writes without asking and send a summary message after." | Their yes covers their request. It does not cover moving VIP users off the VIP paywall, which is what putting the new audience first does to anyone in both segments. |
| "I put the new segment at priority 0 so DE/AT users reliably get the cheaper paywall." | Priority 0 also outranks every existing segment. "Before the default" is the position that changes nobody else. |
| "If the server rejects a field name or value, it reports one error per field. I'd fix the names from that error and re-run, without guessing further." | A wrong name is refused; a right name with the wrong meaning is accepted. Read the name off an existing segment. |
"app_version, with value 2.0." |
Versions are strict SemVer. 2.0 matches nobody, with no error. Write 2.0.0, and settle current versus on-install first. |
"I put the flow id in the audience's paywall_id field. The reference covers only paywalls" |
Every entry carries content_type, and a flow entry carries flow_id. The placement's own entries show which one it takes. |
Quotes are verbatim from baseline runs without this skill (2026-10-12).
Red Flags — STOP
- A new audience with a lower
prioritynumber than an existing segment's, and no answer from the user about who wins - An
--audiencesarray with fewer entries thanplacements getreturned, plus one - A
field_namethat is not in the confirmed list and was not read off an existing segment - An app version with fewer than three parts
- "Country" in a filter and no answer to "IP or store account?"
placements listwithout--page-size 100
Anything not covered here
$ADAPTY <topic> <command> --help for flags on the installed version; the adapty-docs skill for
any dashboard behaviour. Do not guess a flag, a field name, or a URL.