mastra-ai/mastra · error · HTTPException

Access denied: durable run belongs to a different resource

Error message

Access denied: durable run belongs to a different resource

What it means

Thrown by validateDurableToolCallAccess (used by the approve/decline tool-call routes, both generate and stream variants) when no durable workflow run exists for the given runId under the AGENTIC_LOOP workflow. The 403 intentionally hides whether the run exists, tying the run to the caller's resource scope.

Source

Thrown at packages/server/src/server/handlers/agents.ts:217

  runId,
  toolCallId,
  requestContext,
}: {
  mastra: any;
  agent: Agent;
  runId: string;
  toolCallId: string;
  requestContext: RequestContext;
}): Promise<void> {
  if (!isDurableAgentLike(agent)) return;

  const workflowsStore = await mastra.getStorage()?.getStore('workflows');
  const workflowRun = await workflowsStore?.getWorkflowRunById({
    workflowName: DurableStepIds.AGENTIC_LOOP,
    runId,
  });
  if (!workflowRun) {
    throw new HTTPException(403, { message: 'Access denied: durable run belongs to a different resource' });
  }

  let snapshot = workflowRun.snapshot as Record<string, any> | string | undefined;
  if (typeof snapshot === 'string') {
    try {
      snapshot = JSON.parse(snapshot) as Record<string, any>;
    } catch {
      snapshot = undefined;
    }
  }

  const input = snapshot?.context?.input;
  const persistedResourceIds = new Set(
    [
      workflowRun.resourceId,
      input?.state?.resourceId,
      input?.messageListState?.memoryInfo?.resourceId,
      input?.requestContextEntries?.[MASTRA_RESOURCE_ID_KEY],

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Confirm the runId comes from a currently-suspended durable agent run in the same storage backend/environment
  2. Re-check that the caller's resourceId matches the resource that started the run — approve/decline from the originating resource's context
  3. Handle the 403 by refreshing run state from the agent's tool-call list instead of retrying the stale runId

Example fix

// before
await client.approveToolCall({ runId: staleRunId, toolCallId })
// after
const pending = await client.getPendingToolCalls({ resourceId });
if (pending.some(c => c.runId === runId && c.toolCallId === toolCallId)) {
  await client.approveToolCall({ runId, toolCallId });
}
Defensive patterns

Strategy: try-catch

Validate before calling

// client-side: only act on runIds returned by the durable agent run in this environment
const pending = await getPendingToolCalls({ resourceId: myResourceId });
if (!pending.some(c => c.runId === runId)) return skip();

Type guard

function isRunnableRun(run: { status: string } | null | undefined): run is { status: 'suspended' } {
  return !!run && run.status === 'suspended';
}

Try / catch

try {
  await client.approveToolCall({ runId, toolCallId });
} catch (e) {
  if (isHttpException(e, 403) && String(e.message).includes('durable run')) {
    // run unknown to this storage/resource: refresh pending list, do not retry
  }
}

Prevention

When it happens

Trigger: POSTing to the durable tool approve/decline endpoints with a runId that was never persisted, was already completed and pruned, or belongs to another resource; storage not returning the run because getWorkflowRunById is scoped by workflowName DurableStepIds.AGENTIC_LOOP and the run was created under a different workflow.

Common situations: Approving a tool call after the run finished and its snapshot was cleaned up; sharing a runId between users/resources (multi-tenant setups); pointing at the wrong Mastra server/environment where the runId does not exist; calling approve twice after the run transitioned out of suspension.

Understand the failure class

Related errors


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