# 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`](https://github.com/cynisca/weir-spec/blob/main/SCHEMA.md).

After any flow edit, re-embed its native baseline before expecting `weir app-doctor` to pass;
see [the edit → verify loop](/docs/troubleshooting/#weir_app_doctor-reports-baseline-config-or-baseline-freshness).

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

Save this as `weir/flows/onboarding.json`.

```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

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

| 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

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](/docs/sandbox-preview/) 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](/docs/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

- **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 operator** — `gt` 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`](https://github.com/cynisca/weir-spec/blob/main/SCHEMA.md) and the
[`weir-spec`](https://github.com/cynisca/weir-spec) repo. The 13 stock screen types (plus
`custom`) are summarized on [Reference → Node types](/docs/reference/#node-types).
