Reference
Permalink to ReferenceGenerated in part straight from source at build time (packages/mcp/src/tools/index.ts's ALL_TOOLS and packages/spec/src/events.ts's EVENT_TYPES) so the tool and event lists here can't silently drift from the code the way prose docs do. Node-type field shapes mirror the spec's schema source of truth — the full Zod-equivalent shapes and JSON Schema are public at weir-spec/SCHEMA.md and weir-spec/schema/flow.schema.json.
MCP tools & CLI verbs
Permalink to MCP tools & CLI verbsEvery tool is defined once and mounted on two identical surfaces from the same list: an MCP server (packages/mcp/src/server.ts, stdio) and a CLI (packages/mcp/bin/weir.ts, one subcommand per tool, generic --fieldName value flag parsing against the same Zod schema). The CLI verb is always the tool name with its weir_ prefix stripped and underscores turned to dashes.
| MCP tool | CLI verb | What it does |
|---|---|---|
weir_dev | weir dev | Browser preview of a built bundle (stock screen types only — a custom screen has no browser preview) with deterministic mocked native behavior: |
weir_walk | weir walk | Persona/branch screenshotter — first-class in the toolkit, replacing weir_screenshot. |
weir_conform | weir conform | Run the native-feel contract eval against a built browser-preview bundle directory (the internal index.html + manifest.json authoring harness, NOT the shipped deliverable — the published artifact is config.json): |
weir_embed | weir embed | Embed a flow's native v4 config.json and referenced assets at the baselineBundle declared in weir/app.json (requires a local app rebuild). |
weir_funnel | weir funnel | Step-through funnel with drop-off per screen for a flow, from the Weir API's ingest DB (GET /read/funnel/:flowId). |
weir_health | weir health | Reliability metrics for a flow (crash-free flow-session rate, p50/p95 flow_render_ms, bridge_error rate, flow_fallback count, queue flush failure rate), from the Weir API's ingest DB (GET /read/health/:flowId). |
weir_events | weir events | Live-ish tail of the most recent ingested events, for device-day debugging (GET /read/events). |
weir_app_doctor | weir app-doctor | Validate a host app's versioned weir/app.json contract: |
weir_audit | weir audit | Audit a host app's declared Weir onboarding contract without changing it: |
weir_ship_ready | weir ship-ready | Runtime readiness profile for a BUILT application, complementing weir_app_doctor's source-contract checks. |
weir_app_status | weir app-status | Summarize configured Weir flows for one app: |
weir_app_token | weir app-token | Mint a cryptographically random per-app Bearer token and print the exact entry an authorized services/api operator must add to WEIR_API_TOKENS. |
weir_app_scaffold | weir app-scaffold | Generate weir/app.json and a starter flow spec (+ its embedded native v4 config.json baseline) for a brand-new app id, so a new integration starts from a validated, doctor-passing skeleton instead of a hand-written config. |
weir_init | weir init | Scaffold a brand-new app's Weir integration in under a minute: |
weir_rollback | weir rollback | Revert flowId's remote config to a prior signed version via POST /rollback/:flowId — a server-side forward-rollback copy (publish history is append-only, nothing is deleted or overwritten; this creates a new, higher version with the target version's exact bytes). |
weir_release | weir release | Release one configured app flow safely: |
weir_experiment_scaffold | weir experiment-scaffold | Generate a valid pre-registration markdown doc skeleton from a flow's experiments[] entry: |
weir_experiment_start | weir experiment-start | Create an experiment against the Weir API from a pre-registration doc (run weir_experiment_scaffold first to generate a valid one from the flow's experiments[] entry). |
weir_experiment_status | weir experiment-status | Render an experiment's live state from the Weir API: |
weir_experiment_decide | weir experiment-decide | Apply the pre-registered decision rule to a running experiment, mechanically, from its pre-registration doc. |
weir_experiment_ship_winner | weir experiment-ship-winner | Ship the winning arm of a decided experiment in one guarded step: |
weir_asset_tokens | weir asset-tokens | List every valid moment.image/welcome.image token and every option.icon token, each with a human description of what it actually looks like (written by looking at the asset, not its name) — so you never pick one blind. |
weir_schema | weir schema | Print the flow-spec vocabulary — top-level flow shape, theme/colour tokens, and every screen type's required/optional fields with a minimal valid example — generated live from the real Zod schema in packages/spec, not a hand-written reference. |
weir_components_sync | weir components-sync | Sync the app's registered native components (weir/components.json) to the Weir API's component manifest (PUT /components/:appId) and write the generated WeirComponentRegistry.generated.{swift,kt,ts} files (one per platform with at least one declared component) embedding the assigned manifestVersion and the computed registryHash. |
Retired, not in this list: an earlier surface also had weir_validate, weir_lint, weir_render_preview, weir_screenshot, weir_list_flows, and weir_diff_baseline. They are unregistered — some of their internals live on as library code other tools call, but they are not callable tools or CLI verbs today. If you find a doc anywhere in this repo referencing those by name as if they're live commands, the code (tools/index.ts's ALL_TOOLS) is correct and the doc is stale — weir_dev + weir_walk cover the preview/screenshot ground those retired tools used to.
Tool detail
Permalink to Tool detailweir_dev
Permalink to weir_dev
CLI: weir dev
Browser preview of a built bundle (stock screen types only — a custom screen has no browser preview) with deterministic mocked native behavior: permission grant/deny personas, purchase success/cancel/fail personas, mock localized product catalog (incl. an intro-offer + a non-USD PPP-tier product), visible fake system dialogs, safe-area/notch overlay, keyboard-inset simulation. Provide exactly one of specPath/specJson, optional persona (e.g. "notifications=deny,purchase=cancel"), and serve/port to run a local static server instead of a file:// path.
weir_walk
Permalink to weir_walk
CLI: weir walk
Persona/branch screenshotter — first-class in the toolkit, replacing weir_screenshot. Drives a dev-mocked bundle headlessly, screenshotting every screen along the path each persona produces (fake permission/purchase dialogs included), and writes a walk-report.json (path, events, final variables per persona). Provide exactly one of specPath/specJson, plus optional personas (persona flag strings), answerMaps (drive typed answers into select/input screens to reach a specific branch — one JSON object per persona), variant (force every persona onto one experiment arm), and outDir.
weir_conform
Permalink to weir_conform
CLI: weir conform
Run the native-feel contract eval against a built browser-preview bundle directory (the internal index.html + manifest.json authoring harness, NOT the shipped deliverable — the published artifact is config.json): persona walk reaches complete(), auto-emitted event stream matches the manifest, every declared variable gets set, purchase/permission calls only go through the mocked native-call path, no literal currency strings, no network on the paint path, and perf budgets (TTI, gz bundle size). Expected runtime: a headless persona walk through a ~10-screen flow takes roughly 3-5 minutes (each screen renders in a real browser + settles animations); this is normal, not a hang. Progress is streamed to stderr as screen N/total: <screenId> ok while the walk runs, so a caller polling output can confirm it's alive rather than assuming it stalled.
weir_embed
Permalink to weir_embed
CLI: weir embed
Embed a flow's native v4 config.json and referenced assets at the baselineBundle declared in weir/app.json (requires a local app rebuild). Prefer flowId mode: it resolves both source and destination from the app contract. The explicit appResourcesDir mode remains available only for legacy HTML preview bundles. Remote delivery without a rebuild is available via weir_release; see https://weir-docs.web.app for the delivery model.
weir_funnel
Permalink to weir_funnel
CLI: weir funnel
Step-through funnel with drop-off per screen for a flow, from the Weir API's ingest DB (GET /read/funnel/:flowId). Also reports the launch-plan §7 metrics: completion rate, time-to-paywall median, verdict_ready reach rate, trial-start rate. Pass --by variant to split per variant.
weir_health
Permalink to weir_health
CLI: weir health
Reliability metrics for a flow (crash-free flow-session rate, p50/p95 flow_render_ms, bridge_error rate, flow_fallback count, queue flush failure rate), from the Weir API's ingest DB (GET /read/health/:flowId).
weir_events
Permalink to weir_events
CLI: weir events
Live-ish tail of the most recent ingested events, for device-day debugging (GET /read/events). --tail N controls how many rows (default 50); --flow filters to one flowId.
weir_app_doctor
Permalink to weir_app_doctor
CLI: weir app-doctor
Validate a host app's versioned weir/app.json contract: each flow spec, embedded native v4 config baseline, optional integration file, delivery-mode declaration, and telemetry/fallback configuration. It never reads secrets or changes the app.
weir_audit
Permalink to weir_audit
CLI: weir audit
Audit a host app's declared Weir onboarding contract without changing it: inventory flows, stock and app-owned custom components, embedded baselines, remote-update and telemetry declarations, then include the real weir_app_doctor result. It explicitly does not invent active-install coverage, device proof, or business-outcome estimates.
weir_ship_ready
Permalink to weir_ship_ready
CLI: weir ship-ready
Runtime readiness profile for a BUILT application, complementing weir_app_doctor's source-contract checks. Opens the actual artifact and verifies the SDK reached the binary, each declared baseline bundle is present INSIDE the artifact, and those bundle bytes are identical to the source tree that was conformed. For standard React Native/Expo artifacts, it also reads WeirIngestWriteToken/weir.ingestWriteToken directly; other hosts report ingest and kill-switch configuration from caller assertions — undetermined is a WARN, never a silent pass. Motivating incidents: a stale XcodeGen rewrote an iOS resource folder reference to a repo-root path, so the app built green with NO baseline bundle while app-doctor stayed happy; and a prettier pre-commit hook rewrote a generated manifest.json, making a byte-exact freshness check permanently unsatisfiable. Neither is visible from source alone.
weir_app_status
Permalink to weir_app_status
CLI: weir app-status
Summarize configured Weir flows for one app: currently published manifest bundle plus existing funnel and health reports. Reads WEIR_API_URL and optional WEIR_API_TOKEN from the environment; secrets never live in weir/app.json.
weir_app_token
Permalink to weir_app_token
CLI: weir app-token
Mint a cryptographically random per-app Bearer token and print the exact entry an authorized services/api operator must add to WEIR_API_TOKENS. It only generates a secret locally: it does not contact, register, or modify any server.
weir_app_scaffold
Permalink to weir_app_scaffold
CLI: weir app-scaffold
Generate weir/app.json and a starter flow spec (+ its embedded native v4 config.json baseline) for a brand-new app id, so a new integration starts from a validated, doctor-passing skeleton instead of a hand-written config. Does not generate native Swift wiring — run weir_app_doctor after this and wire Weir.configure/present yourself.
weir_init
Permalink to weir_init
CLI: weir init
Scaffold a brand-new app's Weir integration in under a minute: weir/app.json, a starter specVersion-4 flow (welcome + paywall, using only stock screen types) that already passes validate() and the weir_conform gate, and its embedded native v4 config.json baseline. Zero network, zero account required. Prints the agent-legible next steps: wire the native SDK, weir_app_doctor, optional browser QA, then a dry-run release gate.
weir_rollback
Permalink to weir_rollback
CLI: weir rollback
Revert flowId's remote config to a prior signed version via POST /rollback/:flowId — a server-side forward-rollback copy (publish history is append-only, nothing is deleted or overwritten; this creates a new, higher version with the target version's exact bytes). Requires a running Weir API (default http://localhost:8787 for local dev, override WEIR_API_URL to point at your Weir account's API) and WEIR_API_TOKEN if it requires one. Run weir_app_status or GET /read/releases/:flowId first to see which versions are available.
weir_release
Permalink to weir_release
CLI: weir release
Release one configured app flow safely: resolve it from weir/app.json, build once, run the full conform gate, and publish those exact bytes only if conform passes. With dryRun=true it performs every local check but never calls the API; a live release fetches the returned manifest and verifies its bundle/version. With draft=true (RFC-007), a build that FAILS conform is published to a draft slot instead — visible only to the dashboard's draft-preview view, never to /manifest or any real user; a build that passes conform is refused (use plain release for that).
weir_experiment_scaffold
Permalink to weir_experiment_scaffold
CLI: weir experiment-scaffold
Generate a valid pre-registration markdown doc skeleton from a flow's experiments[] entry: every mechanical field weir_experiment_start requires (exact arms JSON, minSamplePerArm/minDurationMs, and the decision rule with the literal ≥/≤/× thresholds the parser needs) is already correct — only the hypothesis and primary-metric prose are left as TODOs for you to fill in. Run weir_experiment_start against the result as-is (with the TODOs replaced) rather than hand-writing the format from scratch.
weir_experiment_start
Permalink to weir_experiment_start
CLI: weir experiment-start
Create an experiment against the Weir API from a pre-registration doc (run weir_experiment_scaffold first to generate a valid one from the flow's experiments[] entry). Validates the doc has every mechanical field (hypothesis, arms, primary metric, fixed horizon, decision rule), validates the configured flow's spec experiments[] entry matches the doc's arms exactly, and refuses if an experiment with this id already exists (pre-registration is immutable — never recreated).
weir_experiment_status
Permalink to weir_experiment_status
CLI: weir experiment-status
Render an experiment's live state from the Weir API: arms with assigned/converted/conversionRate, SRM flagged/not, assignment-drift count, and day-N-of-horizon. Results are explicitly labeled INTERIM until the fixed horizon is reached — this tool never applies the decision rule (see weir_experiment_decide for that).
weir_experiment_decide
Permalink to weir_experiment_decide
CLI: weir experiment-decide
Apply the pre-registered decision rule to a running experiment, mechanically, from its pre-registration doc. REFUSES before the fixed horizon (reports the exact unlock date, not a result). At/after horizon, applies the doc's ordered invalid/ship-treatment/ship-control/no-call rule and reports the verdict plus the exact follow-up (which arm to release via weir_release) — it does not itself publish; the release step stays a separate, explicit action.
weir_experiment_ship_winner
Permalink to weir_experiment_ship_winner
CLI: weir experiment-ship-winner
Ship the winning arm of a decided experiment in one guarded step: go through weir_experiment_decide's exact horizon/pre-reg guard, transform the flow spec (drop the losing arm's gated screens, un-gate the winner's, remove the experiments[] entry, re-link transitions), run the FULL weir_release conform+publish gate against the transformed spec, and finally POST /experiments/:id/stop. REFUSES before the fixed horizon, on a no-call/invalid verdict, if the transform cannot be resolved statically, or if the transformed spec fails conform — never publishing a best-effort broken flow. dryRun (the default) prints the would-be transform diff and conforms it but makes NO network write; re-run with --dryRun=false to publish and stop for real.
weir_asset_tokens
Permalink to weir_asset_tokens
CLI: weir asset-tokens
List every valid moment.image/welcome.image token and every option.icon token, each with a human description of what it actually looks like (written by looking at the asset, not its name) — so you never pick one blind. Text only; see https://weir-docs.web.app/docs/assets/ for the real rendered images/icons. If none of the 5 built-in image tokens fit, declare your own via the flow spec's customAssets[] and reference it as "custom:<id>" — see https://weir-docs.web.app/docs/reference/#custom-images. option.icon stays a closed set (no custom-icon upload yet).
weir_schema
Permalink to weir_schema
CLI: weir schema
Print the flow-spec vocabulary — top-level flow shape, theme/colour tokens, and every screen type's required/optional fields with a minimal valid example — generated live from the real Zod schema in packages/spec, not a hand-written reference. Pass screenType to see just one thing: a screen type (e.g. "singleSelect"), "flow", or "theme".
weir_components_sync
Permalink to weir_components_sync
CLI: weir components-sync
Sync the app's registered native components (weir/components.json) to the Weir API's component manifest (PUT /components/:appId) and write the generated WeirComponentRegistry.generated.{swift,kt,ts} files (one per platform with at least one declared component) embedding the assigned manifestVersion and the computed registryHash. Requires a running Weir API (default http://localhost:8787, override WEIR_API_URL) and, if it requires one, WEIR_API_TOKEN.
Node types
Permalink to Node types13 stock screen types, closed — "no new stock types" is still the rule; an app that needs something the 13 don't cover registers a real native custom component instead of Weir growing the schema. Full field-level shapes are public at weir-spec/SCHEMA.md.
| Node type | Purpose |
|---|---|
welcome | First screen: pitch + optional star-rating social proof strip and a canned chat demo. |
singleSelect | Pick exactly one option from a list of icon tiles or a radio group. |
multiSelect | Pick 0+ options into a string-array variable, with min/max selection bounds. |
slider | Drag a numeric value between min and max into a number variable. |
numberInput | Type a numeric value directly into a number variable. |
textInput | Free-text input, with optional suggestion chips, email validation, and a skip affordance. |
loader | A timed "personalizing your plan" beat that computes one or more derived variables. |
socialProof | A dedicated testimonial/rating screen (distinct from welcome's inline strip). |
permissionPrime | Prime and then trigger a real system permission prompt (notifications, ATT, HealthKit, camera, location). |
paywall | A purchase screen inside the onboarding flow: feature list, product cards, and a CTA that calls purchase.start. Standalone paywall placements can remain in a dedicated paywall product. |
moment | A personalized full-bleed affirmation/reveal beat with real bundled photography, no input. |
holdToCommit | Press-and-hold to "seal" an intention into a string variable — emits a dedicated commit event. |
demo | A non-interactive, canned sourced-answer chat demo (citation chips under app replies). |
custom | A native component the app registered with the Weir SDK (weir components sync) — the escape hatch for anything the 13 stock types don't cover. props is opaque JSON at the spec-schema layer; its real shape is validated against the component manifest at publish time. |
Node detail
Permalink to Node detailwelcome
Permalink to welcome
First screen: pitch + optional star-rating social proof strip and a canned chat demo.
Key fields: title, subtitle?, image?, socialProof?, demo?, cta.
singleSelect
Permalink to singleSelect
Pick exactly one option from a list of icon tiles or a radio group.
Key fields: title, variable, options[>=2], required?, autoAdvance?.
multiSelect
Permalink to multiSelect
Pick 0+ options into a string-array variable, with min/max selection bounds.
Key fields: title, variable (stringArray), options[>=2], minSelections?, maxSelections?.
slider
Permalink to slider
Drag a numeric value between min and max into a number variable.
Key fields: title, variable (number), min, max, step?, default?, unit?.
numberInput
Permalink to numberInput
Type a numeric value directly into a number variable.
Key fields: title, variable (number), min?, max?, placeholder?, unit?.
textInput
Permalink to textInput
Free-text input, with optional suggestion chips, email validation, and a skip affordance.
Key fields: title, variable (string), placeholder?, suggestions?, validation? (email), skipCta?, benefits?.
loader
Permalink to loader
A timed "personalizing your plan" beat that computes one or more derived variables.
Key fields: title?, durationMs?, messages[>=1], computes? (variable ids).
socialProof
Permalink to socialProof
A dedicated testimonial/rating screen (distinct from welcome's inline strip).
Key fields: title?, rating?, ratingCount?, testimonials[>=1], cta.
permissionPrime
Permalink to permissionPrime
Prime and then trigger a real system permission prompt (notifications, ATT, HealthKit, camera, location).
Key fields: title, permission, primeCta, skipCta?, benefits?, resultVariable?.
paywall
Permalink to paywall
A purchase screen inside the onboarding flow: feature list, product cards, and a CTA that calls purchase.start. Standalone paywall placements can remain in a dedicated paywall product.
Key fields: headline, features?, products[>=1], cta, restoreLabel?, termsUrl?, privacyUrl?.
moment
Permalink to moment
A personalized full-bleed affirmation/reveal beat with real bundled photography, no input.
Key fields: headline, body?, image? (asset token), citation?, kicker?, cta.
holdToCommit
Permalink to holdToCommit
Press-and-hold to "seal" an intention into a string variable — emits a dedicated commit event.
Key fields: title, variable (string), suggestions?, allowCustom?, holdMs? (300-5000), sealedTitle?, cta.
demo
Permalink to demo
A non-interactive, canned sourced-answer chat demo (citation chips under app replies).
Key fields: title?, subtitle?, messages[1..6], cta.
custom
Permalink to custom
A native component the app registered with the Weir SDK (weir components sync) — the escape hatch for anything the 13 stock types don't cover. props is opaque JSON at the spec-schema layer; its real shape is validated against the component manifest at publish time.
Key fields: component ("<namespace>.<name>"), props?, gating?.
Cross-cutting, on every screen: theme.motion presets, a closed IconToken SVG set for option/ feature icons, cta.variant (primary | accent, the gold treatment for high-intent screens), onAccent text color, and a *emphasis* two-tone-headline convention (wrap a run in asterisks inside any title/headline to render it in the brand's gold serif-italic accent). moment.image/ welcome.image are also a closed token set — see Asset tokens to look at every image/icon token before picking one, rather than guessing from its id.
Custom components and gating
Permalink to Custom components and gatingA custom screen (see Node types above) names a registered native component and its props:
{ "id": "bodyStats", "type": "custom", "component": "yourapp.bodyStatsQ", "props": { "min": 120 } }component must match ^[a-zA-Z][a-zA-Z0-9_]*\.[a-zA-Z][a-zA-Z0-9_]*$ — always "<namespace>.<name>" with exactly one dot. The namespace is a single identifier (letters, digits, underscore), so a dotted bundle id (com.greeter.demo) cannot be the namespace — it violates the one-dot rule. Derive a namespace from your appId instead (for example greeter from com.greeter.demo, then greeter.hero). props is opaque JSON here; register the component and its real prop schema first with weir components-sync (see Concepts → Custom components and the component manifest) — a config naming an unregistered component, or sending props that don't match its schema, is refused at publish time. Before build QA, run weir components-sync --check: a read-only preflight that fails when weir/components.json, the last synced manifest cache, or a generated registry source file has drifted, without contacting the API or writing anything.
Every screen (stock or custom), and the flow as a whole, can also declare gating:
{
"gating": {
"minAppVersion": "1.4.0", // optional — semver
"platforms": ["ios"], // optional — omit for "every platform"
"fallback": "serveOlderConfig", // "skip" | "substitute" | "serveOlderConfig" (default)
"substituteScreen": "fallbackHero" // required iff fallback is "substitute"; that screen must itself carry no gating
}
}A flow-level minAppVersion/platforms (same two fields, at the top of the spec) is a cheap whole-flow pre-check before any per-screen gate runs. See Concepts → Gating for what a build that fails a gate actually gets served.
Experiments and the variant gate
Permalink to Experiments and the variant gateA flow's top-level experiments array declares two-or-more-arm experiments the flow's screens can gate on:
{
"id": "onboarding_copy_v2", // identifier: ^[a-zA-Z][a-zA-Z0-9_]*$ — no hyphens
"variants": [
{ "id": "control", "weight": 1 }, // weight optional, defaults to 1
{ "id": "treatment", "weight": 1 }
],
"holdout": 0, // 0-1, fraction excluded from every arm; optional, defaults to 0
"targeting": { // optional — narrows who is eligible at all
"platforms": ["ios"],
"locales": ["en"], // BCP-47 prefixes
"minAppVersion": "1.4.0",
"newVsReturning": "new" // "new" | "returning" | "both" (default)
}
}A user who fails targeting is excluded the same way a holdout miss is: no arm assignment, no variant_assigned event.
variants needs at least 2 entries. Any screen can then declare an optional variant gate, naming the experiment and which of its variants show that screen:
{
"id": "pricing_treatment",
"type": "paywall",
"variant": { "experiment": "onboarding_copy_v2", "showFor": ["treatment"] },
"headline": "…"
}A screen with no variant field is always included, unchanged. A screen with variant set is included in the walk only for users assigned to one of showFor's variants — the same published bundle carries every arm; nothing is rebuilt or republished per arm.
Worked example — a two-arm flow, one shared welcome screen, then a control/treatment split on the paywall headline:
{
"specVersion": 4,
"id": "onboarding",
"name": "Onboarding",
"experiments": [
{ "id": "paywall_copy_v1", "variants": [{ "id": "control" }, { "id": "treatment" }], "holdout": 0 }
],
"screens": [
{ "id": "welcome", "type": "welcome", "title": "Welcome", "cta": { "label": "Continue" }, "next": "pw_control" },
{
"id": "pw_control",
"type": "paywall",
"variant": { "experiment": "paywall_copy_v1", "showFor": ["control"] },
"headline": "Unlock everything",
"products": [{ "id": "pro_monthly", "label": "Monthly" }],
"cta": { "label": "Continue" }
},
{
"id": "pw_treatment",
"type": "paywall",
"variant": { "experiment": "paywall_copy_v1", "showFor": ["treatment"] },
"headline": "Start your 7-day free trial",
"products": [{ "id": "pro_monthly", "label": "Monthly" }],
"cta": { "label": "Continue" }
}
]
}Note the experiment id (paywall_copy_v1) uses underscores, not hyphens — it goes through the same Identifier rule as every other id in the spec (^[a-zA-Z][a-zA-Z0-9_]*$). Pre-registration doc filenames are conventionally hyphenated (e.g. exp-001-paywall-copy.md) — that is a filename convention only, unrelated to the id field inside the flow spec or the pre-registration doc's ## Exact arms JSON block, which both must satisfy Identifier.
What "skipped" actually means, stated exactly: a gated screen a user's assigned arm doesn't match is not shown, and the walk falls through to the next screen in array order — the same implicit fallthrough an ungated screen with no next field uses. It is not "the next screen that happens to declare a next pointing here" and it is not "the flow ends." Worked example — two arms sharing everything except one screen in the middle:
{
"experiments": [
{ "id": "extra_tip_v1", "variants": [{ "id": "control" }, { "id": "treatment" }] }
],
"screens": [
{ "id": "weightInput", "type": "numberInput", "title": "What do you weigh today?", "variable": "bodyweight" },
{
"id": "extraTip",
"type": "moment",
"variant": { "experiment": "extra_tip_v1", "showFor": ["treatment"] },
"headline": "One more thing before we build your plan",
"cta": { "label": "Continue" }
},
{ "id": "personalizing", "type": "loader", "title": "Building your plan" }
]
}weightInput and personalizing declare no next at all — array order is their transition. A treatment-arm user sees all three screens in order. A control-arm user reaches weightInput, finds extraTip gated out for their arm, and falls straight through to personalizing — the same next-in-array-order rule, just skipping over the one screen that didn't apply to them. Nothing about weightInput or personalizing needs to know the experiment exists.
Run weir_experiment_scaffold against a flow with an experiments[] entry to generate a pre-registration doc skeleton with every mechanical field already correct — see Experiments → Pre-registration.
Both moment.image/welcome.image and option.icon are validated closed enums — an unrecognized token in either fails validate()/weir_conform at authoring time, not a silent runtime fallback. See Asset tokens to see every valid built-in token rendered before picking one — or read on to supply your own.
Custom images
Permalink to Custom imagesThe five built-in moment.image/welcome.image tokens (see Asset tokens) are all one app's (Niyat's) art direction. If none fit your flow, declare your own image once at the top of the spec and reference it as "custom:<id>":
{
"customAssets": [
{ "id": "heroPlant", "path": "assets/hero-plant.jpg" }
]
}id: an identifier — reference it from a screen as"custom:<id>".path: a file on disk, resolved relative to the flow spec file's own directory (keep your images next to the spec, or in a subfolder beside it). Supported formats: JPEG, PNG, WebP.
Resolution: supply the image at 3x the viewport, not 1x. A moment/welcome image renders full-bleed at the device's own CSS viewport size — typically 390×844 for an iPhone screen — but a modern device pixel ratio needs the source image at 3x that box or it looks soft. For a 390×844 viewport that means a source image of at least 1170×2532. The Previews page's asset-density check enforces this: any image whose intrinsic size is under 3x its rendered CSS box is flagged (effectiveScale < 3). Author at 3x up front — don't wait to trip the check.
Use it from any moment/welcome screen exactly like a built-in token:
{
"id": "welcomePlant",
"type": "moment",
"headline": "Grow something *real*.",
"image": "custom:heroPlant",
"cta": { "label": "Continue" }
}weir_embed/weir_release/weir_dev/weir_walk read the file and embed it as a data URI at build time — the same zero-network bundle shape as a built-in token, rendered through the exact same path (same scrim, same full-bleed sizing). weir_conform checks it resolved and embedded correctly, and the bundle's total size still counts against the 350KB budget honestly — an oversized custom image fails budget-bundle-size exactly like any other cause of an oversized bundle. Identical images declared under different ids are embedded once and shared, not duplicated.
A customAssets[] entry no screen references is legal but pointless (a warning, unused_custom_asset, same as an unused variable); a screen referencing an undeclared id is a hard error (unknown_custom_asset).
Worked example — a plant-care app's own hero photo:
{
"specVersion": 4,
"id": "onboarding",
"name": "Sprout onboarding",
"customAssets": [
{ "id": "heroPlant", "path": "assets/hero-plant.jpg" }
],
"screens": [
{ "id": "welcome", "type": "welcome", "title": "Welcome to Sprout", "cta": { "label": "Get started" }, "next": "moment" },
{
"id": "moment",
"type": "moment",
"headline": "Grow something *real*.",
"image": "custom:heroPlant",
"cta": { "label": "Continue" }
}
]
}Event vocabulary
Permalink to Event vocabularyThe instrumentation contract: the exact, fixed set of events the renderer emits. This is the product's core guarantee — agents get full creative freedom on presentation, but the event stream underneath is asserted byte-exact by the weir_conform eval. Every event carries a monotonic seq starting at 0; weir_conform fails if a built bundle's actual emitted stream diverges from what the flow spec implies.
| Event type | Fires when |
|---|---|
flow_started | A flow began rendering, at the declared entry screen. |
screen_impression | A screen was shown (its type and position in the flow). |
quiz_answer | A singleSelect/multiSelect/slider/numberInput screen's answer was recorded. |
input_submitted | A textInput screen's value was submitted. |
branch_decision | A next.branches condition was evaluated and a target chosen. |
variant_assigned | An experiment variant was assigned (Pillar 3, frozen). |
loader_completed | A loader screen finished and reports which variables it computed. |
permission_prompt_shown | A permissionPrime screen's real system prompt was triggered. |
permission_result | The system permission prompt resolved granted/denied. |
paywall_shown | A paywall screen was shown, with the product ids offered. |
purchase_intent | The user tapped a paywall CTA for a specific product, before the purchase call resolves. |
purchase_result | purchase.start/purchase.restore resolved: purchased, cancelled, failed, or restored. |
commit | A holdToCommit screen's press-and-hold completed — a higher-intent signal than input_submitted. |
screen_skipped | A skippable screen (textInput.skipCta, etc.) was dismissed via its skip affordance. |
screen_exit | |
screen_fallback | |
flow_completed | The flow reached complete() — reason is reached_end, purchased, or dismissed. |
Every event envelope is { flowId, screenId?, variantId?, sessionId, userId?, ts, seq, payload, screenDwellMs? } (see Concepts → Instrumentation). It gets appended to a file-backed offline queue in seq order on the native side and flushes to the Weir API's ingest as one batch, carrying device context (platform, app version, SDK version, locale, device class) once per batch rather than once per event. weir_funnel/weir_health/ weir_events all read from that same ingested stream.
Compliance linting
Permalink to Compliance lintingThere is currently no automated compliance/App-Review lint in the toolkit — an earlier weir_compliance_lint tool was removed along with its COMPLIANCE.md reference doc. If you're about to ship a flow with a paywall or permissionPrime screen, that judgment call (clear pricing, no dark patterns, ATT copy matching what the system prompt will say) is manual today. Don't rely on a docs page or a stale skill reference implying otherwise.