Skip to content

Commit e5e8082

Browse files
authored
fix(workflow-block): improvements to pulsing effect, active execution state, and running workflow blocks in parallel (#927)
* fix: same child workflow executing in parallel with workflow block * fixed run button prematurely showing completion before child workflows completed * prevent child worklfows from touching the activeBlocks & layer logic in the parent executor * surface child workflow errors to main workfow * ack PR comments
1 parent 8a08afd commit e5e8082

File tree

4 files changed

+440
-78
lines changed

4 files changed

+440
-78
lines changed

apps/sim/executor/handlers/workflow/workflow-handler.test.ts

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,13 @@ describe('WorkflowBlockHandler', () => {
107107

108108
// Simulate a cycle by adding the execution to the stack
109109

110-
;(WorkflowBlockHandler as any).executionStack.add('parent-workflow-id_sub_child-workflow-id')
111-
112-
const result = await handler.execute(mockBlock, inputs, mockContext)
110+
;(WorkflowBlockHandler as any).executionStack.add(
111+
'parent-workflow-id_sub_child-workflow-id_workflow-block-1'
112+
)
113113

114-
expect(result).toEqual({
115-
success: false,
116-
error: 'Cyclic workflow dependency detected: parent-workflow-id_sub_child-workflow-id',
117-
childWorkflowName: 'child-workflow-id',
118-
})
114+
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
115+
'Error in child workflow "child-workflow-id": Cyclic workflow dependency detected: parent-workflow-id_sub_child-workflow-id_workflow-block-1'
116+
)
119117
})
120118

121119
it('should enforce maximum depth limit', async () => {
@@ -128,13 +126,9 @@ describe('WorkflowBlockHandler', () => {
128126
'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11',
129127
}
130128

131-
const result = await handler.execute(mockBlock, inputs, deepContext)
132-
133-
expect(result).toEqual({
134-
success: false,
135-
error: 'Maximum workflow nesting depth of 10 exceeded',
136-
childWorkflowName: 'child-workflow-id',
137-
})
129+
await expect(handler.execute(mockBlock, inputs, deepContext)).rejects.toThrow(
130+
'Error in child workflow "child-workflow-id": Maximum workflow nesting depth of 10 exceeded'
131+
)
138132
})
139133

140134
it('should handle child workflow not found', async () => {
@@ -146,27 +140,19 @@ describe('WorkflowBlockHandler', () => {
146140
statusText: 'Not Found',
147141
})
148142

149-
const result = await handler.execute(mockBlock, inputs, mockContext)
150-
151-
expect(result).toEqual({
152-
success: false,
153-
error: 'Child workflow non-existent-workflow not found',
154-
childWorkflowName: 'non-existent-workflow',
155-
})
143+
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
144+
'Error in child workflow "non-existent-workflow": Child workflow non-existent-workflow not found'
145+
)
156146
})
157147

158148
it('should handle fetch errors gracefully', async () => {
159149
const inputs = { workflowId: 'child-workflow-id' }
160150

161151
mockFetch.mockRejectedValueOnce(new Error('Network error'))
162152

163-
const result = await handler.execute(mockBlock, inputs, mockContext)
164-
165-
expect(result).toEqual({
166-
success: false,
167-
error: 'Child workflow child-workflow-id not found',
168-
childWorkflowName: 'child-workflow-id',
169-
})
153+
await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow(
154+
'Error in child workflow "child-workflow-id": Child workflow child-workflow-id not found'
155+
)
170156
})
171157
})
172158

apps/sim/executor/handlers/workflow/workflow-handler.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export class WorkflowBlockHandler implements BlockHandler {
4646
throw new Error(`Maximum workflow nesting depth of ${MAX_WORKFLOW_DEPTH} exceeded`)
4747
}
4848

49-
// Check for cycles
50-
const executionId = `${context.workflowId}_sub_${workflowId}`
49+
// Check for cycles - include block ID to differentiate parallel executions
50+
const executionId = `${context.workflowId}_sub_${workflowId}_${block.id}`
5151
if (WorkflowBlockHandler.executionStack.has(executionId)) {
5252
throw new Error(`Cyclic workflow dependency detected: ${executionId}`)
5353
}
@@ -90,6 +90,9 @@ export class WorkflowBlockHandler implements BlockHandler {
9090
workflowInput: childWorkflowInput,
9191
envVarValues: context.environmentVariables,
9292
workflowVariables: childWorkflow.variables || {},
93+
contextExtensions: {
94+
isChildExecution: true, // Prevent child executor from managing global state
95+
},
9396
})
9497

9598
const startTime = performance.now()
@@ -105,24 +108,41 @@ export class WorkflowBlockHandler implements BlockHandler {
105108
logger.info(`Child workflow ${childWorkflowName} completed in ${Math.round(duration)}ms`)
106109

107110
// Map child workflow output to parent block output
108-
return this.mapChildOutputToParent(result, workflowId, childWorkflowName, duration)
111+
const mappedResult = this.mapChildOutputToParent(
112+
result,
113+
workflowId,
114+
childWorkflowName,
115+
duration
116+
)
117+
118+
// If the child workflow failed, throw an error to trigger proper error handling in the parent
119+
if ((mappedResult as any).success === false) {
120+
const childError = (mappedResult as any).error || 'Unknown error'
121+
throw new Error(`Error in child workflow "${childWorkflowName}": ${childError}`)
122+
}
123+
124+
return mappedResult
109125
} catch (error: any) {
110126
logger.error(`Error executing child workflow ${workflowId}:`, error)
111127

112128
// Clean up execution stack in case of error
113-
const executionId = `${context.workflowId}_sub_${workflowId}`
129+
const executionId = `${context.workflowId}_sub_${workflowId}_${block.id}`
114130
WorkflowBlockHandler.executionStack.delete(executionId)
115131

116132
// Get workflow name for error reporting
117133
const { workflows } = useWorkflowRegistry.getState()
118134
const workflowMetadata = workflows[workflowId]
119135
const childWorkflowName = workflowMetadata?.name || workflowId
120136

121-
return {
122-
success: false,
123-
error: error.message || 'Child workflow execution failed',
124-
childWorkflowName: childWorkflowName,
125-
} as Record<string, any>
137+
// Enhance error message with child workflow context
138+
const originalError = error.message || 'Unknown error'
139+
140+
// Check if error message already has child workflow context to avoid duplication
141+
if (originalError.startsWith('Error in child workflow')) {
142+
throw error // Re-throw as-is to avoid duplication
143+
}
144+
145+
throw new Error(`Error in child workflow "${childWorkflowName}": ${originalError}`)
126146
}
127147
}
128148

0 commit comments

Comments
 (0)