medusajs/medusa · error · MedusaError

Cannot skip a step when status is ${step.getStates().status}

Error message

Cannot skip a step when status is ${step.getStates().status}

What it means

skipTransactionStep/skipStep APIs mark a step as skipped so the flow continues without executing it. This is only legal while the step is still pending/awaiting response (e.g. WAITING_FOR_CONFIRMATION). Skipping a step that is already running, done, or failed would corrupt the flow, so MedusaError NOT_ALLOWED is thrown with the current status in the message.

Source

Thrown at packages/core/orchestration/src/transaction/transaction-orchestrator.ts:1902

      await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(
        responseIdempotencyKey,
        handler,
        transaction
      )

    if (step.getStates().status === TransactionStepStatus.WAITING) {
      this.emit(DistributedTransactionEvent.RESUME, {
        transaction: curTransaction,
      })

      await TransactionOrchestrator.skipStep({
        transaction: curTransaction,
        step,
      })

      await this.executeNext(curTransaction)
    } else {
      throw new MedusaError(
        MedusaError.Types.NOT_ALLOWED,
        `Cannot skip a step when status is ${step.getStates().status}`
      )
    }

    return curTransaction
  }

  /**
   * Manually force a step to retry even if it is still in awaiting status
   * @param responseIdempotencyKey - The idempotency key for the step
   * @param handler - The handler function to execute the step
   * @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
   */
  public async retryStep({
    responseIdempotencyKey,
    handler,
    transaction,

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Refresh the transaction/step state immediately before skipping and only proceed if the step is still awaiting (e.g. status 'waiting_for_response' / not invoked)
  2. Make skip endpoints idempotent: treat already-completed states as success no-op
  3. Guard against races with a per-transaction lock

Example fix

// before
await orchestrator.skipStep(transactionId, action)

// after
const [, step] = await orchestrator.getTransactionStep(transactionId, action)
if (step.getStates().status === 'invoking' || step.getStates().status === 'done') {
  return { skipped: false, reason: 'step already progressed' }
}
await orchestrator.skipStep(transactionId, action)
Defensive patterns

Strategy: validation

Validate before calling

const [, step] = await orchestrator.getTransactionStep(transactionId, action)
const skippable = ['waiting_for_response', 'not_started', 'queue']
if (skippable.includes(step.getStates().status)) {
  await orchestrator.skipStep(transactionId, action)
} else {
  return { skipped: false, status: step.getStates().status }
}

Try / catch

try { await orchestrator.skipStep(transactionId, action) } catch (e) { if (e.type === 'not_allowed' && /Cannot skip/.test(e.message)) return { skipped: false } throw e }

Prevention

When it happens

Trigger: Calling skipStep on a step whose status is not in the skippable set — e.g. after it already executed, while it is currently invoking, or after the transaction finished. Common with waitFor-style steps once their status advanced.

Common situations: User-approval flows where the skip request races with the step auto-completing; UI allowing skip on already-processed steps; retrying a skip request that already succeeded.

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/8b42c4d323629835. Report an issue: GitHub.