{"record":{"id":"ac9248637bc43141","repo":"medusajs/medusa","slug":"step-obj-action-is-already-defined-in-workflow","errorCode":null,"errorMessage":"Step ${obj.action} is already defined in workflow.","messagePattern":"Step (.+?) is already defined in workflow\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/core/orchestration/src/transaction/transaction-orchestrator.ts","lineNumber":1620,"sourceCode":"\n    const actionNames = new Set<string>()\n    const queue: any[] = [\n      { obj: flow, level: [TransactionOrchestrator.ROOT_STEP] },\n    ]\n\n    const features = {\n      hasAsyncSteps: false,\n      hasStepTimeouts: false,\n      hasRetriesTimeout: false,\n      hasNestedTransactions: false,\n    }\n\n    while (queue.length > 0) {\n      const { obj, level } = queue.shift()\n\n      if (obj.action) {\n        if (actionNames.has(obj.action)) {\n          throw new Error(`Step ${obj.action} is already defined in workflow.`)\n        }\n\n        actionNames.add(obj.action)\n        level.push(obj.action)\n        const id = level.join(\".\")\n        const parent = level.slice(0, level.length - 1).join(\".\")\n\n        if (!existingSteps || parent === TransactionOrchestrator.ROOT_STEP) {\n          states[parent].next?.push(id)\n        }\n\n        const definitionCopy = { ...obj } as TransactionStepsDefinition\n        delete definitionCopy.next\n\n        const isAsync = !!definitionCopy.async\n        const hasRetryInterval = !!(\n          definitionCopy.retryInterval || definitionCopy.retryIntervalAwaiting\n        )","sourceCodeStart":1602,"sourceCodeEnd":1638,"githubUrl":"https://github.com/medusajs/medusa/blob/5e06e544a296b9033f20f71f11c559f81a0e5739/packages/core/orchestration/src/transaction/transaction-orchestrator.ts#L1602-L1638","documentation":"While flattening the workflow graph into executable step ids, the orchestrator records every action alias it visits. Encountering the same alias twice means two steps share one action name, which would break step addressing/idempotency keys — so it throws immediately at workflow definition time.","triggerScenarios":"Defining a workflow where the same step alias is inserted twice (e.g. appending a step object that is reused in two places, or two createStep calls that got the same generated/assigned alias via .invoke config reuse), or composing sub-workflows that both expose a step with the same name.","commonSituations":"Reusing a step instance variable in two parts of a flow; copy-pasting a step definition and forgetting to change its alias; merging parallel branches that contain identically named steps.","solutions":["Give every step a unique alias/name within the workflow (pass distinct names to createStep or rename on compose)","Do not reuse the same step node object in multiple positions of the graph; create separate instances","When composing parallel branches, prefix sub-workflow step names (namespacing)"],"exampleFix":"// before\nconst sameStep = createStep('charge', ...)\ncreateWorkflow('w', () => { sameStep(); sameStep() })\n\n// after\nconst chargeA = createStep('charge-a', ...)\nconst chargeB = createStep('charge-b', ...)\ncreateWorkflow('w', () => { chargeA(); chargeB() })","handlingStrategy":"validation","validationCode":"const names = new Set()\nfor (const s of mySteps) {\n  if (names.has(s.__action__)) throw new Error(`duplicate step ${s.__action__}`)\n  names.add(s.__action__)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never reuse a single step node in two graph positions; instantiate per position","Namespace step aliases when embedding sub-workflows in parallel branches"],"tags":["orchestration","workflow-definition","duplicate-step"],"backgroundTag":"duplicate-workflow-step","analyzedSha":"5e06e544a296b9033f20f71f11c559f81a0e5739","analyzedAt":"2026-08-27T07:24:39.599Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}