# Find the app state that breaks a flow

A flow passes from one saved state and fails from another, and the two differ in a dozen values. Squiggle finds the few that make it fail, and names the lines that write them and the components that read them.

This needs an app that `squiggle up` has prepared, with its state in Zustand or Legend State stores, which is what a save slot holds. Each run starts from the state Squiggle loads, so the flow doesn’t relaunch the app, and its `prerequisite` is skipped.

## Save the two states

Put the app in a state the flow passes from and save it, then in one it fails from and save that:

```shell
squiggle save state-lab-green
squiggle save state-lab-failing
```

Take both on the same screen, where the flow starts. The search loads the slots on the device the flow runs on: the one you pass with `--udid`, or the booted simulator. See [Save and load app state](https://squiggle.sh/docs/reference/cli#save-and-load-app-state).

## Find the values that matter

```shell
squiggle flow state-lab --bisect-state state-lab-green,state-lab-failing
```

Output:

```text
bisect state state-lab · state-lab-green against state-lab-failing · 6 candidate(s)
field                         state-lab-green  state-lab-failing    cause
────────────────────────────  ───────────────  ───────────────────  ─────
zustand:useStateLab.coupon    ""               "SPRING"             yes
zustand:useStateLab.currency  "EUR"            "USD"
zustand:useStateLab.giftWrap  false            true
zustand:useStateLab.note      ""               "leave at the door"
zustand:useStateLab.quantity  1                3
zustand:useStateLab.region    "eu"             "us"
  ✔ found: the failure needs zustand:useStateLab.coupon, 1 of 6 candidates
  ! restores: 7, against 10 for one candidate at a time
  ! useStateLab.coupon: declared at apps/example/src/store/stateLab.ts:64
  ! useStateLab.coupon: written at apps/example/src/store/stateLab.ts:73 (loadReturningCart)
  ! useStateLab.coupon: written at apps/example/src/store/stateLab.ts:76 (setCoupon)
  ! useStateLab.coupon: read by CouponRow at src/app/state-lab.tsx:36
  ! useStateLab.coupon: read by OrderTotal at src/app/state-lab.tsx:45
  .squiggle/agent/briefs/flow-state-bisect-state-lab-state-lab-green-state-lab-failing-zustand-usestatela.json
```

Squiggle runs the flow from each slot first, to check that one passes and the other fails. Then it loads the passing slot with some of the failing slot’s values put in, runs the flow again, and narrows to the smallest set of values that still fails. It loads that set once more to confirm the failure, and finishes by loading the passing slot, so the app is left in a state that works.

The table lists every value that differs between the two slots, and marks the ones the failure needs. Under it, for each of those, are the line that declares the store, each line that writes the value, and each component that reads it. The same findings are saved as a brief in `.squiggle/agent/briefs/`, where your coding agent picks them up with the `briefs` MCP tool. Agents can run the same search with the `bisect_state` MCP tool.

> **Each load goes through your app**
>
> Squiggle loads every state the same way `squiggle load` does. If a store’s fields no longer match the slot, your app refuses the load and the search stops; save the slots again.

## When it can’t say

| The result says | What it means, and what to do |
| --- | --- |
| `green-fails` | The flow fails from the first slot too. Save a slot the flow passes from. |
| `failing-passes` | The flow passes from the second slot. The failure needs something a slot doesn’t hold, such as the network or timing. |
| `outside-candidates` | Loading every differing value still passes, so the cause is somewhere the slots don’t compare. MMKV storage is kept as it is in the first slot, and is named here when the slots hold any. |
| `not-reproduced` | The smallest set failed once and passed when loaded again. The flow doesn’t give the same result every time; make it repeatable first. |
| `refused` | Your app refused to load a state, or a slot name doesn’t exist. The reason follows. |

The command exits 1 unless it finds the values. It can’t be combined with `--fresh`, `--cache-twins`, `--tune`, `--twin`, `--fuzz` or `--matrix`, which each choose what a run starts from.

## Races that depend on state

```shell
squiggle flow race-lab --race --bisect-state race-green,race-failing
```

Output:

```text
bisect state race-lab (duplicate-mutation) · race-green against race-failing · 4 candidate(s)
  ✔ found: the failure needs zustand:useRaceLab.guardSubmit, 1 of 4 candidates
  ! restores: 7, against 8 for one candidate at a time
  ! useRaceLab.guardSubmit: written at apps/example/src/store/raceLab.ts:28 (toggleGuard)
bisect state race-lab (navigation-after-back) · race-green against race-failing · 4 candidate(s)
  ✗ green-fails: the flow fails from race-green too
```

Add `--race` to search for races from the failing slot. For each race it finds, Squiggle first shortens the flow to the fewest steps that still reproduce it, then searches the state with that shorter flow, so the brief names both. A race that happens from the passing slot too doesn’t depend on the state, and is reported as `green-fails`. See [Find what breaks on a bad network](https://squiggle.sh/docs/flows/network) for the race search itself.
