Phase transitions are not atomic
useWorkflow.executeTransition performs a phase change as a sequence of independent
client-issued writes, with no transaction wrapping them:
INSERTintoworkflow_transitions(the audit row)UPDATE cases(status + base-date side-effects)UPDATE case_deadlines(auto-complete the leaving phase's deadlines)- a loop of one
INSERTper checklist item intocase_checklist_items - auto-create
case_gatesfor the new phase UPSERTintocase_deadlinesfor the new phase's rulesINSERTintocase_audit_events
If the browser dies, the network drops, or any single step errors after the first, the case is left in a partially transitioned state — e.g. status advanced but no checklist created, or checklist half-inserted. Nothing rolls back. There is no idempotency key, so a retry re-runs the successful steps too (duplicate transitions, duplicate checklist items).
There is also no optimistic concurrency: the mutation reads the case, then updates by
id with no status guard in the WHERE. Two users advancing the same case race to
last-write-wins.
The tacit assumption: exactly one actor drives a case, on a reliable connection, and never mid-fails.
What breaks: corrupt cases that the UI can't represent (see three status generations), duplicated checklist items, orphaned gates.
Rebuild implication
Make a transition a single server-side transactional operation (one RPC / one
application-service command) with an idempotency key and an optimistic from_status
guard. This is the same server-side move required by
client-side integrity.