mastra-ai/mastra · warning · HTTPException
error.message (workflow resume already claimed conflict)
Error message
error.message (workflow resume already claimed conflict)
What it means
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.
Source
Thrown at packages/server/src/server/handlers/error.ts:117
attempted: error.attempted,
offendingLabel: error.offendingLabel,
},
};
const res = new Response(JSON.stringify(body), {
status: 422,
headers: { 'content-type': 'application/json' },
});
throw new HTTPException(422, {
res,
message: error.message,
cause: error,
});
}
// A losing concurrent resume is a conflict on run state, not a malformed request, so it maps
// to 409 and clients can distinguish it from a 400/500 and re-read the run.
if (isWorkflowResumeAlreadyClaimedError(error)) {
throw new HTTPException(409, {
message: error.message,
stack: error.stack,
cause: error,
});
}
if (isWorkflowSchemaValidationError(error)) {
throw new HTTPException(400, {
message: error.message,
stack: error.stack,
cause: error,
});
}
const apiError = error as ApiError;
const apiErrorStatus = apiError.status || apiError.details?.status || 500;
View on GitHub (pinned to 75dd419e61)
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.
Example fix
// before: blindly resuming, crashing on conflict
await workflow.resume({ runId });
// after: tolerate the losing concurrent resume
try {
await workflow.resume({ runId });
} catch (e) {
if (isConflict409(e)) {
const run = await getRun(runId); // re-read state, another caller claimed the resume
return run;
}
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
const run = await getAgentBuilderActionRun(runId);
if (run.status !== 'suspended') {
throw new Error(`Run ${runId} is not suspended (status: ${run.status}); nothing to resume`);
} Type guard
function isConflictError(e: unknown): e is { status: 409 } {
return !!e && typeof e === 'object' && (e as any).status === 409;
} Try / catch
try {
await streamAgentBuilderActionRun(runId, resumeData);
} catch (e) {
if (isConflictError(e)) {
return getAgentBuilderActionRun(runId); // re-read: another caller claimed the resume
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- WORKFLOW_RESUME_ALREADY_CLAIMED
- ${file} was opened by another process during compaction — is
- This workflow run is still running, cannot time travel
- Workflow ${workflowId} not found
- Workflow ID is required
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/2c567907e911faa0.
Report an issue: GitHub.