{"record":{"id":"09254b97f0c09c85","repo":"medusajs/medusa","slug":"key-key-already-exists-in-app-metadata","errorCode":null,"errorMessage":"Key ${key} already exists in app metadata","messagePattern":"Key (.+?) already exists in app metadata","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/core-flows/src/auth/steps/set-auth-app-metadata.ts","lineNumber":57,"sourceCode":" *   authIdentityId: \"au_1234\",\n *   actorType: \"user\", // or `customer`, or custom type\n *   value: null\n * })\n * ```\n */\nexport const setAuthAppMetadataStep = createStep(\n  setAuthAppMetadataStepId,\n  async (data: SetAuthAppMetadataStepInput, { container }) => {\n    const service = container.resolve<IAuthModuleService>(Modules.AUTH)\n\n    const key = `${data.actorType}_id`\n    const authIdentity = await service.retrieveAuthIdentity(data.authIdentityId)\n\n    const appMetadata = authIdentity.app_metadata || {}\n\n    // If the value is null, we are deleting the association with an actor\n    if (isDefined(appMetadata[key]) && data.value !== null) {\n      throw new Error(`Key ${key} already exists in app metadata`)\n    }\n\n    const oldValue = appMetadata[key]\n    appMetadata[key] = data.value\n\n    await service.updateAuthIdentities({\n      id: authIdentity.id,\n      app_metadata: appMetadata,\n    })\n\n    return new StepResponse(authIdentity, {\n      id: authIdentity.id,\n      key: key,\n      value: data.value,\n      oldValue,\n    })\n  },\n  async (idAndKeyAndValue, { container }) => {","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/core/core-flows/src/auth/steps/set-auth-app-metadata.ts#L39-L75","documentation":"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).","triggerScenarios":"Cancelling a transactionId that already finished, was cancelled, expired from Redis, or belongs to a different workflow id.","commonSituations":"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.","solutions":["Treat this error as idempotent success in handlers (transaction already gone)","Double-check transactionId and workflowId pairing before cancelling","Catch and log-and-ignore when the goal is merely ensuring the transaction is stopped"],"exampleFix":"// before\nawait engine.cancel(wfId, txId)\n// after\ntry { await engine.cancel(wfId, txId) } catch (e) { if (e.message === 'Transaction not found') return { idempotent: true } throw e }","handlingStrategy":"try-catch","validationCode":"const tx = await engine.getRunningTransaction(workflowId, transactionId).catch(() => null)\nif (!tx) return { alreadyGone: true }\nawait engine.cancel(workflowId, transactionId)","typeGuard":null,"tryCatchPattern":"try { await engine.cancel(wf, tx) } catch (e) { if (e.message === 'Transaction not found') return { idempotent: true }; throw e }","preventionTips":["Design cancel handlers to be idempotent against vanished transactions","Check transaction state before cancelling to avoid races"],"tags":["workflow-engine","redis","cancel","not-found"],"backgroundTag":"transaction-not-found","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}