Skip to content

fix(core): preserve approval data on workflow restart - #1436

Open
pei711 wants to merge 1 commit into
VoltAgent:mainfrom
pei711:fix/workflow-restart-approved-step
Open

pei711 wants to merge 1 commit into
VoltAgent:mainfrom
pei711:fix/workflow-restart-approved-step

Conversation

@pei711

@pei711 pei711 commented Oct 5, 2026 •

Copy link
Copy Markdown

PR Checklist

  • The commit message follows our guidelines.
  • Related issue linked.
  • Tests for the changes have been added.
  • Docs have been added / updated (no public documentation change needed).
  • Changeset has been added.

What is the current behavior?

If a workflow process crashes after an approval is accepted and a step begins, workflow.restart(executionId) restores the pre-resume checkpoint. The step loses its resumeData and can suspend for the same approval again.

What is the new behavior?

Persist the resume data and checkpoint before executing the resumed step, then restore them during restart. Expose a stable stepExecutionId (executionId:stepId) in the workflow execution context so integrations can use it as an idempotency key for external side effects. Clear the one-shot resume checkpoint after the resumed step is checkpointed.

Fixes #1435

Notes for reviewers

Added a regression test covering approval recovery after an interrupted side effect, including reuse of the stable step identity.

Validation:

  • vitest run --typecheck packages/core/src/workflow/core.spec.ts: 31 tests passed.
  • tsc --noEmit -p packages/core/tsconfig.json: passed.
  • lerna run build --scope @voltagent/core --include-dependencies: passed.
  • Full @voltagent/core suite: 1,396 passed, 2 skipped, 1 todo; 5 unrelated workspace sandbox tests failed on Windows with spawn C:Program ENOENT.

Summary by cubic

Fixes workflow restart losing approval data after an interrupted approval side effect. When a process crashes after an approval is accepted, workflow.restart(executionId) now restores the persisted resume data and checkpoint so the step resumes with the same approval instead of suspending again.

  • Exposes a stable stepExecutionId (executionId:stepId) in the workflow execution context for use as an idempotency key around external side effects.
  • Clears the one-shot resume checkpoint once the resumed step is checkpointed.
  • Adds a regression test covering approval recovery after an interrupted side effect.

Written for commit 2d4b3f0. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Interrupted workflows now retain their resume data when restarted, helping them continue from the appropriate step.
    • Workflow steps now have a stable execution ID that can be used to avoid repeating external side effects, such as a payment, when a workflow resumes.

@changeset-bot

changeset-bot Bot commented Oct 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2d4b3f0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a6743a0f-88cc-4607-85d0-ce3b3ecc998b
📥 Commits

Reviewing files that changed from the base of the PR and between 72a46c7 and 2d4b3f0.

📒 Files selected for processing (5)
  • .changeset/warm-ants-restart.md
  • packages/core/src/workflow/context.ts
  • packages/core/src/workflow/core.spec.ts
  • packages/core/src/workflow/core.ts
  • packages/core/src/workflow/registry.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The workflow registry now stores resume data and checkpoint details before resumed execution. Restart restores a valid pending resume checkpoint. Each step receives a stable execution ID, and completion of the resumed step persists a checkpoint and clears the pending resume metadata.

Changes

Workflow Resume Recovery

Layer / File(s) Summary
Persist and select resume checkpoints
packages/core/src/workflow/registry.ts, packages/core/src/workflow/core.ts
The registry stores resume data, step index, event sequence, and checkpoint in workflow metadata. Restart detects valid pending resume metadata and passes it to execution; otherwise, it uses the regular restart checkpoint.
Continue resumed execution with stable identity
packages/core/src/workflow/context.ts, packages/core/src/workflow/core.ts, packages/core/src/workflow/core.spec.ts, .changeset/warm-ants-restart.md
Each step receives an ID formed from the workflow execution ID and step ID. Completion of the step receiving resume data forces checkpoint persistence and clears pending resume metadata. The added test checks that restart restores approval data, does not repeat a recorded payment, and reuses the step ID.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant WorkflowRegistry
  participant WorkflowState
  participant Workflow
  participant restartExecution
  participant Step
  Operator->>WorkflowRegistry: Submit resumeData
  WorkflowRegistry->>WorkflowState: Store resume checkpoint metadata
  Operator->>Workflow: Restart execution
  Workflow->>restartExecution: Call restartExecution
  restartExecution->>WorkflowState: Read pending resume checkpoint
  restartExecution->>Step: Execute with resumeData and stepExecutionId
  Step-->>restartExecution: Complete resumed step
  restartExecution->>WorkflowState: Persist checkpoint and clear resume metadata
Loading

Merge Risk: ⚪ Minimal · up to 2d4b3

No established issue blocks merging. Workflows configured without running checkpoints may replay unpersisted work after a crash, as expected for that setting.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 2d4b3

Normal interrupted recovery now preserves approval input and reuses the step identity. However, a new replay can inherit an earlier execution’s pending approval and reuse it after interruption. Because the replay has a different execution identity, the original idempotency key would not contain repeated side effects.

Retained concerns

  • Medium · security · inferred: A pending approval can cross into a distinct replay execution. If a resumed source run fails before checkpoint retirement, timeTravel copies its resume checkpoint into the new execution’s metadata because the lineage sanitizer removes only the regular restart checkpoint. If the replay is interrupted, restart prefers that inherited checkpoint and supplies the source approval and step state instead of the replay’s intended recovery state. The replay’s different executionId also produces a different stepExecutionId, so external deduplication keyed as documented would not prevent repeating the source action. Normal checkpointed success clears the record; the concern requires a retained pending record, replay and interruption. The new persistence and reader introduce this interaction with otherwise unchanged replay code.
Security review details

Security Blast Radius

  • inferred — The identified concern spans execution identities within the same workflow and its configured storage. Its security impact depends on application-defined resume inputs authorizing privileged side effects. It requires a source with retained pending input, creation of a replay, and interruption followed by recovery; anonymous reachability, cross-tenant exposure and production downstream privileges were not established.

Security Findings and Attack Paths

  • inferred — The introduced failure path is persisted source approval, terminal source failure, metadata inheritance into a new replay, interruption, then restart consuming the inherited approval. Recovery can thereby authorize work under a new idempotency identity without approval input supplied for that replay. This is a source-supported architecture concern, not a demonstrated production exploit.

Trust Boundaries and Controls

  • observed — Restart verifies persisted workflowId and running status. Replay verifies the source workflowId and rejects running sources. These controls prevent direct cross-workflow recovery but do not bind an inherited pending approval to its originating execution; the pending record contains a positional step index rather than an execution or workflow-version binding.
  • observed — The registry’s suspended-status check and active-controller map predate the PR, as do acceptance of caller resumeData and the inspected handler’s forwarding behavior. Missing caller-to-execution authorization and duplicate-resume ownership guarantees therefore remain contextual limitations, not independently established PR-introduced concerns.

Resilience and Maintainability Implications

  • observed — The regression test demonstrates same-execution approval restoration, stable step identity, provider-modeled deduplication and successful retirement. It manually changes error status back to running to model a killed process, so it does not establish replay-lineage isolation or real process-crash durability.
  • observed — Direct streaming resume paths invoke executeInternal without the registry’s new pre-execution persistence. Their approval-recovery limitation remains outside the implemented fix rather than becoming a separately retained introduced concern.

Hardening Proposals

  • proposed — Exclude pending resume authority from replay lineage metadata and bind recovery records to the originating execution and stable step identity. Reject mismatched records rather than treating copied metadata as approval for a new execution; define workflow-version compatibility before reusing approval across definition changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preserving approval data when a workflow restarts.
Description check ✅ Passed The description covers the current and new behavior, links issue #1435, reports tests and validation, and includes reviewer notes. It also records that no public documentation change is needed.
Linked Issues check ✅ Passed Issue #1435 requires restart to retain an accepted approval and provide a stable identity for the resumed step. The change persists resume data and checkpoint metadata before the resumed workflow runs…
Out of Scope Changes check ✅ Passed All reported changes support issue #1435. The workflow changes implement persistence and restoration, the context field exposes the stable step identity, the regression test covers recovery, and the c…
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 4 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/workflow/registry.ts">

<violation number="1" location="packages/core/src/workflow/registry.ts:254">
P2: This checkpoint is written while the execution is still `suspended`; a crash before `workflow.run()` changes it to `running` leaves `restart()` rejecting the saved approval. Set `status: "running"` in this update so the checkpoint is restartable.</violation>
</file>

<file name="packages/core/src/workflow/core.ts">

<violation number="1" location="packages/core/src/workflow/core.ts:1492">
P2: When the resumed step falls outside `checkpointInterval`, this forced save replaces the stored event history with the new `executeInternal`'s events, dropping the original run and suspension events. Merge the existing event history with the resumed events before persisting this checkpoint.</violation>

<violation number="2" location="packages/core/src/workflow/core.ts:1520">
P2: The resume checkpoint is cleared only inside `persistRunningCheckpoint`, which early-returns when `disableCheckpointing` is set. With `disableCheckpointing: true`, the completed step's `VOLTAGENT_RESUME_CHECKPOINT_KEY` is never deleted, so it stays in the persisted metadata. A later crash and `restart()` will then see a pending resume checkpoint whose step has already completed and replay that step with the stale `resumeData` (re-applying the same approval / duplicate side effects); the stale key also lingers on the completed execution state. Clear the one-shot resume checkpoint on resumed-step completion independently of the checkpoint-interval/disableCheckpointing logic.</violation>
</file>

Reply to a comment to ask cubic a question or push back. It learns from your replies.

Re-trigger cubic

// process dies while that step is running, restart() can replay it with
// the same resume data instead of presenting the approval again.
await registeredWorkflow.workflow.memory.updateWorkflowState(executionId, {
metadata: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This checkpoint is written while the execution is still suspended; a crash before workflow.run() changes it to running leaves restart() rejecting the saved approval. Set status: "running" in this update so the checkpoint is restartable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/workflow/registry.ts, line 254:

<comment>This checkpoint is written while the execution is still `suspended`; a crash before `workflow.run()` changes it to `running` leaves `restart()` rejecting the saved approval. Set `status: "running"` in this update so the checkpoint is restartable.</comment>

<file context>
@@ -246,6 +246,22 @@ export class WorkflowRegistry extends SimpleEventEmitter {
+        // process dies while that step is running, restart() can replay it with
+        // the same resume data instead of presenting the approval again.
+        await registeredWorkflow.workflow.memory.updateWorkflowState(executionId, {
+          metadata: {
+            ...workflowState.metadata,
+            [VOLTAGENT_RESUME_CHECKPOINT_KEY]: {
</file context>
Suggested change
metadata: {
status: "running",
metadata: {

...(stateManager.state?.usage ? { usage: stateManager.state.usage } : {}),
[VOLTAGENT_RESTART_CHECKPOINT_KEY]: restartCheckpoint,
});
if (isCompletingResumedStep) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The resume checkpoint is cleared only inside persistRunningCheckpoint, which early-returns when disableCheckpointing is set. With disableCheckpointing: true, the completed step's VOLTAGENT_RESUME_CHECKPOINT_KEY is never deleted, so it stays in the persisted metadata. A later crash and restart() will then see a pending resume checkpoint whose step has already completed and replay that step with the stale resumeData (re-applying the same approval / duplicate side effects); the stale key also lingers on the completed execution state. Clear the one-shot resume checkpoint on resumed-step completion independently of the checkpoint-interval/disableCheckpointing logic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/workflow/core.ts, line 1520:

<comment>The resume checkpoint is cleared only inside `persistRunningCheckpoint`, which early-returns when `disableCheckpointing` is set. With `disableCheckpointing: true`, the completed step's `VOLTAGENT_RESUME_CHECKPOINT_KEY` is never deleted, so it stays in the persisted metadata. A later crash and `restart()` will then see a pending resume checkpoint whose step has already completed and replay that step with the stale `resumeData` (re-applying the same approval / duplicate side effects); the stale key also lingers on the completed execution state. Clear the one-shot resume checkpoint on resumed-step completion independently of the checkpoint-interval/disableCheckpointing logic.</comment>

<file context>
@@ -1509,15 +1513,20 @@ export function createWorkflow<
+          ...(stateManager.state?.usage ? { usage: stateManager.state.usage } : {}),
+          [VOLTAGENT_RESTART_CHECKPOINT_KEY]: restartCheckpoint,
+        });
+        if (isCompletingResumedStep) {
+          delete checkpointMetadata[VOLTAGENT_RESUME_CHECKPOINT_KEY];
+        }
</file context>

const isCompletingResumedStep =
options?.resumeFrom?.resumeData !== undefined &&
lastCompletedStepIndex === options.resumeFrom.resumeStepIndex;
if ((lastCompletedStepIndex + 1) % checkpointInterval !== 0 && !isCompletingResumedStep) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the resumed step falls outside checkpointInterval, this forced save replaces the stored event history with the new executeInternal's events, dropping the original run and suspension events. Merge the existing event history with the resumed events before persisting this checkpoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/core/src/workflow/core.ts, line 1492:

<comment>When the resumed step falls outside `checkpointInterval`, this forced save replaces the stored event history with the new `executeInternal`'s events, dropping the original run and suspension events. Merge the existing event history with the resumed events before persisting this checkpoint.</comment>

<file context>
@@ -1485,7 +1486,10 @@ export function createWorkflow<
+        const isCompletingResumedStep =
+          options?.resumeFrom?.resumeData !== undefined &&
+          lastCompletedStepIndex === options.resumeFrom.resumeStepIndex;
+        if ((lastCompletedStepIndex + 1) % checkpointInterval !== 0 && !isCompletingResumedStep) {
           return;
         }
</file context>

This branch has not been deployed

No deployments
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.

[BUG] workflow.restart() after a crash re-suspends an approval that was already given, and approving again repeats the step's side effect

1 participant

Sponsor
SponsoredKunjungi sekarang
Promo