fix(workflow-sdk): Async/nested runAsStep propagation (#12675)

FIXES CLO-524

**What**
Add hidden stepDefinition object as part of the step argument and ensure the runAsStep handlers rely on the latest definition when config is being used on the returned step in order to ensure async configuration propagation and nested configuration
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2025-06-10 07:23:12 +00:00
1 parent b456044060
commit 1a78476608
10 files changed
+202 -15

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/workflow-engine-inmemory": patch
"@medusajs/workflow-engine-redis": patch
"@medusajs/workflows-sdk": patch
---
fix(workflow-sdk): Async propagation
@@ -9,12 +9,143 @@ import { transform } from "../transform"
import { WorkflowData } from "../type" import { WorkflowData } from "../type"
import { when } from "../when" import { when } from "../when"
import { createHook } from "../create-hook" import { createHook } from "../create-hook"
import { TransactionStepsDefinition } from "@medusajs/orchestration"
let count = 1 let count = 1
const getNewWorkflowId = () => `workflow-${count++}` const getNewWorkflowId = () => `workflow-${count++}`
describe("Workflow composer", () => { describe("Workflow composer", () => {
describe("running sub workflows", () => { describe("when running workflows as sub-workflows", () => {
describe("handling of async and nested workflow configurations", () => {
it("should set the runAsStep as nested and async when parent workflow is async", async () => {
const subworkflowStep1 = createStep("step1", async (_, context) => {
return new StepResponse({ result: "sub workflow step1" })
})
const subWorkflowId = getNewWorkflowId()
const subWorkflow = createWorkflow(
subWorkflowId,
function (input: WorkflowData<string>) {
subworkflowStep1()
return new WorkflowResponse(void 0)
}
)
const step1 = createStep(
{ name: "step1", async: true },
async (_, context) => {
return new StepResponse({ result: "step1" })
}
)
const workflowId = getNewWorkflowId()
const workflow = createWorkflow(workflowId, function () {
step1()
const subWorkflowRes = subWorkflow.runAsStep({
input: "hi from outside",
})
return new WorkflowResponse(subWorkflowRes)
})
expect(workflow().getFlow().async).toBe(true)
expect(subWorkflow().getFlow().async).toBeUndefined()
const runAsStep = workflow().getFlow()
.next! as TransactionStepsDefinition
expect(runAsStep.action).toBe(`${subWorkflowId}-as-step`)
expect(runAsStep.async).toBe(true)
expect(runAsStep.nested).toBe(true)
})
it("should set the runAsStep as nested and async when parent workflow is sync but sub workflow is async", async () => {
const subworkflowStep1 = createStep(
{ name: "step1", async: true },
async (_, context) => {
return new StepResponse({ result: "sub workflow step1" })
}
)
const subWorkflowId = getNewWorkflowId()
const subWorkflow = createWorkflow(
subWorkflowId,
function (input: WorkflowData<string>) {
subworkflowStep1()
return new WorkflowResponse(void 0)
}
)
const step1 = createStep("step1", async (_, context) => {
return new StepResponse({ result: "step1" })
})
const workflowId = getNewWorkflowId()
const workflow = createWorkflow(workflowId, function () {
step1()
const subWorkflowRes = subWorkflow.runAsStep({
input: "hi from outside",
})
return new WorkflowResponse(subWorkflowRes)
})
expect(workflow().getFlow().async).toBeUndefined()
expect(subWorkflow().getFlow().async).toBe(true)
const runAsStep = workflow().getFlow()
.next! as TransactionStepsDefinition
expect(runAsStep.action).toBe(`${subWorkflowId}-as-step`)
expect(runAsStep.async).toBe(true)
expect(runAsStep.nested).toBe(true)
})
it("should set the runAsStep as nested and async when parent workflow is sync as well as sub workflow but the step is configured as async", async () => {
const subworkflowStep1 = createStep("step1", async (_, context) => {
return new StepResponse({ result: "sub workflow step1" })
})
const subWorkflowId = getNewWorkflowId()
const subWorkflow = createWorkflow(
subWorkflowId,
function (input: WorkflowData<string>) {
subworkflowStep1()
return new WorkflowResponse({})
}
)
const step1 = createStep("step1", async (_, context) => {
return new StepResponse({ result: "step1" })
})
const workflowId = getNewWorkflowId()
const workflow = createWorkflow(workflowId, function () {
step1()
const subWorkflowRes = subWorkflow
.runAsStep({
input: "hi from outside",
})
.config({ async: true })
return new WorkflowResponse(subWorkflowRes)
})
expect(workflow().getFlow().async).toBeUndefined()
expect(subWorkflow().getFlow().async).toBeUndefined()
const runAsStep = workflow().getFlow()
.next! as TransactionStepsDefinition
expect(runAsStep.action).toBe(`${subWorkflowId}-as-step`)
expect(runAsStep.async).toBe(true)
expect(runAsStep.nested).toBe(true)
})
})
it("should succeed", async function () { it("should succeed", async function () {
const step1 = createStep("step1", async (_, context) => { const step1 = createStep("step1", async (_, context) => {
return new StepResponse({ result: "step1" }) return new StepResponse({ result: "step1" })
@@ -4,7 +4,7 @@ import {
WorkflowStepHandler, WorkflowStepHandler,
WorkflowStepHandlerArguments, WorkflowStepHandlerArguments,
} from "@medusajs/orchestration" } from "@medusajs/orchestration"
import { isString, OrchestrationUtils } from "@medusajs/utils" import { isDefined, isString, OrchestrationUtils } from "@medusajs/utils"
import { ulid } from "ulid" import { ulid } from "ulid"
import { resolveValue, StepResponse } from "./helpers" import { resolveValue, StepResponse } from "./helpers"
import { createStepHandler } from "./helpers/create-step-handler" import { createStepHandler } from "./helpers/create-step-handler"
@@ -173,6 +173,10 @@ export function applyStep<
...localConfig, ...localConfig,
} }
if (isDefined(newConfig.nested)) {
newConfig.nested ||= newConfig.async
}
delete localConfig.name delete localConfig.name
const handler = createStepHandler.bind(this)({ const handler = createStepHandler.bind(this)({
@@ -198,6 +198,7 @@ export function createWorkflow<TData, TResult, THooks extends any[]>(
}, },
async (stepInput: TData, stepContext) => { async (stepInput: TData, stepContext) => {
const { container, ...sharedContext } = stepContext const { container, ...sharedContext } = stepContext
const isAsync = stepContext[" stepDefinition"]?.async
const workflowEngine = container.resolve(Modules.WORKFLOW_ENGINE, { const workflowEngine = container.resolve(Modules.WORKFLOW_ENGINE, {
allowUnregistered: true, allowUnregistered: true,
@@ -212,7 +213,7 @@ export function createWorkflow<TData, TResult, THooks extends any[]>(
} }
let transaction let transaction
if (workflowEngine && runAsAsync) { if (workflowEngine && isAsync) {
transaction = await workflowEngine.run(name, { transaction = await workflowEngine.run(name, {
input: stepInput as any, input: stepInput as any,
context: executionContext, context: executionContext,
@@ -227,7 +228,7 @@ export function createWorkflow<TData, TResult, THooks extends any[]>(
return new StepResponse( return new StepResponse(
transaction.result, transaction.result,
runAsAsync ? stepContext.transactionId : transaction isAsync ? stepContext.transactionId : transaction
) )
}, },
async (transaction, stepContext) => { async (transaction, stepContext) => {
@@ -237,6 +238,7 @@ export function createWorkflow<TData, TResult, THooks extends any[]>(
} }
const { container, ...sharedContext } = stepContext const { container, ...sharedContext } = stepContext
const isAsync = stepContext[" stepDefinition"]?.async
const workflowEngine = container.resolve(Modules.WORKFLOW_ENGINE, { const workflowEngine = container.resolve(Modules.WORKFLOW_ENGINE, {
allowUnregistered: true, allowUnregistered: true,
@@ -252,7 +254,7 @@ export function createWorkflow<TData, TResult, THooks extends any[]>(
const transactionId = step.__step__ + "-" + stepContext.transactionId const transactionId = step.__step__ + "-" + stepContext.transactionId
if (workflowEngine && runAsAsync) { if (workflowEngine && isAsync) {
await workflowEngine.cancel(name, { await workflowEngine.cancel(name, {
transactionId: transactionId, transactionId: transactionId,
context: executionContext, context: executionContext,
@@ -21,7 +21,10 @@ function buildStepContext({
stepArguments.context!.idempotencyKey = idempotencyKey stepArguments.context!.idempotencyKey = idempotencyKey
const flowMetadata = stepArguments.transaction.getFlow()?.metadata const flow = stepArguments.transaction.getFlow()
const flowMetadata = flow?.metadata
const stepDefinition = stepArguments.step.definition
const executionContext: StepExecutionContext = { const executionContext: StepExecutionContext = {
workflowId: metadata.model_id, workflowId: metadata.model_id,
stepName: metadata.action, stepName: metadata.action,
@@ -36,6 +39,7 @@ function buildStepContext({
preventReleaseEvents: flowMetadata?.preventReleaseEvents ?? false, preventReleaseEvents: flowMetadata?.preventReleaseEvents ?? false,
transactionId: stepArguments.context!.transactionId, transactionId: stepArguments.context!.transactionId,
context: stepArguments.context!, context: stepArguments.context!,
" stepDefinition": stepDefinition,
" getStepResult"( " getStepResult"(
stepId: string, stepId: string,
action: "invoke" | "compensate" = "invoke" action: "invoke" | "compensate" = "invoke"
@@ -198,6 +198,11 @@ export interface StepExecutionContext {
* Adding a space hides the method from the autocomplete * Adding a space hides the method from the autocomplete
*/ */
" getStepResult"(stepId: string, action?: "invoke" | "compensate"): any " getStepResult"(stepId: string, action?: "invoke" | "compensate"): any
/**
* Get access to the definition of the step.
*/
" stepDefinition": TransactionStepsDefinition
} }
export type WorkflowTransactionContext = StepExecutionContext & export type WorkflowTransactionContext = StepExecutionContext &
@@ -208,10 +208,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return { acknowledgement, ...ret } return { acknowledgement, ...ret }
} }
@@ -317,10 +321,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return { acknowledgement, ...ret } return { acknowledgement, ...ret }
} }
@@ -411,10 +419,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return ret return ret
} }
@@ -477,10 +489,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return ret return ret
} }
@@ -1,7 +1,6 @@
import { import {
createStep, createStep,
createWorkflow, createWorkflow,
parallelize,
StepResponse, StepResponse,
WorkflowResponse, WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
@@ -495,6 +495,9 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
transactionId: "transaction_1", transactionId: "transaction_1",
}, },
stepResponse: { uhuuuu: "yeaah!" }, stepResponse: { uhuuuu: "yeaah!" },
options: {
throwOnError: false,
},
}) })
;({ data: executionsList } = await query.graph({ ;({ data: executionsList } = await query.graph({
entity: "workflow_executions", entity: "workflow_executions",
@@ -268,10 +268,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return { acknowledgement, ...ret } return { acknowledgement, ...ret }
} }
@@ -373,10 +377,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return { acknowledgement, ...ret } return { acknowledgement, ...ret }
} }
@@ -467,10 +475,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return ret return ret
} }
@@ -534,10 +546,14 @@ export class WorkflowOrchestratorService {
await this.triggerParentStep(ret.transaction, result) await this.triggerParentStep(ret.transaction, result)
} }
if (throwOnError && ret.thrownError) { if (throwOnError && (ret.thrownError || ret.errors?.length)) {
if (ret.thrownError) {
throw ret.thrownError throw ret.thrownError
} }
throw ret.errors[0].error
}
return ret return ret
} }