Skip to main content

Phase transitions are not atomic

useWorkflow.executeTransition performs a phase change as a sequence of independent client-issued writes, with no transaction wrapping them:

  1. INSERT into workflow_transitions (the audit row)
  2. UPDATE cases (status + base-date side-effects)
  3. UPDATE case_deadlines (auto-complete the leaving phase's deadlines)
  4. a loop of one INSERT per checklist item into case_checklist_items
  5. auto-create case_gates for the new phase
  6. UPSERT into case_deadlines for the new phase's rules
  7. INSERT into case_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.