{"record":{"id":"2c567907e911faa0","repo":"mastra-ai/mastra","slug":"error-message-workflow-resume-already-claimed-con","errorCode":null,"errorMessage":"error.message (workflow resume already claimed conflict)","messagePattern":"error\\.message \\(workflow resume already claimed conflict\\)","errorType":"http","errorClass":"HTTPException","httpStatus":409,"severity":"warning","filePath":"packages/server/src/server/handlers/error.ts","lineNumber":117,"sourceCode":"        attempted: error.attempted,\n        offendingLabel: error.offendingLabel,\n      },\n    };\n    const res = new Response(JSON.stringify(body), {\n      status: 422,\n      headers: { 'content-type': 'application/json' },\n    });\n    throw new HTTPException(422, {\n      res,\n      message: error.message,\n      cause: error,\n    });\n  }\n\n  // A losing concurrent resume is a conflict on run state, not a malformed request, so it maps\n  // to 409 and clients can distinguish it from a 400/500 and re-read the run.\n  if (isWorkflowResumeAlreadyClaimedError(error)) {\n    throw new HTTPException(409, {\n      message: error.message,\n      stack: error.stack,\n      cause: error,\n    });\n  }\n\n  if (isWorkflowSchemaValidationError(error)) {\n    throw new HTTPException(400, {\n      message: error.message,\n      stack: error.stack,\n      cause: error,\n    });\n  }\n\n  const apiError = error as ApiError;\n\n  const apiErrorStatus = apiError.status || apiError.details?.status || 500;\n","sourceCodeStart":99,"sourceCodeEnd":135,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/handlers/error.ts#L99-L135","documentation":"handleError detects errors whose id is WORKFLOW_RESUME_ALREADY_CLAIMED and rethrows them as an HTTPException with status 409 Conflict. The library maps this case explicitly because a concurrent resume of the same workflow run is a state conflict on the run, not a malformed request or a server fault. Clients are expected to distinguish 409 from 400/500 and re-read the run's current state instead of blindly retrying the resume.","triggerScenarios":"Two callers concurrently call the resume/stream endpoints for the same workflow run (CREATE_AGENT_BUILDER_ACTION_RUN_ROUTE / STREAM_AGENT_BUILDER_ACTION_ROUTE path); the run's suspend/resume was already claimed by another worker between the read and the resume call.","commonSituations":"A user double-clicks a 'resume' button; two server instances process the same suspended run from a queue; a client retries a timed-out resume request that actually succeeded on the first attempt.","solutions":["Treat HTTP 409 as expected: re-fetch the run state (GET_AGENT_BUILDER_ACTION_RUN_BY_ID_ROUTE) and continue from its current status instead of resuming again.","Add client-side idempotency: disable the resume control while a resume request is in flight and debounce duplicate submissions.","If consumers are workers, use single-claim semantics (e.g. one worker per run via queue/lock) so only one resume is attempted."],"exampleFix":"// before: blindly resuming, crashing on conflict\nawait workflow.resume({ runId });\n// after: tolerate the losing concurrent resume\ntry {\n  await workflow.resume({ runId });\n} catch (e) {\n  if (isConflict409(e)) {\n    const run = await getRun(runId); // re-read state, another caller claimed the resume\n    return run;\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"const run = await getAgentBuilderActionRun(runId);\nif (run.status !== 'suspended') {\n  throw new Error(`Run ${runId} is not suspended (status: ${run.status}); nothing to resume`);\n}","typeGuard":"function isConflictError(e: unknown): e is { status: 409 } {\n  return !!e && typeof e === 'object' && (e as any).status === 409;\n}","tryCatchPattern":"try {\n  await streamAgentBuilderActionRun(runId, resumeData);\n} catch (e) {\n  if (isConflictError(e)) {\n    return getAgentBuilderActionRun(runId); // re-read: another caller claimed the resume\n  }\n  throw e;\n}","preventionTips":["Disable resume buttons/controls while a resume request is in flight; debounce duplicates.","In worker setups, ensure only one worker owns a run at a time (lease/lock).","Treat 409 as an expected outcome in retry policies — re-read, don't blindly retry."],"tags":["http-409","concurrency","workflow","race-condition"],"backgroundTag":"workflow-resume-already-claimed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}