medusajs/medusa · error · MedusaError

Transaction ${transactionId} could not be found.

Error message

Transaction ${transactionId} could not be found.

What it means

TransactionOrchestrator.loadTransactionById (used by resume/cancel/retry paths) retrieves the persisted transaction by model id and transaction id from storage. If storage returns nothing, NOT_FOUND is thrown — the transaction was never checkpointed, was already cleaned up, or the ids are wrong.

Source

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

  /** Returns an existing transaction
   * @param transactionId - unique identifier of the transaction
   * @param handler - function to handle action of the transaction
   */
  public async retrieveExistingTransaction(
    transactionId: string,
    handler: TransactionStepHandler,
    options?: { isCancelling?: boolean }
  ): Promise<DistributedTransactionType> {
    const existingTransaction =
      await TransactionOrchestrator.loadTransactionById(
        this.id,
        transactionId,
        { isCancelling: options?.isCancelling }
      )

    if (!existingTransaction) {
      throw new MedusaError(
        MedusaError.Types.NOT_FOUND,
        `Transaction ${transactionId} could not be found.`
      )
    }

    const transaction = new DistributedTransaction(
      existingTransaction.flow,
      handler,
      undefined,
      existingTransaction?.errors,
      existingTransaction?.context
    )

    return transaction
  }

  private static getStepByAction(
    flow: TransactionFlow,

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Verify the transactionId and modelId exactly match what was returned when the workflow started
  2. Ensure the same workflow storage backend/instance is used across restarts (persistent Redis/DB, not memory)
  3. Check retention: if executions expire or clearExpiredExecutions purged them, the transaction is gone — restart the workflow instead of resuming

Example fix

// before
await orchestrator.resume(txId) // throws if never persisted

// after
const existing = await orchestrator.retrieveExistingTransaction(txId).catch(() => null)
if (!existing) {
  // start fresh instead of resuming
  await orchestrator.run(...)
} else {
  await orchestrator.resume(existing)
}
Defensive patterns

Strategy: try-catch

Try / catch

try { await orchestrator.resume(txId) } catch (e) { if (e.type === 'not_found' && /could not be found/.test(e.message)) { await orchestrator.run(/* fresh */) } else throw e }

Prevention

When it happens

Trigger: Calling resume/cancel/skip with a transactionId that does not exist in the configured workflow storage, or after the execution record expired/was purged, or when the storage is empty (wrong Redis DB / different storage configured than the one that ran the workflow).

Common situations: Restarting the app with a fresh/in-memory workflow storage and trying to resume pre-restart transactions; pointing at a different Redis/database than where the workflow ran; typo'd or truncated transactionId (URL decoding issues).

Related errors


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