Author your first flow
Permalink to Author your first flowA Weir screen does not scroll. Every screen renders in one fixed viewport, the same way a native onboarding screen does — there is no scroll fallback for content that doesn't fit. Content that overflows is clipped, not revealed by scrolling. Size every screen's content (title length, option count, feature list length) to fit the viewport at authoring time; don't rely on a user being able to scroll to see the rest. weir_conform's content-overflow check catches a screen that clips, but the fix is always to shorten or restructure the content, never to add scrolling.
A flow spec is one JSON file: a theme, a list of typed variables, and a list of screens. This page is a complete, working example you can copy, then the field-by-field reasoning behind it. The full field list for every node type is public at weir-spec/SCHEMA.md.
After any flow edit, re-embed its native baseline before expecting weir app-doctor to pass; see the edit → verify loop.
The example below passes @x/spec's validate() with zero errors and zero warnings. It is a five-question fitness intake: it collects a goal and a bodyweight, branches on the goal, computes a plan label in a loader, personalizes a reveal, and ends on a paywall.
The complete flow
Permalink to The complete flowSave this as weir/flows/onboarding.json.
{
"specVersion": 4,
"id": "onboarding",
"name": "Fit onboarding",
"meta": { "app": "example-app", "description": "Worked example for the docs site." },
"theme": {
"colors": { "background": "#0f1020", "primary": "#5b6cff", "accent": "#f5c451" },
"motion": { "headline": "fade-up" }
},
"variables": [
{ "id": "goal", "type": "enum", "enumValues": ["cut", "bulk", "maintain"], "description": "The user's primary training goal." },
{ "id": "bodyweight", "type": "number", "description": "Current bodyweight in kilograms." },
{ "id": "planLabel", "type": "string", "default": "a plan built around your goal", "description": "Computed by the loader; a short human label for the generated plan." }
],
"entry": "welcome",
"screens": [
{
"id": "welcome",
"type": "welcome",
"title": "Build the *right* body",
"subtitle": "Answer three questions and get a plan tuned to your goal.",
"socialProof": { "rating": 4.8, "ratingCount": 1240, "tagline": "Trusted by lifters worldwide" },
"cta": { "label": "Get started" },
"next": "goalSelect"
},
{
"id": "goalSelect",
"type": "singleSelect",
"title": "What is your goal right now?",
"variable": "goal",
"options": [
{ "id": "cut", "label": "Cut", "value": "cut", "icon": "leaf", "description": "Lose fat, keep muscle." },
{ "id": "bulk", "label": "Bulk", "value": "bulk", "icon": "star", "description": "Add size and strength." },
{ "id": "maintain", "label": "Maintain", "value": "maintain", "icon": "shield", "description": "Hold steady and recomposition." }
],
"next": {
"branches": [
{ "when": { "var": "goal", "op": "eq", "value": "maintain" }, "goto": "maintainNote" }
],
"default": "weightInput"
}
},
{
"id": "maintainNote",
"type": "moment",
"kicker": "Good choice",
"headline": "Maintenance is a *skill*",
"body": "Holding a hard-won physique is its own discipline. We will keep you dialed in.",
"cta": { "label": "Continue" },
"next": "weightInput"
},
{
"id": "weightInput",
"type": "numberInput",
"title": "What do you weigh today?",
"variable": "bodyweight",
"min": 30,
"max": 300,
"unit": "kg",
"style": "stepper",
"step": 1,
"cta": { "label": "Next" },
"next": "personalizing"
},
{
"id": "personalizing",
"type": "loader",
"title": "Building your plan",
"durationMs": 2500,
"messages": ["Reading your goal", "Balancing calories and volume", "Finalizing {{planLabel}}"],
"computes": ["planLabel"],
"next": "planReady"
},
{
"id": "planReady",
"type": "moment",
"kicker": "Ready",
"headline": "Your *{{goal}}* plan is ready",
"body": "We built {{planLabel}} for a body weight of {{bodyweight}} kg.",
"cta": { "label": "See my plan", "variant": "accent" },
"next": "paywall"
},
{
"id": "paywall",
"type": "paywall",
"headline": "Start your *{{goal}}* plan",
"features": [
{ "icon": "check", "title": "Adaptive weekly targets" },
{ "icon": "clock", "title": "5-minute check-ins" }
],
"products": [
{ "id": "app.example.pro.yearly", "label": "Yearly", "priceHint": "$39.99/yr", "badge": "BEST VALUE", "highlighted": true },
{ "id": "app.example.pro.monthly", "label": "Monthly", "priceHint": "$5.99/mo" }
],
"cta": { "label": "Start now", "variant": "accent" },
"footnote": "Cancel anytime."
}
]
}Field by field
Permalink to Field by fieldTop level. specVersion must be exactly 4 (the current spec version — RFC-010). id and name are required. entry names the first screen; if you omit it, the first screen in the array is the entry. meta is optional labeling.
Variables are typed and declared once. This flow uses three:
| Variable | Type | Written by | Read by |
|---|---|---|---|
goal | enum (cut/bulk/maintain) | the goalSelect screen | the branch on goalSelect, and {{goal}} in later copy |
bodyweight | number | the weightInput screen | {{bodyweight}} in planReady |
planLabel | string | computed by the personalizing loader | {{planLabel}} in the loader and planReady |
An enum variable must declare enumValues. A singleSelect that writes an enum variable must use option values inside that set — here each option's value (cut/bulk/maintain) is one of goal's enumValues.
Branching. goalSelect.next is a conditional transition, not a static screen id. Its branches are checked in order; the first when that matches wins, and default is taken if none match. Here a goal of maintain goes to the maintainNote moment; every other goal falls through to weightInput. Both paths rejoin at weightInput, so no screen is stranded.
A condition is { "var": <id>, "op": <operator>, "value": <value> }. The operator must fit the variable type: gt/gte/lt/lte need a number variable, includes needs a stringArray, and eq/neq/in/nin work on scalars. eq on the goal enum is valid.
Interpolation. Any {{variableId}} in a title, subtitle, headline, body, sealedTitle, or a loader/demo message is replaced at render time. planReady interpolates all three variables. One trap: a variable that only a loader computes has no value until runtime, so it renders blank in preview — this example gives planLabel a default, so it shows real text in preview too. Interpolating a computed variable with no default earns a validation warning for exactly this reason.
The *emphasis* convention. Wrapping a run of a title or headline in asterisks (Build the *right* body) renders it in the brand's accent treatment. It is presentation only and does not affect the event stream.
Verify it
Permalink to Verify itTwo commands, in order. First check the spec is structurally and semantically valid — this is fast and needs no browser:
weir walk --specPath weir/flows/onboarding.json --outDir .eval-out/walkweir_walk builds the spec and drives it headlessly. A spec with an unknown screen target, a branch on an undeclared variable, or a type mismatch fails here with an agent-legible path: message [code] error before you ever build a bundle. See Sandbox preview & branch verification for reading its screenshots and walk-report.json.
Then run the full contract gate against a built bundle:
weir conform --bundleDir <built-bundle-dir>weir_conform walks the built bundle with personas and asserts the whole instrumentation contract: the walk reaches complete(), every declared variable gets set, the emitted event stream matches the manifest, and the native-feel checks pass. It streams screen N/total: <screenId> ok to stderr as it walks (expected runtime is a few seconds for a ten-screen flow — it is not hung), and exits 0 when every check is green. Inside an onboarded app the usual path is weir embed --appRoot . --flowId onboarding (which builds the bundle and prints the exact --bundleDir to pass) — see Quickstart. weir_release runs this exact gate for you internally before they publish, so this is for the embedded (offline) artifact specifically, not a required step before every remote publish.
Common mistakes
Permalink to Common mistakes- A branch
goto(ornext) naming a screen that does not exist — reported asunknown_target. Everygoto,default, and staticnextmust be a real screenid. - A branch condition on an undeclared variable, or a wrong-typed operator —
gton a string variable,includeson a non-stringArray. Declare the variable and match the operator to its type. - An unreachable screen — a screen no transition leads to fails validation. Make sure every screen is a
next,goto, ordefaulttarget of another, or falls through in array order. - A
singleSelecton anenumvariable with an option value outsideenumValues— reported asenum_value_out_of_range. - Interpolating
{{name}}for a variable you never declared — reported asinterpolation_unknown_variable.
The exhaustive field shapes and the JSON Schema are public at weir-spec/SCHEMA.md and the weir-spec repo. The 13 stock screen types (plus custom) are summarized on Reference → Node types.