Files
twenty/packages/twenty-client-sdk
Thomas Trompette ba94c3b857 feat(workflow): idempotent stop + retry failed runs from failing step (#21458)
https://github.com/user-attachments/assets/5a25396f-8959-4bd8-93cb-1187559ffe5f



## Summary

Two workflow-run improvements, with all non-trivial logic isolated in
pure, unit-tested utils.

### 1. Idempotent stop
`stopWorkflowRun` no longer throws when a run is already in a terminal
status (`COMPLETED` / `FAILED` / `STOPPED`) or already `STOPPING`; it
returns the run unchanged. This fixes:
- bulk stop aborting on the first non-stoppable run in a
mixed/select-all selection,
- the click-vs-processing race on a single run (run finishes between
click and mutation).

It also releases the cached not-started throttle slot when stopping a
`NOT_STARTED` run (prevents counter drift), and ends runs with no
`state` directly.

### 2. Retry a failed run from the failing step
New `retryWorkflowRun` mutation (same guards/passthrough as
`stopWorkflowRun`). It resets the failed step(s) to `NOT_STARTED`, flips
the run to `RUNNING`, and enqueues a `RunWorkflowJob` with the steps to
re-execute; downstream execution and status computation are unchanged.

Logic lives in pure utils:
- `build-retry-step-infos.util.ts` - decides per failed step what to
reset; delegates iterator-specific logic to
`build-retry-iterator-step-infos.util.ts` (an iterator that failed
mid-loop is restored to `RUNNING` with cursor preserved, an iterator
that failed itself restarts its whole loop).
- `get-runnable-step-ids.util.ts` - reuses the executor's
`shouldExecuteStep` to also resume branches that never started (avoids
hangs), excluding loop-interior steps.

The service method only orchestrates; the job's status check is a race
guard (retriability is enforced in the service before enqueue).

A "Retry" command menu item surfaces only for `FAILED` runs
(`someEquals(selectedRecords, "status", "FAILED")`).

### 3. Keep the run diagram visible across regenerations
The run diagram is regenerated on every run state change, producing
fresh nodes without the dimensions Reactflow had measured. Reactflow
hides unmeasured nodes until it re-measures them, so the diagram could
flicker and disappear when the last regeneration before going idle left
nodes unmeasured (reproducible after retrying a failed run). The
regenerated nodes now carry over the previously measured dimensions (by
id) so they stay rendered.

## Test plan
- [x] Unit tests for both retry utils (9 cases: plain failed step,
non-failed untouched, iterator mid-loop restore, iterator self-failure,
frontier parent gating, entry steps, loop-interior exclusion, parallel
branches)
- [x] `twenty-server` + `twenty-front` typecheck
- [x] `lint:diff-with-main` clean for both packages
- [x] Manual: retry a failed run repeatedly and confirm the diagram
stays visible
- [ ] Manual: stop a COMPLETED/mixed selection (no error), retry a
failed run and confirm it resumes from the failing step
2026-06-12 15:25:53 +00:00
..