medusajs/medusa · error · Error

Key ${key} already exists in app metadata

Error message

Key ${key} already exists in app metadata

What it means

After resolving the flow, the Redis engine's cancel() fetches the running transaction. If no transaction with that transactionId exists for the workflow, it throws 'Transaction not found' (a transaction that already completed or never existed falls into this path when the graceful 'exists:false' branch doesn't apply).

Source

Thrown at packages/core/core-flows/src/auth/steps/set-auth-app-metadata.ts:57

 *   authIdentityId: "au_1234",
 *   actorType: "user", // or `customer`, or custom type
 *   value: null
 * })
 * ```
 */
export const setAuthAppMetadataStep = createStep(
  setAuthAppMetadataStepId,
  async (data: SetAuthAppMetadataStepInput, { container }) => {
    const service = container.resolve<IAuthModuleService>(Modules.AUTH)

    const key = `${data.actorType}_id`
    const authIdentity = await service.retrieveAuthIdentity(data.authIdentityId)

    const appMetadata = authIdentity.app_metadata || {}

    // If the value is null, we are deleting the association with an actor
    if (isDefined(appMetadata[key]) && data.value !== null) {
      throw new Error(`Key ${key} already exists in app metadata`)
    }

    const oldValue = appMetadata[key]
    appMetadata[key] = data.value

    await service.updateAuthIdentities({
      id: authIdentity.id,
      app_metadata: appMetadata,
    })

    return new StepResponse(authIdentity, {
      id: authIdentity.id,
      key: key,
      value: data.value,
      oldValue,
    })
  },
  async (idAndKeyAndValue, { container }) => {

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Treat this error as idempotent success in handlers (transaction already gone)
  2. Double-check transactionId and workflowId pairing before cancelling
  3. Catch and log-and-ignore when the goal is merely ensuring the transaction is stopped

Example fix

// before
await engine.cancel(wfId, txId)
// after
try { await engine.cancel(wfId, txId) } catch (e) { if (e.message === 'Transaction not found') return { idempotent: true } throw e }
Defensive patterns

Strategy: try-catch

Validate before calling

const tx = await engine.getRunningTransaction(workflowId, transactionId).catch(() => null)
if (!tx) return { alreadyGone: true }
await engine.cancel(workflowId, transactionId)

Try / catch

try { await engine.cancel(wf, tx) } catch (e) { if (e.message === 'Transaction not found') return { idempotent: true }; throw e }

Prevention

When it happens

Trigger: Cancelling a transactionId that already finished, was cancelled, expired from Redis, or belongs to a different workflow id.

Common situations: Race conditions where the workflow completes while a cancel request is in flight; Redis TTL/eviction removing transaction state; replaying cancel requests from a queue.

Related errors


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