# Run settings

Flags change how one run delivers steps and how long it waits. A flow that always needs a setting declares it in its own config block, so nobody has to remember the flag.

## Flags

| Flag | What it does |
| --- | --- |
| `--direct-scroll` | Makes mode: direct the default for scrollUntilVisible. Much faster; skips momentum, paging and snapping. |
| `--auto-scroll` | Scrolls an off-screen control into view instead of failing the step. |
| `--no-fast-settle` | Waits the full fixed time before each step, instead of moving on when the app stops moving. |
| `--no-in-process` | Sends taps, text and swipes as real touches from outside the app. Slower; use it when the touch handling itself is what you test. |
| `--no-animations` | Turns iOS animations off in the app, for runs that must be identical. |
| `--capture-net` | Lets the flow wait for the network and stub requests. |
| `--strict-assert` | Fails the run when a step couldn’t be performed at all. |
| `--chaos <profile>` | Runs the flow on a bad network. See below. |
| `--stubs <run>` | Answers the network from an earlier run. See below. |
| `--matrix` | Runs the flow once in each simulator setup and lists the layout problems each one adds. See below. |
| `--ax-only` | Finds every control through the accessibility tree only, the way VoiceOver reaches it. See below. |
| `--settle-debug` | Prints every wait under the step it belongs to. When a wait runs long, it names what was still animating: the component and its file:line. |
| `--headless` | Runs without a simulator, inside the app’s JavaScript. Taps by position and gestures are reported as unsupported. |
| `--ignore-flow-config` | Ignores the flow’s own config block. |

In Studio, the same settings are in the Flows screen’s **Replay options**: Direct scroll, Auto-scroll, Fast settle, In-process delivery, Disable animations and Capture network.

## Settings a flow declares

Some settings decide whether a flow tests the right thing. A flow that tests infinite scroll needs real drags; one that checks a screen’s own animation timing needs the fixed waits. Declare them in the flow:

```yaml
flow: product list paging
config:
  scrollMode: gesture       # gesture or direct
  fastSettle: false         # use the fixed waits
  autoScroll: true          # scroll an off-screen control into view
  strictAssert: true        # a step that couldn't run fails the run
  stepDelayFloorMs: 150     # pause after each step, ms (0–2000, default 80)
  transitionSettleMs: 900   # longest wait before each step, ms (0–10000, default 600)
  chaos: slow               # bad-network profile: slow, flaky, truncated, offline-midstep
steps: …
```

A misspelt key, or a value out of range, stops the flow from loading and lists the valid ones.

## When a flag and the flow disagree

For one flow, a flag you type wins: a one-off run is what flags are for. For a folder of flows, the flow wins, because one flag can’t suit every flow in a suite. Try both:

Either way, the run prints which value it used before the first step:

```shell
squiggle flows .squiggle/flows --direct-scroll
```

Output:

```text
! flow config: scrollMode=gesture (was direct) — declared by the flow (a batch run never overrides it)
```

A step’s own `mode:` beats both. Only flags you type count: a default never overrides the flow.

> **What a flow can't declare**
>
> Settings that are fixed when the app launches (animations, network capture), that name where you run (simulator, port, profile), or that change from one run to the next (`--fresh`, `--record`, `--junit`) are flags only. Putting one in `config` stops the flow from loading, with the reason.

## Run a flow on a bad network

`--chaos` degrades every request the app makes during the run, so you reach the loading, error and retry states without breaking your own connection:

| Profile | What the app gets |
| --- | --- |
| `slow` | Every response, 2 seconds late. |
| `flaky` | A 500 for the 1st, 6th, 11th… request. The same requests fail on every run. |
| `truncated` | Every response body cut off halfway. |
| `offline-midstep` | Everything works until the middle step (the earlier of the two, for an even number of steps), then every request fails as if the device went offline. |

```shell
squiggle flow orders --chaos offline-midstep
```

A flow whose subject is an error state can declare its profile instead: `config: { chaos: flaky }`. Then `--chaos none` runs it on a working network.

The run turns on network capture, which happens when the app launches. If the app is already running without it, the run fails before its first step and says so; run it again with `--fresh`. Requests to Metro and to Squiggle itself are never degraded.

## Run a flow without the network

`--stubs` answers every request the app makes from an earlier run of the flow, so it runs with no server and no test data to maintain. First run it once against the network with `--capture-net`, then pass that run’s id, the name of its log in `.squiggle/runs`, or `latest` for the run just before:

```shell
squiggle flow orders --capture-net
squiggle flow orders --stubs latest
squiggle flow orders --stubs mve7k1uh-2-4zs6
```

Each endpoint gets the status it returned most often, its median response time, and a body of the same shape: the same fields, types and list lengths, with placeholder values (`title 1`, `title 2`, ids counting up from 1). The run lists every stub before its first step.

> **What is recorded about a response**
>
> With network capture on, a development build of your app records the shape of each JSON response, never its values. Fields whose names look like secrets (tokens, passwords, session ids) are left out, and an object keyed by data, such as user ids or emails, keeps only how many keys it had. A release build records no shapes.

A request that never got a response is not stubbed. An endpoint whose shape wasn’t recorded, because it isn’t JSON or the run had network capture off, is answered with its status and an empty body. Stubs combine with `--chaos`: a stubbed request is answered, and the profile degrades the rest.

## Check every screen in each setup

`--matrix` runs the flow three times, each from a fresh launch: once in the setup the simulator is in, once in the other appearance (light or dark), and once at the largest text size. On the first visit to each screen it checks for controls that overlap, get cut off or end up off-screen, and reports only what a setup adds to the first run, since a problem in every setup is about the screen, not the setup. The simulator’s settings are put back afterwards, even when a run fails or you stop it with Ctrl-C.

```shell
squiggle flow settings --matrix
```

The run lists each setup as `as found (…)`, `light` or `dark`, and `largest text`, with each problem it adds and its source line. It exits 1 when a setup adds any. With `--json` it prints `{ cells: [{ cell, ran, screens, findings, added }] }`, where each entry in `added` has a `route`, `check`, `handle` and `detail`.

`--matrix` needs an iOS simulator. On a Mac app it stops with `E_MATRIX_PLATFORM` and exit code 2.

## Repair what a setup breaks

When a screen’s spacing is declared with [`defineTunable`](https://squiggle.sh/docs/reference/sdk#definetunable), `--repair` searches it for values that work in every setup. Name the tunables it may move:

```shell
squiggle flow layout-lab --matrix --repair layout.gap,layout.padding,layout.minHeight
```

Output:

```text
  ✗ largest text: 1 screen(s) audited · 1 finding(s) · 1 not in the first cell
    layout-lab · noClipping · 'layout-sign-out' is cut off by the viewport edge — 67% of it is on screen
  ✗ before · as found (dark, large): loss 16.7: layout-edit-profile hitTarget 3.3 pt; …
  ✗ before · light: loss 16.7: layout-edit-profile hitTarget 3.3 pt; …
  ✗ before · largest text: loss 33.2: layout-sign-out noClipping 33.2 pt
  ✔ search · grid: layout.gap 0, layout.padding 0, layout.minHeight 44: no finding in any cell after 23 probe(s)
  ✔ after · as found (dark, large): loss 0
  ✔ after · light: loss 0
  ✔ after · largest text: loss 0
  ✔ repaired: layout.gap 2, layout.padding 2, layout.minHeight 44: no finding in any of 3 cells, 32 probe(s)
  .squiggle/tune/mvglnkg2-repair-8t5h.json · propose_fix { tuning: 'mvglnkg2-repair-8t5h' } turns it into an edit
```

After the matrix, it runs the flow once to reach its last screen and measures that screen in each setup, relaunching the app for each one. Each problem is scored by how far it is from fixed, in points: how far a control reaches past the screen’s edge, how deep two controls overlap, and how far a control falls short of 44 pt. The score is 0 only when nothing is wrong, and a control that was on the screen at the start must still be on it. It sets the tunables in the running app and changes nothing else.

It accepts values only when the score is 0 in every setup, including the one the simulator was in, so a fix for the largest text can’t break the default. Then it moves each value onto the nearest of the tunable’s `tokens` that still works, and flags a value it couldn’t move. It stops after 40 measurements for each setup that had a problem, and exits 1 when nothing it tried clears every setup. A control a scroll view can bring back doesn’t count as lost. `--repair-search` picks the search: `grid` (the default) tries the values in order from the smallest; `random:<seed>` and `descent` are there to check it against. `--repair-control <seeds>` runs that check: it counts what the grid, random search over that many seeds and descent each spend to reach a clean layout, on one set of measurements. On the Layout Lab the grid needed 13 measurements, random search 40 on average over 100 seeds, and descent never got there. The control takes thousands of measurements, so it keeps them in `.squiggle/tune/` and a stopped run picks up where it left off. The result is saved under `.squiggle/tune/`, and an agent turns it into edits of each tunable’s `default` with `propose_fix { tuning }`.

> **A control pushed out of sight**
>
> The checks look at the controls the accessibility tree reports as displayed, which is a control whose centre is on the screen. So the matrix also remembers which controls each screen showed in the first setup. In a later setup, a control that is no longer displayed is a problem, unless a scroll view can bring it back the way it went: a control below the fold of a list or a page is reachable, one pushed off a screen that doesn’t scroll is not. It needs the app launched with `squiggle up`; without that answer the control is listed as not checked, never passed.

## Run a flow the way VoiceOver reaches it

`--ax-only` finds the control for every tap, type, swipe and scroll through the accessibility tree alone. A step that passes normally and fails here is a control a screen-reader user can’t reach.

```shell
squiggle flow checkout --ax-only
```

- Only `testID`, `label`, `text` and `role` can be matched, and `text` matches only the accessibility label, which is what VoiceOver reads. A step whose selector uses `component`, `callSite`, `source`, `within`, `nth`, `point` or `route` is reported as unsupported, and fails the run only with `--strict-assert`.
- Two elements that VoiceOver announces the same way fail the step instead of one being picked.
- There is no healing and no navigation fallback: a selector either reaches the control or the step fails.
- Assertions still read the app’s own UI tree, so only how controls are reached changes.
- It needs a simulator; with `--headless` every action is unsupported.

A step that fails here fails with `ax_unreachable`. Give the control an `accessibilityLabel` or a `testID` that names it uniquely.

> **Repeat a timing-dependent failure**
>
> Set `SQUIGGLE_JITTER_SEED` to a whole number to vary the pauses, swipe durations and tap holds of each step, and find a step that only passes at one speed. The same seed gives the same timings, but not always the same failure, because the app’s own timing still varies from run to run. Run a failing seed several times before you treat it as a reproduction.

## Name where a tap lands

A tap that opens another screen can name it with `expectRoute`. Once that screen appears, the next step starts without waiting out the rest of the transition. Naming the wrong screen only costs the wait. Recorded flows fill it in for you.

```yaml
- tap: { text: Checkout, expectRoute: OrderConfirmation }
```

Every key is in the [Flow syntax](https://squiggle.sh/docs/reference/flow) reference.
