@segmently/cli
v1.7.0
Published
Segmently CLI for projects, funnels, publishing, and automation
Readme
Segmently CLI
First-party command-line interface for Segmently automation.
The CLI is JSON-first and talks to the stable /api/cli/v1/* facade. It does
not read Firestore directly and does not call scattered internal product
endpoints as its primary contract.
Installation
npm install -g @segmently/cli
segmently --version
segmently auth loginThe first public package version is @segmently/[email protected].
One-off execution without a global install:
npm exec --yes --package @segmently/cli -- segmently --versionProduction is the default target. Public users do not need to pass an environment flag.
Full installation and update instructions are documented in
modules/cli/INSTALLATION.md.
Authentication
segmently auth login
segmently auth status
segmently auth logoutProduction is the default environment. Public users do not need to pass
--env prod.
For automation, create a scoped project service token and use it through
SEGMENTLY_API_KEY.
segmently capabilities
segmently auth token create \
--project <projectId> \
--name "CI publish" \
--scopes projects:read,funnels:read,publish:write \
--expires-at 2026-06-25T10:00:00.000Z
SEGMENTLY_API_KEY=sgly.prod... segmently publish web --project <projectId> --web-placement <webPlacementId>
segmently auth token list --project <projectId>
segmently auth token revoke <tokenId> --project <projectId>For read-only launch checks, service tokens can use
funnels:read,themes:read,stripe:read,publish:read,analytics:read,domains:read,content-plan:read.
Tokens with publish:write also satisfy publish:read; tokens with
analytics:write also satisfy analytics:read; tokens with
content-plan:write also satisfy content-plan:read.
Image upload automation requires assets:write.
The raw service token is returned only once by auth token create. Store it in
the CI secret manager, not in the CLI credential file.
Service tokens created by regular project users are short-lived. If
--expires-at is omitted, Segmently sets the expiration automatically. The
create response includes the exact expiration time.
Service-token scopes are necessary but not sufficient for project operations.
Project-scoped product commands are also checked against Billing Access on the
server: Web Onboarding commands require web_onboarding.access or a wildcard
such as web_onboarding.*, block generation requires
web_onboarding.block.regenerate, and Content Plan commands require
content_plan.access or content_plan.*, and article commands require
articles.access or articles.* (the Segmently Articles subscription). CLI image uploads require
cli_assets.upload_image or cli_assets.*.
auth token create, auth token list, and auth token revoke require user
auth from browser login. They intentionally do not use SEGMENTLY_API_KEY,
because service tokens should not manage other service tokens.
Unit economics (offline, no account)
segmently ue runs the arithmetic of the public calculator
(https://www.segmently.ai/unit-economics) on your machine. Nothing is sent
anywhere and no login is needed; every result ends with a link that opens the
same scenario on the page. It prints a scenario, never a forecast.
npx -y @segmently/cli ue init my-app --product "Monthly~19.99~1month~none" --platform web
npx -y @segmently/cli ue evaluate .ue/my-app.json --explain
npx -y @segmently/cli ue link .ue/my-app.json
npx -y @segmently/cli ue parse "https://www.segmently.ai/unit-economics?v=2&…"
npx -y @segmently/cli ue --version # CLI version + the hashes of the bundled arithmetic and the served method
npx -y @segmently/cli ue check answer.md --project-file .ue/my-app.json # lint a finished answer before it is sent
npx -y @segmently/cli ue target .ue/my-app.json --goal roas=1.5 # what each lever must be, alone, to reach a goal
npx -y @segmently/cli ue plan .ue/my-app.json --growth 5 --months 24 # the scenario month by monthue init <slug> starts a project from your products: --product
<name>~<price>~<cadence>~<trial> is repeatable, and both long and short
cadence spellings are accepted (Monthly~19.99~1month~none and
Monthly~19.99~1m~none are the same product). A trial nobody stated is left
off (Monthly~19.99~1month): it is read as none, the product is recorded in
the project's unstatedTrials, and walk calls that none assumed ("not
stated — the CLI's init reads it as none") until the file carries a trial.
It writes .ue/<slug>.json, and that file is accepted anywhere the other verbs take a scenario —
evaluate, link, parse and rank all read a .ue/<slug>.json project
file the same way they read a plain scenario JSON file, a page link, or -
for stdin. ue parse of a project (the file, or its JSON) also prints the whole
project document as project, beside its scenario; any other input prints no
project key.
parse, evaluate, link, init, rank, check and every stat verb take --format table
for a human-readable view instead of JSON (ue --version does too); ue mcp is a server and has no table view.
ue evaluate --format table prints the same words the public page
prints for each figure (valuePerTap, cacPerPayer, profitPerStart,
roas, …), plus the readiness line and a unit column saying what each
figure is measured per. The JSON carries the same map as units: every
deducted.* amount is per_tap (per tap on Buy) and every waterfall row is
per_payer, so a figure is quoted in the unit its own field states.
ue evaluate --explain also prints walk, the scenario's inputs placed for a
boundary: walk.opener (the boundary's first sentence), walk.lines (one
reader-facing line per input the scenario carries, the chain's four rates on one
line per class — the chain, each product's
price, cadence, trial, take, charges, completion and trial conversion, the
volume, every deduction, the book's first renewal behind payback, observed
inputs — each marked measured or assumed with its source) and walk.items (the
same inputs as { id, class, source, text }). A take equal to the split
ue init proposes for the same products reads "assumed: the CLI's init split".
--explain also adds words.breakEvens: the break-evens as the answer prints them, one line
per knob of breakEvens.chain in the order cps, p1, p2, p3, each beside the value it starts from
in the scenario — "cost per start ≤ $0.90 (from $1.50)", "p3 ≥ 58.31 % (from 35 %)". A threshold
is rounded away from its claim, so the printed value itself clears: a rate's "≥" rounds up at two
decimals and the cost per start's "≤" rounds down to the cent (unlike the nearest rounding of the
other number words). A rate in breakEvens.unreachable reads "p2 — no conversion rate at that
step clears this scenario on its own (from 44 %)" instead of a figure above 100 %. A knob with no
break-even (nobody pays) gets no line. Charges per payer are not in the list: they are found by
sweeping paymentsCounted, one evaluate per point.
On a .ue/<slug>.json project file, ue evaluate also prints stage (read off
the file) and offers — the stage table's three next steps for that stage, the
same list ue init prints.
Every result that carries a link carries privacy beside it — ue link,
ue evaluate, ue init and ue rank (recommendation.privacy.first /
.runnerUp for its two lifted links): due (the link carries a figure that is
not the book's), carries (which of prices, chain, volume, takes,
charges, completion, trialConv, mechanics, deductions), pricesOnly
(only prices are not the book's) and sentence, the privacy sentence to print
on its own line above the link when due is true. Since 1.7.0 carries also
names goal (the link carries goal=) and dynamics (it carries dyn=): a
reader's target or plan figures are never the book's, so either makes it due.
Inputs are a JSON scenario file, a .ue/<slug>.json project file, a page
link, or - for stdin. The public page reads every key a minted link carries,
fee= (the provider scheme in its fourth field included), tax=, refunds=,
disputes=, activation=, fees=, basis=, measured= and upgrade=
among them, so a link opens there on the figures the CLI prints (checked on the
deployed page 2026-09-23). A key the page does not read raises one
page_shows_gross warning naming it; the warning says GROSS only when that
key is a deduction. goal= and dyn= (1.7.0, minted by ue target and
ue plan, and by ue link for a scenario that carries a goal or month-by-month
inputs) are not read by the deployed page yet: until its section «What it
takes» is live, the warning names them ("The public page does not read goal=
yet…") and the page opens the scenario without them.
Which experiment first — ue rank
ue rank answers "which test do I run first?" for one scenario. For each lever —
p1, p2, p3 (the funnel steps), close (payment completion), bought (the paid
share of buy-taps), mix (the take of the product whose share move gains the most),
trialConv and renewals — it moves that one input by a relative lift through the
calculator's own controls, evaluates the moved scenario once, and prints what the lift
is worth and what it costs to learn at your traffic. Every lifted value lands on the
calculator's own grid before it is evaluated (the same round trip ue link/ue evaluate
use); a landed lift ≠ the asked one carries snapped (within half a control step) or
clamped (further away) in warnings:
npx -y @segmently/cli ue rank .ue/my-app.json --override p3=0.25 # +25 % on paywall → buy-tap, +10 % elsewhereA script reads rows[] in the CLI's own order (learnable within the shortest horizon
first, then by gain per day of testing): per row lever, lift (the landed lift —
a control that clamps or snaps it says so in warnings), liftLabel (assumed lift for
ue rank's default, your belief for an --override or a typed --lift, null when the
lever did not move), gainPerMonth, roasAfter,
populationPerDay, populationName (the people this lever's test draws from, in words),
askedLift (the relative lift this row was tried at), nPerArm, days, spendRouted,
mde (keyed by horizon), realistic and note; then base (profitPerStart, roas,
startsPerDay), recommendation.first / recommendation.runnerUp (realistic rows that
gain, or null) with their round-trip-checked recommendation.links,
recommendation.mostProfit (the realistic row with the largest gainPerMonth > 0, or
null), and warnings. --lift, --horizon 30,90 and a repeatable
--override <lever>=<lift> change the defaults (+10 %, 30 and 90 days). A figure that
cannot be computed is null (n/a in the table).
boundary is the run's whole boundary, to print after the table: opener on its own line, then
each of lines as a bullet — the base's walk.lines (as ue evaluate --explain prints them for
the same input), sentences.beliefs, sentences.assumedLift, the horizon line, the 50/50 split
line (a card run: "the 50/50 split — assumed: the test design"), sentences.decidable and
sentences.notTestableSoon, a null one left out.
--horizon-default (with exactly one --horizon, else refused, field horizon-default) labels
that horizon the default nobody named: "the 30-day horizon — assumed: default horizon, none named".
sentences holds the run's own sentences, each filled only from that run's fields:
traffic, ranking, population, realism[], ceiling[], blocked[], price,
decidable, notTestableSoon, assumedLift, beliefs[], horizons, pick, mostProfit and
warnings[] (each warnings entry's message as a line of its own, in the same order;
pick — "p2 is the pick (recommendation.first), p1 the runner-up
(recommendation.runnerUp)." from the run's own recommendation, "…; there is no runner-up."
with none, "No lever that gains is learnable in this run — there is no pick." without a
first — to print after the last belowTable line and above the table's boundary;
horizons — "the 14-, 21- and 30-day horizons of this choice table — assumed: offered for
the choice, none picked yet" — only when --horizon was typed with more than one value, a
choice run; null for a single horizon and for a run with no --horizon), plus the table
itself: table[] (the markdown table of rows — header, separator, one line per row, each
cell the row's field rounded as the table view rounds it, the lift cell with its label and
any typed → landed, a | inside a cell escaped as \|) and belowTable[] (ranking,
population, realism[], ceiling[], blocked[], price, then warnings[]
— never mostProfit or pick: belowTable's order is pinned by UC-10/UC-11) — print traffic,
table, a blank line, then each belowTable line as a - bullet, in that order.
Repeatable --cost <lever=usd> and --odds <lever=p> price a believed lift for a lever
that already has its own --override (never rank's own default lift): the row gains bet
— gainPerMonthIfHolds (that row's gainPerMonth), cost and odds (each null when not
given — never defaulted), expectedGainPerMonth (odds × gainPerMonthIfHolds, null
without odds), costRecoveredInMonths (cost ÷ gainPerMonthIfHolds, null without a cost
or when the row does not gain) and sentence (all three, with their operands, in words), and
sentences.beliefs carries, right after that lever's belief line, one boundary line in the
same words — "p2: your cost to build $1,600.00 and your odds of 40 % that it holds —
assumed: your estimate" (either part alone when only one was given; no line without
either). A cost or odds for a lever without its own --override, odds outside 0–1, or a
negative or non-numeric cost is refused by field (cost / odds):
npx -y @segmently/cli ue rank .ue/my-app.json --horizon 21 --override p2=0.15 --cost p2=1600 --odds p2=0.4Only on an --override row, guardrail names the steps that must not drop while that
lever is tested: steps (the steps after the lever, in funnel order p1 → p2 → p3 →
bought → close → trialConv → renewals, minus any this scenario blocks — no trial
product, no trialConv), nearest (the first of them, null when no step follows),
cancellingDrop (the relative drop of nearest's rate at which the row's gainPerMonth
is 0, found on the calculator's own evaluation; null when there is no nearest step, the
row does not gain, or no drop inside that step's own control cancels it), horizon (the
shortest horizon), testDays (the length the test runs: the row's own days, or the
shortest horizon when the row has none), mde (the smallest change of nearest readable
within testDays; null for renewals, a calendar read, for a step whose trial clock
outlasts the test, or for a rate at 100 %), checkpointNPerArm (the people per arm on
nearest at which the cancelling drop is readable — the same n ue stat size gives for
that drop), checkpointDay (the day the test reaches that n on nearest, its trial clock
included), visible (checkpointDay ≤ testDays; false when the trial clock outlasts
the test; null with no cancelling drop or on renewals), watch and sentence (the
drop and whether it is readable within the test, in words: the day it becomes readable,
or when it is not, how long the test would have to run). Over ue mcp, ue_rank keeps
the earlier guardrail — mde and visible at the shortest horizon, no testDays or
checkpoint.
With exactly one --override, --card <file> also writes that lever's lifted scenario —
the one its row was evaluated on — to <file> as a scenario document (parent folders
created, an existing file overwritten — never a ue_project file), labelled with the landed lift and a measuredOn
of "believed lift, not observed, ", and adds card: prepared[] (each field the
lever moved, "chain.p2 0.61 × 1.15 = 0.7015 — your numbers, prepared"), snapped[]
(where the calculator's grid put it, "chain.p2 0.7015 → 0.7"), evaluateCommand,
linkCommand and roasTie (the file's ROAS equals the row's roasAfter — checked
before it is printed). No --override, a second one, -, the rank input itself, a project
file, a lever that does not move in this scenario or an unwritable path is refused by field
(card):
npx -y @segmently/cli ue rank .ue/my-app.json --horizon 21 --override p2=0.15 --card .ue/my-app/card-p2.jsonExperiment sizing — ue stat
ue stat sizes and reads a two-arm test: size (sample size per arm for a relative
lift over a baseline rate), mde (the smallest relative change readable at a population
per day over a number of days), read (a finished test's verdict from each arm's
n/x) and means (sample size for a difference of means, given σ):
npx -y @segmently/cli ue stat size --p 0.35 --lift 0.10 # nPerArm 2978
npx -y @segmently/cli ue stat mde --p 0.35 --per-day 148 --days 30
npx -y @segmently/cli ue stat read --a 5000/1500 --b 5000/1700
npx -y @segmently/cli ue stat read --a 1864/708 --b 1864/772 --project-file .ue/my-app.json --lever p1 --horizon 21
npx -y @segmently/cli ue stat read --a 1864/708 --b 1864/772 --project-file .ue/my-app.json --lever p1 --rebase
npx -y @segmently/cli ue stat read --a 1000/957 --b 1000/975 --project-file .ue/my-app.json --lever close --horizon 21
npx -y @segmently/cli ue stat means --sigma 40 --delta 4A script reads nPerArm and pTarget (size), mde (mde, null when even a rate of
100 % is not readable in that window), read.verdict (significant, not yet or
underpowered), read.ci95 (the 95 % interval on the difference), read.needPerArm
(read), and nPerArm (means); every result carries warnings. A request that cannot
be sized is refused by field, never answered: a target rate p × (1 + lift) at or above 1,
a lift or a delta too small to give a finite sample size, or a malformed number
(--p 0.3abc) — exit 1, stderr { "error": { "code": "input_refused", "field", "message" } }.
ue stat read --project-file <file> --lever <lever> [--horizon <days>] reads the
test against the project's own rate for a rank lever and adds context: lever,
subject (how the rate is named: "p1", "close (payment completion)", "mix (Annual's
take)"), product (mix only: the product whose take the read counts, else null),
today (the project's rate before the test), drift ("p1 scenario 38 % → observed
41.42 % (3.42 points higher)"), realizedLift (read.pB ÷ today − 1, six decimals, on a
significant verdict only), realizedGainCommand (the ue rank --override command for
that lift; needs --horizon, and a lift above 0 — a significant drop has none, since
--override takes a lift > 0 only), rebaseCommand (this same read plus --rebase, the command
an MCP host — whose server writes no files — hands the reader to run in their own terminal;
on a significant read of a ue_project file only: null for -, a link, JSON text or a
scenario file, and on the --rebase run itself), walkLines (the project's walk.lines) and
boundaryAdds (the read's own boundary lines, after walkLines; the first opens with
subject). --horizon without --project-file and --lever is refused (field
horizon), and so is --project <file> (field project-file): -p, --project is the
root's Segmently project id, never this file.
context.boundary is the read's whole boundary (opener, then lines: walkLines,
boundaryAdds, the card's belief line, the horizon, the 50/50 split, and on a card's read — with
--belief — the card's table run's notTestableSoon, "in the card's -day table run"). Every read also prints
read.lines: the nine fields as one line, then context.drift and context.more.sentence or
context.realized.sentence with its warnings.
--belief <lift> (the card's believed lift as typed; needs --horizon, field belief) adds the
card's belief line (from the card's own rank run) and its table run's line to context.boundary.
--horizon-default (needs --horizon, field horizon-default) labels the horizon the default
nobody named.
--lever takes every rank lever but renewals, and today is the rate ue rank lifts for
it, so the counts must count that same rate. p1–p3 read a chain rate: n people reach
the step, x pass it. close reads the paywall's take-weighted completion (the paid share
over the pick share, all paywall products as one rate: n picked a paid product, x
paid); bought reads the paid share of buy-taps (n buy-taps, x paid purchases). mix
reads the take of the product ue rank's own mix row shifts picks toward (n buy-taps,
x picks of that product); trialConv reads the trial products' take-weighted trial →
paid (n trial starters, x paid). renewals is refused (field lever) with what to
read instead: charges counted per payer are a calendar read, not a share of n — read
them over a billing period (RevenueCat subscription_retention → paymentsCounted) and
write them into the scenario. A lever ue rank blocks on this scenario (no one picks, no
one buys, fewer than two products for mix, no trial product for trialConv) is refused
with rank's own reason, and any other name by field lever. mix never has a
realized-gain run — ue rank --override mix=<lift> picks its target again at that lift,
so the run could move another product's take — so its realized and
realizedGainCommand are null, and its realized effect is the re-base's lines.
context.more (a read that is not significant; null on a significant one) says how
many more people the observed difference needs: needPerArm (read.needPerArm),
smallerArm, morePerArm, populationPerDay (the lever's populationPerDay in ue rank
on this project), maturityDays (the lever's trial clock — trialConv's longest trial + 3,
else 0), days (the more per arm over half that population a day, plus maturityDays)
and a sentence to copy. Arms that read the same rate have no sample size: needPerArm,
morePerArm and days are null, and the sentence says to record the test as not yet.
context.realized (a significant read with --horizon and a lift above 0, else null)
is the realized-gain ue rank run the CLI makes itself on the project before any re-base:
its landed lift, gainPerMonth, roasAfter (the re-based ROAS), warnings and
assumedLift (that run's sentences), and a sentence; when the grid moves the lift as
printed (two decimals; a landing that prints the typed figure is none), boundaryAdds
carries the landing line ("typed +8.99 % → landed +9.21 % (snapped)", or
the row's reason, "(below the control's step)", when the grid holds the lever at its rate
— then the sentence says so and quotes the row's note, never "$0.00 a month").
boundaryAdds also carries that run's assumedLift line, last — right after the landing
line, else right after the realized-lift line — so a boundary that copies boundaryAdds
whole walks the other levers' default lifts; context.realized.assumedLift keeps the same
text. A significant drop has no realized-gain run: its boundary line reads "the realized
lift on p1 −0.096454 (no realized-gain run: ue rank takes a lift above 0)", and no
assumedLift line follows it (nor on mix, a read that is not significant, or one
without --horizon).
--rebase (CLI only; the MCP server writes no files) re-bases the project file on a
significant read, after context is built. It moves the old scenario into variants[]
("base before the p1 A/B read"), appends the counts to observed[] as an ab_test
document and a decision with the read's boundary to decisions[], and sets the lever at
the read. A chain lever gets read.pB as printed (the loader lands it on the grid); every
other lever moves exactly the way ue rank lifts it, by the factor read.pB ÷ today:
each paywall product's completionRate (close, held between 0.3 and 1) or trialConv
(trialConv, held between 0.02 and 0.95) times that factor, the paywall takes scaled until
the paid share is read.pB (bought, capped where the takes reach 100 % of buy-taps), or
the target's take set to read.pB with the other takes giving it up in proportion (mix,
capped where the target holds every pick; its observed[] entry also names the product).
A re-base the control or the calculator's range stops short of read.pB (a cap, a floor, a
chain rate clamped into range) says so in its decision instead of recording read.pB as the
re-based rate. It sets measuredOn to "p1 A/B read: 1864/708 vs
1864/772", adding "; before: " when the whole fits in 80 characters. It bumps
updatedAt and re-derives stage. context.rebase prints file, measuredOn,
updatedAt, variantName, prepared (one line per field written: "chain.p1 0.38 →
0.414163 (read.pB as printed)", or the factor and then "Monthly with trial's
completionRate 0.95 × 1.019164 = 0.968206", "(capped at 1)" or "(floored at 0.3)" when
the control stopped it, "(capped: the takes reach 100 % of buy-taps)" or "(capped: Annual
holds every pick)" on a capped share move), snapped (each written value's landing on the
grid, or "(clamped to the calculator's range)"), lines (base → re-based profit per start,
ROAS, CAC per payer, payback and stage), evaluateCommand and linkCommand. The
re-based project is evaluated before it is written, and written through a temporary file
and a rename. It is refused, with the file left byte-identical, on a read that is not
significant, on a read already in observed[] (the same lever and counts: never applied
twice), on anything but a ue_project file on disk (a scenario file, - or a link: only a
project has variants[]) — each field rebase — and without --project-file (field
project-file).
ue stat watch --project-file <file> --lever <lever> --a <n>/<x> --b <n>/<x> [--guardrail
<step>=<control n>/<x>,<variant n>/<x>]… [--today <step>=<control n>/<x>,<variant n>/<x>]…
[--date YYYY-MM-DD] [--card <file>] [--belief <spec>] is the daily check of a RUNNING test
(CLI-only — no ue_watch MCP tool, ue mcp unchanged). It reads, never writes, the project's
experiment ledger: it needs a ue exp started entry for --lever and refuses by name
(ue exp start) without one. --a/--b are the primary step's cumulative counts, control and
variant — for progress only; the watch never prints the primary's rate, lift, p-value or
verdict, only its n per arm against the plan and the day the read becomes due (readOn); once
the smaller arm reaches the planned nPerArm it prints the one ue stat read … --lever command
to run. Each --guardrail <step>=… (cumulative counts, repeatable; p1 p2 p3 bought close) is
checked by three rules: (1) crash — a full day's --today count of zero conversions on the
variant → stop, "funnel broken", no statistic printed for that step. The crash rule's
expectation is the base's rate (the card's, on a step the change moves) × the day's own arrivals
in the variant — never the plan's population, which is often the book's: 0 converted → stop
at 20 or more expected, a warning naming both arms' arrivals from 5, and below 5 an ordinary
day, noted on the line (expectedPerArm is that expectation). On cumulative counts the same
arithmetic stops a variant at 0 of n (rate × n ≥ 20) and warns when both arms sit at 0 % with
that support (the funnel or its tracking broken in both). (2) a significant drop past the drop
that cancels the gain — pValue below 0.05 ÷ plannedDays (a Bonferroni correction over the
planned daily looks, named once in lines as a stand-in for a sequential boundary) and the
observed drop at or past the step's cancellingDrop → stop. The daily look's bar is 0.05 ÷
the planned days: on a plan of many days the kill switch is in practice the crash rule. (3) a
significant but smaller drop → warning, and the CLI runs a net check — a calculator
evaluation of a variant carrying both the primary's and the guardrail's observed rates — whose
profitPerStart against the base's decides whether the warning escalates to stop, "gain
cancelled downstream". Anything else is noise, printed with today's readable threshold (mde
at the elapsed days) and the n the cancelling drop needs for its read at the planned end — never
"fine". A step the change itself moves by design gets the crash check only (its drop is the
primary's: no statistic, no drop search, no net check) and is otherwise unwatchable, as is a
calendar step (trialConv before its clock, renewals, charges: no daily counters). On
status: stop the output carries the ue exp close <project> <id> --reason "<reason>" command;
the watch itself writes no file.
Lint an answer — ue check
ue check <file|-> [--project-file <file>] [--runs <dir>] [--expect <case:block>] lints a
finished answer before it is sent and prints { pass, findings[], warnings }, each finding
{ rule, line, message, fix }; a completeness finding also carries file. Its thirteen rules
are the nine below and the four 1.4.2 adds after them: banned (the forecast / prediction
strings and computed ratios), approximation (~, ≈, ±, "about", "roughly" … before a
number), privacy (a link that carries the reader's numbers with no privacy sentence on its
own line above it), call_above_link
(the ue link call, or the ue rank run, above each link), round_trip (a link that does
not decode, or is not the one the CLI mints, or carries no measured= — the
measured_missing warning its ue link gives), boundary_opener (a verdict in an answer that
carries no verbatim boundary at all — a card's bet.sentence or guardrail.sentence line and ue
rank's sentences.pick included; a verdict after the last boundary is restated_verdict's),
measured_missing (a line that reports the measured_missing warning while no link after it
carries measured=; a line that names the re-mint — "→ re-minted with measured=…" — while any link
of the answer carries it passes), walk_rates (with --project-file: every boundary names every
chain rate
of the project's walk, or of a scenario its variants[] holds — the pre-test base after a
re-base) and provenance (with --project-file: a walk line whose text is one of the base
walk's items keeps a class the base's walk or a variant's files it under — "the fee … —
assumed: the book's preset" where the walk says "measured" is flagged, with the walk's own
line to copy; a session line — a belief, the assumed lift, the horizon, the split, the
counts, boundaryAdds — and a variant's own edit (a proposed product, a corridor corner, a
sweep point) are never checked; the one residual: a labelled sweep whose first point equals
a base value the base files as measured is flagged; a line walked "measured" whose tail says
"believed lift" — a card file's own ue evaluate --explain walk copied in place of
card.walkLines — is flagged too). With --project-file, the reader's own
product names, label and measuredOn inside a copied line are theirs, never read as
banned or approximation.
1.4.2 adds --runs <dir> (a directory holding the JSON output of each run the answer prints,
one file per run, or a single run file) and --expect <case:block>, and four rules:
restated_verdict (on every check: a verdict line after the answer's last boundary that is
neither a question nor a next step), completeness (with --runs: every line the answer owes
of those runs — the walk, the break-evens and the rank table once it shows a line of them, the
rank run's warnings and boundary, the card's lines, read.lines, the read's boundary and its
sentences, the re-base lines, and the privacy sentence above the run's own link — is in the
answer, whitespace collapsed and backticks ignored, at most 20 findings per run; its file
names the run — in --format table a column shown only when a finding carries one),
expected_block (with --expect,
e.g. uc-11:card: every label of that block — the card's fields (1)–(10) — opens a line) and
unresolved_slot (on every check: a {{…}} slot marker outside fenced code — a worked
example's placeholder, never an answer's). A slip is pass: false with findings, exit 0; only
an answer, project or run file that cannot be read, an unknown --expect key — or a
--project <file>, the root's project id (field project-file) — is refused, by field,
with a fix.
Goal seek and month by month — ue target, ue plan
ue target <input> --goal <roas|payback|cac|profit>=<value> answers "what would it
take": for a ROAS of at least 1.50×, a payback by month 6, a CAC of at most $40 or a
profit per start of at least $1, what each lever would have to be on its own —
the cost per start, the three chain rates, tapped Buy → bought, each product's price
and charges counted, trial → paid — holding every other number. It is the public
page's Goal fold: the rows are the core's own solve and every line is the page's
words for the same state.
npx -y @segmently/cli ue target .ue/my-app.json --goal roas=1.5
npx -y @segmently/cli ue target "https://www.segmently.ai/unit-economics?v=2&…&goal=roas~1.50" # the link's goal, no flagThe JSON a script reads: goal (kind, value, basis — ROAS and profit read the
tiles' 12-month/first-charge and gross/net basis, payback twelve months of charges,
CAC no value), current (the metric now; null when nobody pays, or a payback not
inside twelve months), met, reachCount, leverCount, rows[] (lever —
cps, p1, p2, p3, bought, price:<id>, checks:<id>, trial:<id>, or
trial for "no free trial on this paywall" — label, current, required (a
number exactly when state is reach or holds, else null, never 0), state
(reach, holds, holds_any, unreachable, no_effect, no_trial,
no_reading), figure and line), lines (the reading as the page prints it —
print them whole), privacy, link (the scenario with goal=<kind>~<value>,
round-tripped like ue link's) and warnings.
ue plan <input> [--growth <pct>] [--months 12|18|24] [--mrr <usd>] [--by <month>]
[--cost-rises <pct>] is the page's Month-by-month fold: the ad budget growing
--growth % a month (0–30; 30.5 lands on 30 with a clamped warning), each month's
new payers renewing like the first, for 12, 18 or 24 months (past month 12 the
renewal tail is an assumption, assumed: true). --mrr watches for an MRR (the
month it is reached); with --by <month> it is goal mode: the growth, the paywall →
tapped Buy rate and the month-1 budget that each reach that MRR by that month, alone,
with their cash cost. --cost-rises <pct> assumes the cost per start rises that
many percent each time the budget doubles (month 1 never moves).
npx -y @segmently/cli ue plan .ue/my-app.json --growth 5 --months 24 --mrr 100000
npx -y @segmently/cli ue plan .ue/my-app.json --growth 5 --months 24 --mrr 150000 --by 12The JSON a script reads: dynamics (the inputs this run read), months[]
(month, budget, cps, newPayers, active, mrr, cashIn, flow,
cumulative, assumed — every payer figure null when nobody pays),
milestones (flowPositive — the first month that collects more than it spends on
ads — low { month, cumulative } or null when cash never goes below zero,
neverBelowZero, backAboveZero, mrrReached; null when nobody pays), totals
(spend, cumulative), month1Budget, cac, basis, goal (goal mode: mrr,
byMonth, atTarget, met, levers[] with required, cash and cashLine;
otherwise null), lines, privacy, link (with dyn=… while any input differs
from its default) and warnings. It prints a scenario month by month, not a plan:
nothing in it is measured.
An input whose document or link already carries a goal (goal=) or month-by-month
inputs (dyn=) needs no flag; a flag replaces the input's value and an overridden
warning says so. Both verbs read the same inputs as every other verb (a file, a
.ue/<slug>.json project file, a page link, or - for stdin), take --format
table (one row per lever / per month, then the answer's lines), and refuse by
field with a fix: goal (no goal anywhere, an unknown kind, a ROAS at or below
0, a payback outside 1–12, a CAC at or below $0, a value that is not a number),
months, growth, mrr, by (outside the horizon, or with no MRR to reach) and
cost-rises (outside 0–100). Both are CLI-only: ue mcp has no tool for them.
MCP
The same seven handlers (evaluate, link, parse, init, rank, stat, check)
plus an eighth, ue_guide, are reachable as eight MCP tools over stdio — the CLI already
ships the server, nothing extra to install. The server carries the method: its
instructions, ue_guide (the segmently-unit-economics skill, topic by topic) and
four prompts (launch-card, trial-or-not, store-vs-web, hypothesis-card) work
without that skill installed anywhere.
Every tool result carries a second text block, next: …, the step guidance for that
tool's result; until the connection has called ue_guide once, it opens with "Call
ue_guide {} first, then the topic of this case." ue_check takes the answer text
(answer), an optional project, and optional runs (the run results as the JSON texts
the tools returned) and expect; ue_stat's read takes the same context as
request.context (project, lever, horizon, and belief / horizonDefault for
--belief / --horizon-default). ue_rank takes "card": true where
the CLI takes --card <file>: the same UC-11 card comes back inline — its scenario
document as card.scenario, in place of the file and its two commands, and field 5's
block whole as card.lines — and no file is written; "horizonDefault": true is
--horizon-default. The server's serverInfo.version is the CLI version ue --version prints.
claude mcp add ue -- npx -y @segmently/cli ue mcp
codex mcp add ue -- npx -y @segmently/cli ue mcpClaude Desktop and Cursor: add the same command under mcpServers in their MCP
configuration file:
{
"mcpServers": {
"ue": { "command": "npx", "args": ["-y", "@segmently/cli", "ue", "mcp"] }
}
}claude.ai in the browser and ChatGPT need a remote server — not offered here; this server speaks stdio only, spawned as a local process by the host above.
1.5.0
Structural levers — ue rank --lever <spec> (price:, trial-add:, trial-remove:,
trial-paid:, mix:, downsell:, upsell:, upgrade:), --belief, --variant <file>,
--together (2–3 arms), --metrics-only; each structural row prints its break-even when the
change has one (searched on the model's grid) — or that it clears at every point / never
clears —, the threshold test that reads it, links to the base and the variant and a short
difference. ue stat read --lever <spec> reads the test against its threshold (holds /
fails / not_enough, --arm, --rebase on holds). ue exp list|start|close keeps the
project's experiment ledger (experiments[]). ue evaluate adds pools and a readiness
clause per live mechanic. The MCP server (ue mcp) is unchanged — the same tools, schemas and
method as 1.4.2.
1.6.0
ue rank's nearest guardrail is judged at the test's own length (the row's days when it has
one, else the horizon): guardrail.testDays, mde at that length, checkpointNPerArm (the
per-arm n at which the cancelling drop is readable) and checkpointDay decide guardrail.visible
— a guardrail whose step is still blind at the test's planned length says so and names the day it
would become readable, or how many days to extend to. ue stat watch is new: the daily check of
a running test with a kill switch (CLI-only — ue mcp and its tools are unchanged, byte-identical
to 1.4.2's recording).
1.7.0
Goal seek and month by month: ue target (what each lever must be, on its own, to reach a ROAS,
a payback month, a CAC or a profit per start) and ue plan (the scenario month by month, with
goal mode's three levers) — the public page's section «What it takes», words and all; their links
carry goal= / dyn=, which ue link and ue parse read and write too. The chain rates p1,
p2, p3 now reach 100 % (a skipped funnel step is entered as 100 %): a value above 95 % is no
longer clamped — trial → paid still stops at 95 %. ue check owes a ue target / ue plan run's
lines whole and reads "around month 4" as an approximation. ue mcp keeps its eight tools and
its 1.4.2 method; its outputs are unchanged except for an input that sets a chain rate above 95 %,
which now lands as typed.
Environments
segmently env list
segmently env currentProduction is the public default. env list shows only the public production
target.
Shell Completion
segmently completion bash > ~/.local/share/bash-completion/completions/segmently
segmently completion zsh > ~/.zfunc/_segmently
segmently completion fish > ~/.config/fish/completions/segmently.fishcompletion is local-only: it does not call the backend and does not require
credentials.
Examples
segmently projects list
segmently funnels list --project <projectId>
segmently funnels versions <funnelId> --project <projectId>
segmently funnels export <funnelId> <versionId> --project <projectId> --output flow.json
segmently funnels audit <funnelId> <versionId> --project <projectId> --required-variable goal,name,email --require-paywall
segmently funnels diff --project <projectId> --source-funnel <funnelId> --source-version <versionId> --target-funnel <funnelId> --target-version <versionId> --explain
segmently funnels graph dry-run --project <projectId> --funnel <funnelId> --version-id <versionId> --file graph.json
segmently funnels graph apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file graph.json
segmently funnels screens apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file screens.json
segmently funnels variables apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file variables.json --dry-run
segmently funnels edges apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file edges.json
segmently funnels conditions apply --project <projectId> --funnel <funnelId> --version-id <versionId> --file conditions.json
segmently stripe products --project <projectId> --mode test
segmently launch preflight --project <projectId> --funnel <funnelId> --version-id <versionId> --web-placement <webPlacementId> --required-variable goal,name,email --require-paywall --require-payment-provider
segmently analytics settings get --project <projectId>
segmently analytics settings apply --project <projectId> --file analytics-settings.json --merge
segmently analytics probe --project <projectId> --required-platform facebook_pixel,facebook_capi --required-url-param fbclid,utm_source,utm_medium,utm_campaign --require-client-side --require-server-side
segmently domains status --project <projectId>
segmently domains verify --project <projectId>
segmently domains home set <alias> --surface article --project <projectId>
segmently domains home clear --surface article --project <projectId>
segmently domains base-url verify https://www.example.com/blog --project <projectId>
segmently domains base-url set https://www.example.com/blog --project <projectId>
segmently domains base-url clear --project <projectId>
segmently assets upload-image ./hero.png --project <projectId> --raw-url
segmently assets upload-image ./hero.png --project <projectId> --asset
segmently assets upload-image ./hero.png --project <projectId> --folder onboarding/reference
segmently content-plan strategies list --project <projectId> --include-counts
segmently content-plan strategies get <strategyId> --project <projectId> --include-posts --output content-plan-strategy.json
segmently content-plan strategies audit <strategyId> --project <projectId> --required-platform linkedin,x --require-generated-posts
segmently content-plan posts schedule <postId> --project <projectId> --author <authorId> --strategy <strategyId> --scheduled-at <iso> --platform facebook --dry-run
segmently content-plan profile export --project <projectId> --author <authorId> --platform linkedin,x --output creator-profile.json
segmently content-plan profile apply --project <projectId> --file creator-profile.json --dry-run
segmently content-plan pillars apply --project <projectId> --author <authorId> --file pillars.json --dry-run
segmently content-plan post-templates apply linkedin --project <projectId> --author <authorId> --file linkedin-templates.json --dry-run
segmently fixtures launch seed --project <projectId> --alias-prefix qa-launch --name "QA Launch" --dry-run
segmently support evidence from-run --file run-artifact.json --output evidence.md
segmently generate block --project <projectId> --file request.json
segmently tasks get <taskId> --project <projectId>
segmently publish web --project <projectId> --web-placement <webPlacementId>
segmently publish web --project <projectId> --funnel <funnelId> --version <versionId> --alias <alias>
segmently publish verify --project <projectId> --url /ab-launch --public-base-url https://api.segmently.ai --query-param fbclid=fb-test,utm_source=facebook --required-variant control,treatment
segmently ab-tests list --project <projectId> --status published --tag copy-test --baseline-variant control --iteration iter-2026-05
segmently ab-tests publications <abTestId> --project <projectId> --tag copy-test --baseline-variant control --iteration iter-2026-05Articles on your main domain (path mode)
domains base-url serves the article site at a path of your own site (for example https://www.example.com/blog)
through your reverse proxy. It acts on the article domain only (--surface article, the default); --surface web
is refused before any request.
base-url verify <url>is a dry check (writes nothing, CLI-only): it validates the URL and confirms it reaches this project through your proxy. Prints{ ok, projectId, publicBaseUrl }. Failures carry a code such asbase_url_wrong_project,base_url_not_proxiedorbase_url_unreachable;rate_limitedmeans wait and retry.base-url set <url>/base-url clearstore the setting, rewrite the site files and republish every live article. The CLI drives the switch job: it calls the server step endpoint repeatedly, prints progress to stderr and finally prints{ mode, publicBaseUrl, homeAlias, results, relatedRefresh, warnings, summary }wheresummaryis"N republished, M skipped, K failed". A live article with unpublished edits (or one retired meanwhile) is skipped with acanonical stays <url> until republishednote.clearin subdomain mode printsnote: "already in subdomain mode"and exits 0.- Exit code 1 when any article failed to republish (run the same command again) or the related-articles refresh
failed (run
segmently content-plan articles related refresh). - Ctrl-C leaves the job running on the server. Run the same
set/clearcommand again: the server answersmode_switch_in_progress, and the CLI resumes the running job. If the running job is a different switch than the one you asked for, it is finished, the CLI exits 1 and tells you to re-run yours.
assets upload-image calls the CLI facade route
/api/cli/v1/projects/:projectId/assets/upload/image, which reuses the existing
Builder Assets upload and resize pipeline. By default, uploaded files are stored
under projects/<projectId>/cli-assets/images/.... The normal JSON response
includes the original URL, the small URL, the full legacy assetUrl variants,
and the V2 asset shape { "original": "...", "small": "..." }.
The command requires assets:write on service tokens plus project Billing
Access capability cli_assets.upload_image.
Paddle catalog and sandbox paywall products (requires matching backend support)
segmently paddle status --project <projectId>
segmently paddle products --project <projectId> --environment sandbox
segmently paddle prices --project <projectId> --product <paddleProductId> --environment sandboxCatalog commands default to Paddle sandbox; --environment production selects the
merchant's production catalog independently of the Segmently API environment. Omitting
--product lists all active prices. Status returns saved connection metadata for both
Paddle environments plus cached webhook health when the backend supports it. The
connection flag is not a live credential, checkout or wallet verification. Use
hasApiKeySecretRef, hasClientToken and hasWebhookSecret for saved-field presence
within each environment. These booleans do not expose values or prove that a referenced
secret can be read. Older backends omit the first two fields; omission means unknown.
Inspect webhookStatus.status, coverage and measuredAt before using the cached counters: a
configured secret does not prove receipt, receipt does not prove downstream delivery,
and reading status does not refresh the measurement. A project
without a saved Paddle connection returns paddle_not_connected.
These project-scoped reads require paddle:read and Web Onboarding access. paddle:write implies read access and also permits sandbox paywall-product ensure. Stripe scopes do not grant Paddle access. Tokens and
secret references are never part of status or catalog responses.
Inspect one exact checkout hostname separately (requires a backend with domain-probe support):
segmently paddle status --project <projectId> --checkout-domain <checkoutHostname> --environment sandboxThis optional read returns checkoutDomain with its environment, observation time,
website approval and Apple Pay domain verification. Supply a hostname without a scheme,
path or port. The domain check defaults to Sandbox; --environment on status requires
--checkout-domain. The Paddle API key needs checkout_domain.read, in addition to the
Segmently paddle:read access described above. Missing provider permission, transient
failure or disconnected selected environment yields actionable unknown evidence and
does not change the saved connection flag. An older backend that ignores the query is
reported as unsupported rather than a successful domain check.
The API returns related domains too; only an exact hostname match is accepted here.
not_found means no exact record was returned, not that an existing Sandbox card
checkout has failed. Apple Pay domain verification is separate from device eligibility;
walletDeviceAvailability remains unknown. Google Pay does not use Apple's domain
verification. This command does not verify a merchant return policy or execute a payment.
See Paddle checkout domains
and Express checkout.
Create a Segmently paywall product from an existing Paddle Sandbox price:
segmently paddle paywall-product ensure --project <projectId> --name "Monthly" --price <paddlePriceId> --environment sandbox
segmently paddle paywall-product ensure --project <projectId> --file paddle-product.json{
"version": "cli.paddle-paywall-product.v1",
"name": "Monthly",
"paddlePriceId": "pri_example",
"paddleEnvironment": "sandbox"
}Use a file or named options, not both. Optional type is derived from the verified
price when absent; an explicit mismatch is rejected. paddleProductId may assert the
expected parent product. Named equivalents include --type, --product,
--description and --checkout-mode.
Ensure creates a Segmently reference, never a Paddle product/price or a production
product. Repeating a compatible normalized name returns the existing id with
created:false, without updating display copy or rechecking the provider price.
Conflicting price/type/product/mode returns conflict. Current stored checkout aliases
are redirect (bakes to overlay) and embedded (bakes to inline); these are not proof
that true merchant-page redirect or inline runtime acceptance is complete.
Provider-neutral launch preflight
launch preflight --require-payment-provider checks saved Stripe test or Paddle Sandbox
connection metadata. --require-stripe-sandbox remains a deprecated alias for one release.
Both flags select the same neutral check; neither establishes live credential or payment
readiness.
For bound Paddle products, a selected placement also produces the separate
paddle.checkout.prerequisites row on supporting backends. It uses the placement policy
and only referenced products, checks an exact HTTPS domain for production/Express when
available, and retains unknown device/return-policy evidence as a warning. No placement
scope means the mode check is skipped. A configured account can therefore pass the
connection check while its checkout still needs review. Stripe-only flows are unchanged.
This check does not execute a payment or verify webhook delivery.
For service tokens, preflight requires all of funnels:read, themes:read,
publish:read, plus at least one effective stripe:read or paddle:read scope.
paddle:write satisfies Paddle read. Web Onboarding Billing Access is also required.
The capabilities response expresses common requirements in scopes and alternatives
in anyOfScopes; both conditions must be met.
Only granted provider connection metadata appears in the report. An ungranted provider
is null in evidence.providers, with restrictedProviders explaining the missing
visibility. It is not a disconnected provider and cannot satisfy the connection check.
The deprecated stripe.sandbox.connected row mirrors the neutral verdict, while Stripe
account metadata stays hidden from Paddle-only tokens. Provider catalog commands still
require their own scope.
