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

  1. 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.
  2. Add client-side idempotency: disable the resume control while a resume request is in flight and debounce duplicate submissions.
  3. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/2c567907e911faa0. Report an issue: GitHub.