Command-line interface for the Helio Public API. Create tests, pull reports, manage participants, and automate workflows from the terminal.
# Run without installing
npx @zurb/helio-cli --help
# Or install globally
npm install -g @zurb/helio-cliRequires Node.js >= 22.
The CLI checks the npm registry for new versions at most once a day and prints a notice on stderr when one is available. Because the notice never touches stdout, it shows up in --output json and in piped or redirected output too — --output json stays parseable, and scripts and agents can still find out the tool has moved. ANSI colors are used only when stderr is a TTY. To update:
helio-cli updatehelio-cli update --check reports whether a newer version exists without installing. If the CLI was installed with pnpm, Yarn, Volta, or run via npx, update prints the matching command instead of using npm. helio-cli doctor also includes a "CLI version" check that warns when a newer version is available. Set HELIO_NO_UPDATE_CHECK=1 (any value other than 0, false, or empty) to disable the background check entirely (doctor then reports the check as disabled); it is also skipped when CI is set.
Get your API ID and Token from my.helio.app/account/organization (scroll to the API section).
# Interactive login
helio-cli auth login
# Or set credentials directly
helio-cli config set api-id YOUR_API_ID
helio-cli config set api-token YOUR_API_TOKEN
# Or use environment variables
export HELIO_API_ID=your_id
export HELIO_API_TOKEN=your_token
# Verify everything works
helio-cli doctorCredential precedence (first wins):
- CLI flags (
--api-id,--api-token) - Environment variables (
HELIO_API_ID,HELIO_API_TOKEN) - Config file (
~/.helio-cli/config.json)
helio-cli auth login # authenticate
helio-cli projects list --output json # find your project
helio-cli tests ux-metric-types --output json # pick metrics FIRST — they build validated, scored sections
helio-cli tests create --dry-run ... # validate before creating
helio-cli tests create ... # create draft (--ux-metrics first, --questions for the rest)
helio-cli tests preview <id> # verify structure
helio-cli tests walkthrough <id> # see what a participant experiences, step by step
helio-cli tests send <id> # launch
helio-cli tests report <id> --output json # get resultshelio-cli guide prints this and the rest of the walkthrough from the binary itself; helio-cli guide --output json is the machine-readable version for agents.
# Projects
helio-cli projects list
helio-cli projects list --name "UX Research"
helio-cli projects get <project-uuid>
helio-cli projects tests <project-uuid>
# Tests
helio-cli tests list
helio-cli tests list --status running
helio-cli tests list --status running complete --tags ux
helio-cli tests get <test-uuid>Pick your UX metrics first. A tagged metric auto-builds its sections with validated, non-leading wording and returns a 0–100 score with a threshold label — comparable across waves and rolled into the test's Overall Score. A question you hand-write to resemble a metric returns only an answer distribution and scores nothing. Use --questions for what no metric covers.
# See what's available before writing anything
helio-cli tests ux-metric-types
helio-cli tests ux-metric-types --type sentiment
# Create a test (saved as draft) — metrics first, custom questions for the rest
helio-cli tests create \
--project-id <uuid> \
--name "My Study" \
--intro "Help us improve our product" \
--target-audience-size 50 \
--ux-metrics sentiment loyalty \
--ux-metric-context "the signup flow" \
--questions '[{"type":"free_response","instructions":"What would you improve?"}]'
# Validate without creating
helio-cli tests create --dry-run \
--project-id <uuid> --name "Test" --intro "Hi" \
--target-audience-size 50 --ux-metrics sentiment loyaltyNo hand-written NPS in that example on purpose: --ux-metrics loyalty builds it, and unlike a typed-out NPS it scores.
Starting stacks — from the Helio test corpus, if you don't know which metrics to pick:
| Situation | Metrics | Note |
|---|---|---|
| First look, one screen | comprehension desirability intent engagement |
engagement needs an image + hotspots |
| Findability, any stage | success sentiment |
success needs an image + hotspots; the corpus pairs this with effort, still web-app only |
| Iteration on a flow | expectations success sentiment |
success needs an image + hotspots |
Only completion and effort are still web-app only — both are built from a Figma prototype section the API can't create. Everything else, including the click-backed metrics and brand_score, is CLI-creatable as of the 2026-07-30 API release. See UX Metrics.
The same metric type may appear more than once. Each instance owns its own sections and is scored separately — what a multi-screen flow measuring expectations on every screen needs:
helio-cli tests create ... --ux-metrics expectations sentiment expectationsThat gap is narrower than it looks, and it narrowed further on 2026-07-30 when five more metrics became creatable. Read the whole coverage list before deciding a question is custom — the overlap is usually with a metric you skipped, not the one you checked. completion and effort are the only web-app-only metrics left; don't approximate those with a hand-written question either.
Genuinely custom means your own domain — pricing, features, workflow specifics, "how did you pay", "which plan fits you" — or a question type no metric emits: ranking, matrix, card_sort, point_allocation, max_diff.
# Add questions one at a time
helio-cli tests add-question <test-uuid> \
--type multiple_choice \
--instructions "How did you pay?" \
--choices "Credit card" "PayPal" "Apple Pay"
helio-cli tests add-question <test-uuid> \
--type likert --instructions "How important is same-day delivery to you?" \
--scale-type importance
# A preference question's options are variations — one full-size image each
helio-cli tests add-question <test-uuid> \
--type preference --instructions "Which do you prefer?" \
--variations '[{"name":"Data In","asset_id":61016},
{"name":"The Vertical","asset_id":61017}]'
# Preview and launch
helio-cli tests preview <test-uuid>
helio-cli tests send <test-uuid>
# Panel tests start recruiting; customer-list tests enqueue invites. A 502 on
# send means the panel rejected the quota (audience too narrow): nothing is
# charged, the test stays draft, and retrying after adjusting is safe.
# Walk through the test the way a participant sees it
helio-cli tests walkthrough <test-uuid> # one screen per page, all at once
helio-cli tests walkthrough <test-uuid> --interactive # advance one screen at a time (TTY required)
helio-cli tests walkthrough <test-uuid> --output json # structured screen list
# Read what real respondents actually answered, one journey at a time
helio-cli tests participants <test-uuid> # transcript per respondent
helio-cli tests participants <test-uuid> --group-by cohort # cluster by cohort
helio-cli tests participants <test-uuid> --sentiment negative --output jsonpreview is a structural summary (every question on one page), and it reads back everything a scripted build writes, so a pre-launch review can verify its own work: the test's audience configuration and device targeting, its UX metrics (which metric owns which questions, with the metric uuids that reorder and remove-ux-metrics accept), per-question branching, stimulus, image display timer and skipped introduction card, and click-test hotspots. Metric-owned questions are tagged inline (e.g. — sentiment metric) — they are auto-generated and structurally locked, so edit them through the metric, not as hand-written questions. --output json emits ux_metrics, audience, branching and hotspots blocks; walkthrough carries branching and hotspots on each screen. A click section with no hotspots is called out, since on a hotspot-scored metric that means it scores zero. walkthrough renders each participant screen separately — intro, then each question with its own input UI (radio buttons, text box, NPS row, etc.) — so you can comprehend the experience step by step. Asset-heavy types (prototypes, click tests, tree tests) render a placeholder pointing to the Helio browser preview.
participants is the report seen one respondent at a time: where walkthrough shows the empty test structure and tests report shows aggregates, participants stitches each person's answers together in order — the rating, the follow-up "why", and that answer's sentiment, plus demographics, audience type, and cohorts. It accepts the same demographic/segment/sentiment filters as report, supports --group-by cohort|audience_type, and emits flat { study, participants: [...] } JSON for piping into jq. It's a convenience wrapper over tests report --include participants. Note: cohorts is empty for non-enroll recruits, and sentiment / prototype grade are eventually consistent — a null means "not computed yet" (shown as pending), never neutral.
# Upload an image (jpg, jpeg, png, gif; max 10MB)
helio-cli assets upload ./homepage-mock.png
# Find asset ids to use with --asset-id
helio-cli assets list
helio-cli assets list --type image --name homepage
# Check processing status and get signed URLs
helio-cli assets get <asset-id>
# Attach an image as a question stimulus
helio-cli tests add-question <test-uuid> \
--type free_response \
--instructions "What stands out on this page?" \
--asset-id <asset-id>
# Most question types take one — and can skip the question introduction card
helio-cli tests add-question <test-uuid> \
--type likert --scale-type agreement \
--instructions "This label tells me what the button does." \
--asset-id <asset-id> \
--skip-question-intro
# A flash test: show the image for 5 seconds, then hide it and ask
helio-cli tests add-question <test-uuid> \
--type multiple_choice \
--instructions "What was the page selling?" \
--choices "Shoes" "Insurance" "Travel" \
--asset-id <image-asset-id> \
--display-seconds 5Uploads return immediately with status: "processing"; poll assets get <asset-id> until status is complete to get dimensions and URLs. Asset ids are numeric (unlike test/project uuids).
A question-level --asset-id is the stimulus shown with the question, and free_response, multiple_choice, likert, nps, ranking, matrix, point_allocation, max_diff and click_test all take one. Preference questions carry their images per option instead (--variations) and card sort has no stimulus slot, so both reject it. --site-link is saved on free_response and click_test only. Before the 2026-09-15 Public API release (zurb/helio#5039), only free_response and click_test saved an asset_id — the other types took it and silently dropped it — so check tests preview, which reads back each question's stimulus.
--skip-question-intro sets disable_instruction_card, the editor's "Skip question introduction": the participant opens straight on the stimulus and answers instead of seeing the question text on its own card first. edit-question --type recreates the section, so pass the flag again to keep it; a UX metric section can't be given it through the API.
--display-seconds (display_seconds in questions JSON) is the editor's "Image Display Time": the participant sees the question's image for 5, 10 or 15 seconds, then it is hidden and the question is revealed. It is how long the image stays up, not a limit on how long they have to answer. It needs an image --asset-id on the same question (a video or audio asset is rejected), and it applies to free_response, multiple_choice, likert, nps, ranking, matrix, point_allocation and max_diff. Preference, card sort and click test reject it, since their take screens never hide the image. As with the introduction card, edit-question --type rebuilds the question, so leaving the flag off turns the timer off, and a UX metric section can't be given one through the API. Needs the 2026-09-19 Public API release (zurb/helio#5048).
free_response, multiple_choice, likert, nps, ranking, preference, matrix, card_sort, point_allocation, max_diff
PascalCase variants also accepted (e.g., FreeResponse, MultipleChoice).
preference is the one type whose options are not choices. They are
variations — one full-size image each — which is why GET /tests/:id has
always read them back under variations, and since the 2026-08-23 API release
that is the key writes take too. Each option is a plain string or an object
{name (or text), asset_id?, site_link?}, and the two forms can be mixed:
asset_id must be an image asset on the test's own account (assets upload). An option with no image still saves as a draft, but leaves an empty
image slot that tests validate reports as a launch blocker — preview and
walkthrough both flag it. choices still works on a preference question as a
legacy alias, so existing scripts keep running; passing both is an error, and
variations on any other question type is rejected with a pointer to choices.
Likert scales: agreement, occurrence, importance, quality, comprehension, impression, expectations, usefulness, difficulty, likelihood, custom
Auto-generate standardized measurement questions. Each tagged metric produces a 0–100 score with a threshold label that is comparable across waves and feeds the test's Overall Score — which is why they come first, before any hand-written question (see Creating Tests).
# Add metrics during test creation
helio-cli tests create \
--project-id <uuid> \
--name "UX Study" \
--intro "Help us evaluate the experience" \
--target-audience-size 50 \
--ux-metrics sentiment loyalty \
--questions '[{"type": "free_response", "instructions": "What did you think?"}]'
# Customize metric wording
helio-cli tests create ... \
--ux-metrics sentiment loyalty \
--ux-metric-context "the Helio dashboard"
# Metrics-only test (no custom questions)
helio-cli tests create \
--project-id <uuid> \
--name "Quick Pulse" \
--intro "Quick feedback" \
--target-audience-size 50 \
--ux-metrics sentiment appeal usefulness
# Object form: per-metric context and per-section overrides (instructions, assets, follow-ups)
helio-cli tests create ... \
--ux-metrics-json '[{"type":"sentiment","context":"the checkout flow","sections":[{"followup":{"question":"Why?","required":true}}]}]'
# Add/remove metrics on existing drafts
helio-cli tests add-ux-metrics <test-uuid> --metrics comprehension loyalty
helio-cli tests add-ux-metrics <test-uuid> --position 2 --metrics-json '[{"type":"sentiment","context":"the checkout flow"}]'
# A TYPE removes every instance of it; a metric id removes just that one
helio-cli tests remove-ux-metrics <test-uuid> --metrics comprehension
helio-cli tests remove-ux-metrics <test-uuid> --metrics <metric-uuid>
# View available metric types
helio-cli tests ux-metric-typesAvailable types, and what each one already asks. Read all eleven before hand-writing a question — the overlap is usually with a metric you skipped, not the one you checked.
| Metric | Covers |
|---|---|
sentiment |
which words describe their impression |
feeling |
which emotions it evokes |
appeal |
overall impression (Likert) |
reaction |
overall impression (Likert) — appeal's twin |
comprehension |
how well they understood it |
frequency |
how often they use it |
loyalty |
would they recommend it (NPS) |
intent |
what they'd most likely do next |
desirability |
impression words + how likely to purchase |
usefulness |
is it useful + does it make things easier to get done |
expectations |
what they expected + how well it was met |
engagement |
where they would go first — click test: image + hotspots |
success |
can they find the one thing — click test: image + hotspots |
usability |
three find-it tasks in a row — 3 click tests: image + hotspots each |
satisfaction |
a find-it task plus how completing it felt — click test: image + hotspots |
brand_score |
do they know you, what impressions you give, would they recommend you — needs brand_choice |
helio-cli tests ux-metric-types --type <name> prints the exact wording a metric generates.
The five bolded types launch fine and measure nothing unless you supply the missing piece. A click-backed metric scores clicks that land inside a hotspot, and brand_score reads its recognition score off the choice you flag as your brand.
helio-cli tests create ... --ux-metrics-json '[
{"type":"success","sections":[
{"asset_id":"<image asset>",
"instructions":"Click where you would check out.",
"hotspots":[{"name":"Checkout","x":0.4,"y":0.5,"width":0.2,"height":0.1}]}]},
{"type":"brand_score","sections":[
{"choices":["Helio","UserTesting","Maze","Other"],"brand_choice":0}]}
]'
# Or fill them in on an existing draft
helio-cli tests edit-question <test-uuid> <section-uuid> \
--hotspots '[{"name":"Checkout","x":0.4,"y":0.5,"width":0.2,"height":0.1}]'
helio-cli tests edit-question <test-uuid> <section-uuid> --brand-choice 0
# Catch what still scores zero before you spend answers on it
helio-cli tests validate <test-uuid>Hotspot coordinates are relative to the image (0–1); x/y are the top-left corner, and x + width / y + height must each stay within 1. Clicks are tracked by absolute coordinates — a click scores only if it falls inside a hotspot rectangle, and nothing snaps to the nearest target — so pad each box out past the visual edges of the element, or clicks plainly aimed at it score as misses. The rule is about the gap between box and element, not hotspot size: a large component needs that slack as much as a small one. create and add-ux-metrics warn when a metric is missing its overrides; validate refuses to pass one.
Per-section override keys: instructions, asset_id, site_link, followup, hotspots, choices, brand_choice. Overrides apply in template order, so sections[0] is the metric's first section. Most metric choice lists are fixed length; brand_score's market recognition list (sections[0]) is resizable down to 2.
Not creatable from the CLI — and rejected by the Public API too, not just client-side: completion and effort. Each is built from a Figma prototype section the API cannot create, so build them in the Helio web app. As of the 2026-07-30 API release these are the only two left out; engagement, success, usability, satisfaction and brand_score used to be excluded and no longer are.
# Summary results
helio-cli tests report <test-uuid>
# Full report
helio-cli tests report <test-uuid> \
--include questions_summary,questions_responses,demographics,ux_metrics
# Filter by demographics
helio-cli tests report <test-uuid> \
--gender Female --age 25-34 35-44
# Paginate responses
helio-cli tests report <test-uuid> \
--include questions_responses --limit 50 --offset 100
# Raw response data
helio-cli tests responses <test-uuid>--include values: questions_summary, questions_followups, questions_responses, audiences_summary, demographics, ux_metrics, prototype_journeys, filter_options
# Custom lists
helio-cli custom-lists list
helio-cli custom-lists participants <list-uuid>
# Add participants
helio-cli participants create \
--email user@example.com \
--customer-list-id <list-uuid> \
--full-name "Jane Doe"
# Bulk add
helio-cli custom-lists add-participants <list-uuid> \
--data '[{"email": "a@b.com", "full_name": "A B"},
{"email": "c@d.com", "full_name": "C D"}]'# Edit a question on a draft
helio-cli tests edit-question <test-uuid> <section-uuid> \
--type free_response --instructions "Updated question"
# Insert a question at a specific position, with a follow-up
helio-cli tests add-question <test-uuid> \
--type multiple_choice --instructions "Pick one" --choices "A" "B" "C" \
--position 2 --followup "Why?" --followup-for-choices 0 2
# Safe edits on UX metric sections (omit --type): instructions, assets,
# choice text (count must match the template; intent may resize down to 3),
# randomize, follow-ups
helio-cli tests edit-question <test-uuid> <metric-section-uuid> \
--choices "Sign up" "Browse pricing" "Leave site" \
--followup "What drove your answer?"
helio-cli tests edit-question <test-uuid> <metric-section-uuid> --remove-followup
# Remove a question
helio-cli tests remove-question <test-uuid> <section-uuid>
# View and reorder questions
helio-cli tests order <test-uuid>
helio-cli tests reorder <test-uuid> \
--order "metric:sentiment" "section:<q1-uuid>" "section:<q2-uuid>"order lists one block per UX metric instance, not per type. A metric type that is on the test more than once must be addressed as metric:<uuid> — metric:<type> can't say which instance you mean and is rejected. order prints the uuid keys directly, since GET /tests/:id returns metric uuids as of the 2026-08 API release; only against older API deploys are repeated-type blocks flagged as unaddressable.
Five audience types decide who takes the test — pick one at create time or replace it later. Default is open (share link; you distribute the URL yourself). The other four recruit for you: basic (Helio panel, no filters), targeted (panel + demographics), advanced (saved panel segments), customer_list (your own lists).
# Find audience ids — they are exactly what --audiences accepts
helio-cli audiences list --name "designers" --source enroll # Helio panel segments (advanced)
helio-cli audiences list --recent # your customer lists, recently used first
helio-cli audiences get <id>
helio-cli audiences clone <id>
# Recruit at create time
helio-cli tests create ... --audience-type targeted \
--demographics '{"age":["25-34","35-44"],"country":["United States"]}'
helio-cli tests create ... --audience-type advanced --audiences <enroll-id>
helio-cli tests create ... --audience-type customer_list --audiences <list-id>
# Or retarget an existing draft (clone-then-retarget flow)
helio-cli tests clone <test-uuid>
helio-cli tests update <new-uuid> --audience-type targeted \
--demographics '{"education":["Bachelor degree"]}'
# Keep tests from sharing participants (monadic cells, iteration vs. baseline)
helio-cli tests create ... --audience-type basic \
--exclude-tests <cell-a-uuid> <cell-b-uuid>
helio-cli tests update <test-uuid> --exclude-tests <other-uuid>
helio-cli tests update <test-uuid> --clear-exclude-tests
# Limit which devices can take the test (default: all three)
helio-cli tests create ... --devices desktop
helio-cli tests update <test-uuid> --devices desktop tablet--demographics keys: gender, age, income, education, continent, country — values are string arrays. Required for targeted, optional for advanced, rejected elsewhere. --audiences is required for advanced/customer_list and rejected for the other types — including open, where the server would silently ignore it (the old trap: a test that looks fine and recruits nobody). On tests update, --audience-type replaces the pending quota wholesale; size carries over unless --target-audience-size is passed too. Audience changes are draft-only (running tests 422). Enroll rows in audiences list show — for participants/tests/last-used — a panel segment has no usage history in your account.
--devices takes any of desktop, tablet and mobile (space- or comma-separated); leave it off and the test runs on all three, which is what every API-built test did before the 2026-09-19 Public API release (zurb/helio#5048). The panel recruits only on the devices you name, and the take page turns away anyone on another device, share-link tests included. On tests update a list replaces the set and leaving the flag off leaves it alone; pass all three to allow every device again. Like every update field it is draft-only, so set it before tests send, which is when the panel reads it. tests preview reads it back (test.devices in JSON, null against an older API).
--exclude-tests <ids...> keeps anyone who took the named tests out of this one, which is what a set of monadic cells or an iteration measured against its own baseline needs. Values are test uuids on your own account (a report_uuid also resolves); an unknown or foreign id is a 404 that writes nothing.
Exclusion is symmetric — a test also excludes anyone who took a test naming it — so a mutually exclusive set of N tests needs one entry per pair, not N-1 entries per test. For four cells A/B/C/D, that means naming later cells from earlier ones rather than cross-listing all four.
The API applies the same three gates the editor does, so the CLI can't build a test the editor then can't show: panel audiences only (basic, targeted, advanced — never an open link or customer list), at most 5 per test (30 on internal_group accounts), and the account needs the exclusions beta flag. Only the panel rule is checkable locally, and only when the same command sets --audience-type; the cap and the account flag come back as warnings, since the CLI can't see your account and shouldn't block an account that does have the entitlement.
On tests update, --exclude-tests replaces the list wholesale and — unlike --audiences — does not need --audience-type: without one it applies to the audience the test already has. --clear-exclude-tests sends an empty list, which is exempt from all three gates, so a test can always be undone.
| Alias | Command |
|---|---|
t |
tests |
p |
projects |
cl |
custom-lists |
pt |
participants |
a |
audiences |
ic |
intercepts |
r |
responses |
helio-cli t list --status running
helio-cli p tests <project-uuid>
helio-cli cl participants <list-uuid>Add --output json to any command for machine-readable output:
helio-cli tests list --output json
helio-cli tests report <id> --output json | jq '.questions_summary'Errors also return structured JSON: { "error": "message", "code": 401 }
| Option | Description |
|---|---|
--output <format> |
json or text (default: text) |
--api-id <id> |
API ID (overrides config/env) |
--api-token <token> |
API token (overrides config/env) |
--base-url <url> |
Base URL (default: https://my.helio.app) |
# Run diagnostics (includes a CLI up-to-date check)
helio-cli doctor
# Update to the latest version
helio-cli update
helio-cli update --check # check only, don't install
# Check auth status
helio-cli auth status
# Use staging or local dev
helio-cli --base-url http://localhost:3000 status
HELIO_BASE_URL=https://staging.helio.app helio-cli statusRun helio-cli guide for a complete getting-started guide, or helio-cli guide --output json for a machine-readable version with full command schemas.
- Node.js >= 22
- @helio-app/sdk (installed automatically)
Private
{ "type": "preference", "instructions": "Which do you prefer?", "variations": [ { "name": "Data In", "asset_id": 61016 }, { "name": "The Vertical", "asset_id": 61017, "site_link": "https://example.com" } ]}