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
- 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)
- Make skip endpoints idempotent: treat already-completed states as success no-op
- 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
- Load fresh step state immediately before skipping
- Disable skip UI for steps that already progressed
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
- Cannot revert a permanent failed transaction.
- Incorrect action type.
- Cannot retry step when status is ${step.getStates().status}
- Method 'clearTransactionTimeout' not implemented.
- Method 'scheduleStepTimeout' not implemented.
AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27).
Data as JSON: /api/errors/8b42c4d323629835.
Report an issue: GitHub.