Skip to content

feat(openworkflow, server, dashboard): add resume workflow run functionality and UI components#624

Open
vudc wants to merge 4 commits into
openworkflowdev:mainfrom
vudc:feat/resume-workflow
Open

feat(openworkflow, server, dashboard): add resume workflow run functionality and UI components#624
vudc wants to merge 4 commits into
openworkflowdev:mainfrom
vudc:feat/resume-workflow

Conversation

@vudc

@vudc vudc commented Jul 22, 2026

Copy link
Copy Markdown

Closes #520

Supersedes #521, which I was unable to reopen (GitHub refuses the transition on both the GraphQL and REST endpoints, with no reason given). This PR is the same work, rebased onto current main and with the outstanding review items addressed.

Summary

Adds resumeWorkflowRun(workflowRunId) to the client and backend, flipping a failed workflow run back to pending so the worker picks it up on the next tick. Completed steps are served from the existing step-attempt cache and are not re-executed; the failing step starts with a fresh retry budget. A "Resume Run" button is wired up on the run-detail page of the dashboard.

Changes

Backend

  • Backend.resumeWorkflowRun({ workflowRunId }) added to the interface, implemented for Postgres and SQLite. In a single transaction it flips status from failed to pending, clears error / worker_id / started_at / finished_at, sets available_at = NOW(), and deletes the run's unsuccessful step_attempts. Non-failed runs throw a clear error.
  • ow.resumeWorkflowRun(workflowRunId) client method.
  • New helper resolveResumeWorkflowRunConflict in core/workflow-run.ts.

Dashboard

  • New RunResumeAction component with a confirmation dialog, following the existing RunCancelAction structure.
  • resumeWorkflowRunServerFn in lib/api.ts, alongside cancelWorkflowRunServerFn.
  • isRunResumableStatus helper in lib/status.ts (currently "failed" only).
  • Button rendered next to the existing "Cancel Run" on the run-detail page.

Tests

  • New resumeWorkflowRun() block in the shared testing/backend.testsuite.ts, so both SQLite and Postgres are covered. The previous revision tested only through worker/execution.test.ts, which runs against Postgres, leaving the SQLite implementation untested.
  • Two end-to-end tests in worker/execution.test.ts: a happy path asserting the completed step is not re-executed, and a negative path for a non-failed run.

What changed since #521

Rebased onto current main, plus the following review fixes.

Only failed step attempts were deleted. StepAttemptStatus includes running, and sleep, signal-wait and child-workflow attempts stay in that state for their whole lifetime. A run can reach failed with one still running, and since step-history.ts builds runningByStepName from whatever attempts it loads, a surviving row made replay treat that step as in-flight instead of re-executing it. Resume now deletes everything that is not completed or succeeded. The new testsuite case fails against the old query, with the leftover running-step attempt surviving the resume.

started_at was not cleared. rescheduleWorkflowRunAfterFailedStepAttempt explicitly nulls it when sending a run back to pending; resume now matches.

SQLite transaction control flow. resolveResumeWorkflowRunConflict threw from inside the try, so the catch issued a second ROLLBACK on an already-rolled-back transaction, which better-sqlite3 rejects and the inner catch {} swallowed. BEGIN now sits outside the try (matching the other transactional methods in this file) and the conflict lookup happens after the transaction closes.

Postgres read outside the transaction. The conflict lookup called this.getWorkflowRun, which uses the outer connection. It now reads through tx, and the method uses the shared withTransaction helper rather than pg.begin with a cast.

Removed the flaky-payment demo. It drove failures from module-scoped mutable state in a workflow handler, which is not a pattern worth demonstrating and made the demo dependent on worker process lifetime.

Review items not changed, with reasoning

  • RETURNING * returning snake_case columns. cancelWorkflowRun and markWorkflowRunFailed in the same file use the identical this.pg<WorkflowRun[]> plus RETURNING * pattern; the column transform is configured globally.
  • Trigger button not wrapped in AlertDialogTrigger. RunCancelAction uses the same controlled open state with a sibling <Button>. Changing only the resume component would leave the two inconsistent. Worth doing for both in a separate PR if you would like.
  • Dialog closing before an error can render. Same situation: the behaviour is inherited verbatim from RunCancelAction.
  • No auth guard on the server function. Matches cancelWorkflowRunServerFn and the rest of the dashboard's server functions.
  • flakyPayment.spec may not exist. Moot now that the demo is removed; it was the same access hello-world.run.ts uses.

Testing

tsc --noEmit is clean. The SQLite and core suites pass locally (370 tests). I was not able to run the Postgres-backed suites in my environment, so the Postgres path relies on CI.

vudc and others added 4 commits July 22, 2026 21:53
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Backend correctness:
- Delete every non-successful step attempt on resume, not just failed
  ones. Sleep, signal-wait and child-workflow attempts sit in 'running'
  and could survive a resume, making replay treat the step as in-flight
  instead of re-executing it.
- Clear started_at so a resumed run reports its new lifecycle, matching
  rescheduleWorkflowRunAfterFailedStepAttempt.
- Restructure the SQLite transaction so the conflict lookup happens
  outside it, removing the double ROLLBACK on an already-closed
  transaction.
- Read the conflicting run through the transaction in Postgres instead
  of the outer connection, and use the shared withTransaction helper.

Tests:
- Add a resumeWorkflowRun() block to the shared backend testsuite so
  both SQLite and Postgres are covered; the SQLite path had none.
- Cover the non-existent-run branch of resolveResumeWorkflowRunConflict.
- Assert started_at is cleared and only successful attempts survive.

Remove the flaky-payment demo, which drove failures from module-scoped
mutable state in a workflow handler.
Correctness:
- Narrow the step-attempt cleanup to failed attempts plus inert running
  attempts (function, signal-send). Running sleep, signal-wait and
  workflow attempts are now preserved: deleting them orphaned live child
  runs via ON DELETE SET NULL, dropped already-delivered signals, and
  restarted durable timers from zero.
- Reset the run-level attempts counter on resume so a resumed run gets
  the fresh retry budget the UI promises.
- Gate resume on an unexpired deadline and reject deadline-expired runs
  with a clear error instead of destroying their failure history for a
  run the next tick would immediately re-fail.

Dashboard:
- Extract the shared RunActionDialog so resume and cancel stop tripping
  the zero-tolerance jscpd duplication gate.
- Use .validator instead of the removed .inputValidator, matching the
  sibling server functions.

Tests:
- Rewrite the attempt-cleanup test to assert in-flight attempts survive,
  and add a child-workflow test proving the linked child is not orphaned
  (both fail against the previous broad DELETE).
- Add deadline-rejection and attempts-reset coverage.
- Give the end-to-end resume test a 20s wait budget so the flaky step's
  1s retry backoff fits inside it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support manual retry for failed workflows

1 participant