Skip to content

Commit 4c0e286

Browse files
authored
Fix evented wflow nested loop (#16312)
## Description <!-- Provide a brief description of the changes in this PR --> Fix evented wflow nested loop ## Related Issue(s) <!-- Link to the issue(s) this PR addresses, using hashtag notation: Fixes #123 --> ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [x] Test update ## Checklist - [ ] I have made corresponding changes to the documentation (if applicable) - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have addressed all Coderabbit comments on this PR <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## ELI5 (Explain Like I'm 5) This PR fixes a bug where a workflow run started inside a loop (a "nested workflow") didn't let its parent loop resume or finish correctly. It detects when a finished child run belongs to a loop step in the parent and routes the completion into the loop handler so the parent can continue; tests ensure nested do-until and do-while loops work. ## Changes Made ### Bug Fix: Nested Loop Handling in Evented Workflows - processWorkflowEnd now inspects parentWorkflow.stepGraph[parentWorkflow.executionPath[0]] and, if that step is a loop, calls processWorkflowLoop with the parent workflow's richer execution metadata so the parent loop resumes/handles the nested run correctly. If the parent step is not a loop it falls back to publishing a workflow.step.end for the parent (including parentContext and nestedRunId). - Added cleanupRun to remove abort controllers, parent-child relationship entries, and runFormats when a workflow completes; cancelRunAndChildren recursively aborts child runs. - parentChildRelationships map added to track child runId → parent runId for nested workflows and used during cancellation/cleanup. ### Type/API Updates - Expanded ParentWorkflow public type (packages/core/src/workflows/evented/workflow-event-processor/index.ts) to include execution/resume fields required by nested handling: - Added: stepGraph: StepFlowEntry[], activeSteps: Record<string, boolean>, resumeSteps: string[], resumeData: any, input: any - Added optional: parentContext?: { workflowId: string; input: any } - Retained existing fields (workflowId, runId, executionPath, resume, stepResults, stepId, parentWorkflow?) ### Tests - workflows/_test-utils/src/domains/loops.ts: expanded test utilities to add nested loop coverage: - New nested dountil and nested dowhile workflow registrations (inner increment workflows used as loop bodies) and corresponding mocks. - Tests exercise nested-until and nested-while scenarios and include other loop cases (immediate-exit, non-nested loops). ### Release/Packaging - .changeset/rare-bikes-wear.md: new changeset marking a patch for @mastra/core describing "Fix nested loops in evented workflow". ## Files Touched (high level) - packages/core/src/workflows/evented/workflow-event-processor/index.ts — core logic: processWorkflowEnd branching, ParentWorkflow type expansion, abort controller & parent-child tracking and cleanup. - workflows/_test-utils/src/domains/loops.ts — added nested loop workflow definitions and test mocks. - .changeset/rare-bikes-wear.md — changeset entry. ## Impact - Fixes nested-loop completion/resume behavior for evented workflows and adds tests to prevent regressions. - Low-risk patch scope but touches core event-processing logic; reviewers should focus on correctness of processWorkflowEnd → processWorkflowLoop interaction, cleanup/cancellation semantics, and the ParentWorkflow shape used across event payloads. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent c6eb39e commit 4c0e286

3 files changed

Lines changed: 291 additions & 21 deletions

File tree

.changeset/rare-bikes-wear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mastra/core': patch
3+
---
4+
5+
Fix nested loops in evented workflow

packages/core/src/workflows/evented/workflow-event-processor/index.ts

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ export type ParentWorkflow = {
5959
stepResults: Record<string, StepResult<any, any, any, any>>;
6060
parentWorkflow?: ParentWorkflow;
6161
stepId: string;
62+
stepGraph: StepFlowEntry[];
63+
activeSteps: Record<string, boolean>;
64+
resumeSteps: string[];
65+
resumeData: any;
66+
input: any;
67+
parentContext?: {
68+
workflowId: string;
69+
input: any;
70+
};
6271
};
6372

6473
export class WorkflowEventProcessor extends EventProcessor {
@@ -361,27 +370,55 @@ export class WorkflowEventProcessor extends EventProcessor {
361370

362371
// handle nested workflow
363372
if (parentWorkflow) {
364-
await this.mastra.pubsub.publish('workflows', {
365-
type: 'workflow.step.end',
366-
runId: parentWorkflow.runId, // Use parent's runId for event routing
367-
data: {
368-
workflowId: parentWorkflow.workflowId,
369-
runId: parentWorkflow.runId,
370-
executionPath: parentWorkflow.executionPath,
371-
resumeSteps,
372-
stepResults: parentWorkflow.stepResults,
373-
prevResult,
374-
resumeData,
375-
activeSteps,
376-
parentWorkflow: parentWorkflow.parentWorkflow,
377-
parentContext: parentWorkflow,
378-
requestContext,
379-
timeTravel,
380-
perStep,
381-
state: finalState,
382-
nestedRunId: runId, // Pass nested workflow's runId for step retrieval
383-
},
384-
});
373+
// get the step from the parent workflow and process it if it's a loop
374+
const step = parentWorkflow.stepGraph[parentWorkflow.executionPath[0]!];
375+
if (step?.type === 'loop') {
376+
// pick workflow information from parentWorkflow as the workflow end being processed here is actually a step in the parentWorkflow
377+
await processWorkflowLoop(
378+
{
379+
workflow: parentWorkflow as unknown as Workflow,
380+
workflowId: parentWorkflow.workflowId,
381+
prevResult,
382+
runId: parentWorkflow.runId,
383+
executionPath: parentWorkflow.executionPath,
384+
stepResults: parentWorkflow.stepResults,
385+
activeSteps: parentWorkflow.activeSteps,
386+
resumeSteps: parentWorkflow.resumeSteps,
387+
resumeData: parentWorkflow.resumeData,
388+
parentWorkflow: parentWorkflow.parentWorkflow,
389+
requestContext,
390+
retryCount: 0,
391+
},
392+
{
393+
pubsub: this.mastra.pubsub,
394+
stepExecutor: this.stepExecutor,
395+
step,
396+
stepResult: prevResult,
397+
},
398+
);
399+
} else {
400+
await this.mastra.pubsub.publish('workflows', {
401+
type: 'workflow.step.end',
402+
runId: parentWorkflow.runId, // Use parent's runId for event routing
403+
data: {
404+
workflowId: parentWorkflow.workflowId,
405+
runId: parentWorkflow.runId,
406+
executionPath: parentWorkflow.executionPath,
407+
resumeSteps,
408+
stepResults: parentWorkflow.stepResults,
409+
prevResult,
410+
resumeData,
411+
activeSteps,
412+
parentWorkflow: parentWorkflow.parentWorkflow,
413+
parentContext: parentWorkflow,
414+
requestContext,
415+
timeTravel,
416+
perStep,
417+
state: finalState,
418+
nestedRunId: runId, // Pass nested workflow's runId for step retrieval
419+
},
420+
});
421+
}
385422
}
386423

387424
await this.mastra.pubsub.publish('workflows-finish', {
@@ -846,11 +883,14 @@ export class WorkflowEventProcessor extends EventProcessor {
846883
stepId: step.step.id,
847884
workflowId,
848885
runId,
886+
stepGraph,
849887
executionPath,
850888
resumeSteps,
851889
stepResults,
852890
input: prevResult,
853891
parentWorkflow,
892+
activeSteps,
893+
resumeData,
854894
},
855895
executionPath: nestedExecutionPath as any,
856896
runId: nestedRunId,
@@ -909,11 +949,14 @@ export class WorkflowEventProcessor extends EventProcessor {
909949
stepId: step.step.id,
910950
workflowId,
911951
runId,
952+
stepGraph,
912953
executionPath,
913954
resumeSteps,
914955
stepResults,
915956
input: prevResult,
916957
parentWorkflow,
958+
activeSteps,
959+
resumeData,
917960
},
918961
executionPath: snapshot?.suspendedPaths?.[nestedSteps[0]!] as any,
919962
runId: nestedRunId,
@@ -961,12 +1004,15 @@ export class WorkflowEventProcessor extends EventProcessor {
9611004
stepId: step.step.id,
9621005
workflowId,
9631006
runId,
1007+
stepGraph,
9641008
executionPath,
9651009
resumeSteps,
9661010
stepResults,
9671011
timeTravel,
9681012
input: prevResult,
9691013
parentWorkflow,
1014+
activeSteps,
1015+
resumeData,
9701016
},
9711017
executionPath: timeTravelParams.executionPath,
9721018
runId: randomUUID(),
@@ -990,12 +1036,15 @@ export class WorkflowEventProcessor extends EventProcessor {
9901036
parentWorkflow: {
9911037
stepId: step.step.id,
9921038
workflowId,
1039+
stepGraph,
9931040
runId,
9941041
executionPath,
9951042
resumeSteps,
9961043
stepResults,
9971044
input: prevResult,
9981045
parentWorkflow,
1046+
activeSteps,
1047+
resumeData,
9991048
},
10001049
executionPath: [0],
10011050
runId: randomUUID(),

0 commit comments

Comments
 (0)