Weir docsv4
View as Markdown

Author your first flow

Permalink to Author your first flow

A 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 flow

Save 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 field

Top 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:

VariableTypeWritten byRead by
goalenum (cut/bulk/maintain)the goalSelect screenthe branch on goalSelect, and {{goal}} in later copy
bodyweightnumberthe weightInput screen{{bodyweight}} in planReady
planLabelstringcomputed 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 it

Two 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/walk

weir_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 (or next) naming a screen that does not exist — reported as unknown_target. Every goto, default, and static next must be a real screen id.
  • A branch condition on an undeclared variable, or a wrong-typed operatorgt on a string variable, includes on 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, or default target of another, or falls through in array order.
  • A singleSelect on an enum variable with an option value outside enumValues — reported as enum_value_out_of_range.
  • Interpolating {{name}} for a variable you never declared — reported as interpolation_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.

Generated from this repo's source at build time — packages/mcp/src/tools/index.ts and packages/spec/src/events.ts are the ground truth for the tool and event tables above. llms-full.txt