Resilience and healing
Fallback selectors and degraded matches when the app drifts, waiting on slow operations, and healing a stale trace.
Replay walks each step's recorded selector ladder in order: the native id
first, then structural (control type + accessible name), then a text
anchor. If the primary selector is dead but a fallback rung still finds the
element, the step runs and the flow stays green, but the step and the run
are marked degraded in result.json (with the matched tier in
selector_tier), the CLI prints a DEGRADED: line pointing at heal, and
RunResult.degraded is set in Python. Degraded-but-passing is the signal
to heal the trace before the remaining rungs die too:
[PASS] s0002 Press plus (matched via structural fallback)
PASS: Add two numbers (2154 ms) -> .flowproof\runs\...\report.html
DEGRADED: fallback selectors were needed, the app drifted; run `flowproof heal calc.flow.yaml`Waiting on slow operations (no sleeps, still deterministic)
Assertions auto-wait: the engine polls until the expectation holds or a
bounded timeout elapses (default 10s), during recording and at every
replay. The bound is recorded into the trace, so replay waits exactly as
long as authoring allowed: deterministic, no sleeps in specs. For slow
backend operations, use an explicit wait step (default bound 60s) or a
within qualifier on either form:
steps:
- Press the generate button
- Wait until page shows Generation complete within 120s
- assert: page shows 100 rows within 5sRepair a spec while recording
When an authoring model is configured, record can repair a failing spec step.
It asks the model for a minimal edit, changes only the .flow.yaml being
recorded, and reruns the flow. Repair stops after three attempts, or earlier
when the same failure recurs without progress. Each attempt and edit is written
to <flow>.repair.json beside the trace.
Set a different limit, from 1 to 10, with --repair-attempts:
flowproof record shop.flow.yaml --repair-attempts 5Disable automatic repair when you want the original failure immediately:
flowproof record shop.flow.yaml --no-repairA model verdict that identifies an engine limitation without trying a fix is provisional. Flowproof gives the whole flow one independent attempt in a fresh driver session. If that attempt fails the same way, the repair report retains both failures as evidence. A repair that exhausted its edit budget does not get the extra attempt. The fresh attempt does not reset the limit: if it fails, the repair loop gets only the attempts the first round left unused.
Healing a stale trace
When the app changes and replay fails, heal re-authors the flow from the
spec against the live app and proposes a reviewable diff; it never touches
the trace on its own:
$ flowproof heal calc.flow.yaml
[CHANGED] s0002 Press plus (selectors)
REVIEW: calc.heal.html (before/after with frames)
PROPOSED: review calc.proposed.jsonl then apply it with --apply-proposal
$ flowproof heal calc.flow.yaml --apply-proposal # explicit opt-in--apply-proposal copies the proposal you reviewed over the trace and removes
it, without touching the app. --apply instead re-records and applies the
result straight away; since re-authoring can come out differently the second
time, it is for when you don't need to review first.
Heal re-records the way record does, so it takes the same --vars <file>,
--var KEY=VALUE (repeatable) and --headed / --headless flags, and loads
the flow's sibling .values.yaml the same way.
Alongside the machine-readable proposal, heal writes <name>.heal.html: a
self-contained review page with a before/after pair per changed step: the
frames each execution's recording captured for that step (recorded run vs.
re-authored run) plus the step JSON, rendered entirely from the structured
report. Open it to see what the app looked like when each version of the
step ran, then decide on --apply.
Healing only the fallback steps
When the run passed but was degraded, re-authoring is more than the drift
needs: the run already knows which rung found each drifted step. Point
heal at that run and it promotes the matched rung to the top of each
degraded step's ladder, keeps the dead one below it, and leaves every other
step byte-for-byte alone. Nothing is re-recorded, so no app or model is
needed:
$ flowproof heal calc.flow.yaml --from-run .flowproof/runs/20260924T100048.993Z
[CHANGED] s0002 Press plus (selectors)
PROPOSED: review calc.proposed.jsonl then apply it with --apply-proposalThe run must have replayed this trace (trace_id must match). --apply
and --apply-proposal work the same way. --step <id> (repeatable) repairs only the steps you
name, so a reviewer can accept some repairs and leave the rest; naming a step
the run did not reach through a fallback is an error.
Exit codes: 0 healthy (or applied), 1 changes proposed for review,
2 error. --json emits the structured report (including diff_html); the
Python API returns a HealResult (with diff_html: Path | None) and the
MCP tool mirrors it. --author <auto|rules|llm> picks the authoring backend
for the re-record, same meaning and same default (auto) as record's flag
of the same name.