{"record":{"id":"9aea56e65dcb1452","repo":"mastra-ai/mastra","slug":"workflow-resume-already-claimed","errorCode":"WORKFLOW_RESUME_ALREADY_CLAIMED","errorMessage":"This suspended workflow run was already resumed by another caller. Workflow \"${this.workflowId}\" run \"${this.runId}\" moved from \"${snapshot.status}\" to \"${current.status}\" before this resume could claim it. Only one resume() call may continue a given suspension; re-read the run state before resuming again.","messagePattern":"This suspended workflow run was already resumed by another caller\\. Workflow \"(.+?)\" run \"(.+?)\" moved from \"(.+?)\" to \"(.+?)\" before this resume could claim it\\. Only one resume\\(\\) call may continue a given suspension; re-read the run state before resuming again\\.","errorType":"error_code","errorClass":"MastraError","httpStatus":null,"severity":"error","filePath":"packages/core/src/workflows/workflow.ts","lineNumber":4369,"sourceCode":"      opts: { status: 'running', expectedStatus: 'suspended' },\n    });\n\n    if (claimed) {\n      return;\n    }\n\n    // The compare-and-set found a status other than `suspended`. Re-read so the error names the\n    // status the run actually landed in rather than guessing.\n    const current = await workflowsStore.loadWorkflowSnapshot({\n      workflowName: this.workflowId,\n      runId: this.runId,\n    });\n\n    if (!current) {\n      throw new Error('No snapshot found for this workflow run: ' + this.workflowId + ' ' + this.runId);\n    }\n\n    throw new MastraError({\n      id: 'WORKFLOW_RESUME_ALREADY_CLAIMED',\n      domain: ErrorDomain.MASTRA_WORKFLOW,\n      category: ErrorCategory.USER,\n      text:\n        `This suspended workflow run was already resumed by another caller. Workflow \"${this.workflowId}\" run \"${this.runId}\" ` +\n        `moved from \"${snapshot.status}\" to \"${current.status}\" before this resume could claim it. ` +\n        `Only one resume() call may continue a given suspension; re-read the run state before resuming again.`,\n      details: {\n        workflowId: this.workflowId,\n        runId: this.runId,\n        expectedStatus: 'suspended',\n        actualStatus: current.status ?? 'unknown',\n      },\n    });\n  }\n\n  protected async _resume<TResume>(\n    params: {","sourceCodeStart":4351,"sourceCodeEnd":4387,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/core/src/workflows/workflow.ts#L4351-L4387","documentation":"A MastraError (id WORKFLOW_RESUME_ALREADY_CLAIMED, USER category) thrown when two callers try to resume the same suspended run concurrently. The store's compare-and-set (status 'suspended' -> 'running') failed, meaning another resume already claimed the run; the error reports the status transition it observed. Only one resume() may continue a given suspension.","triggerScenarios":"Two resume() calls (e.g. from two server instances, a webhook retry plus a poller, or double-clicked UI) racing on the same runId while the storage adapter supports concurrent updates (updateWorkflowState CAS).","commonSituations":"At-least-once webhook deliveries retried resume calls; horizontally scaled deployments sharing one database; frontend firing resume twice; job scheduler retries after a timeout that actually succeeded.","solutions":["Treat as expected contention: catch it, re-load the run state, and skip if status is 'running'/'completed'.","Make resume idempotent on your side — key the trigger (webhook/job) by runId so duplicates are dropped.","Check the run's current status via getWorkflowRunState() before issuing another resume.","If using a store without concurrent-update support, note resumes cannot be de-duplicated atomically and guard at the application level (lock/queue)."],"exampleFix":"// before\nawait run.resume({ resumeData });\n// after\ntry {\n  await run.resume({ resumeData });\n} catch (e) {\n  if ((e as any).id === 'WORKFLOW_RESUME_ALREADY_CLAIMED') {\n    const state = await run.getWorkflowRunState(); // another caller resumed; re-read and no-op\n    return;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const state = await run.getWorkflowRunState();\nif (state.status !== 'suspended') return; // someone else already advanced it","typeGuard":"function isAlreadyClaimed(e: unknown): e is { id: 'WORKFLOW_RESUME_ALREADY_CLAIMED' } {\n  return typeof e === 'object' && e !== null && (e as any).id === 'WORKFLOW_RESUME_ALREADY_CLAIMED';\n}","tryCatchPattern":"try {\n  await run.resume({ resumeData, step });\n} catch (e) {\n  if (isAlreadyClaimed(e)) {\n    logger.info('Resume already claimed by another caller; skipping');\n    return;\n  }\n  throw e;\n}","preventionTips":["Make resume triggers idempotent (dedupe by runId).","Use a storage adapter with concurrent-update support so CAS claiming works.","Re-read run status before issuing another resume.","Avoid polling resume() on a timer; resume once per suspension event."],"tags":["workflow","concurrency","resume","race-condition"],"backgroundTag":"concurrent-resume-conflict","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T08:17:16.595Z"}