Skip to content

Commit 1781279

Browse files
committed
fix(terminal): prevent child block mixing across loop iterations for workflow blocks
1 parent e7ad3f7 commit 1781279

File tree

10 files changed

+72
-12
lines changed

10 files changed

+72
-12
lines changed

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
787787
}
788788
: {}
789789

790+
// Extract per-invocation instance ID and strip from user-visible output
791+
const childWorkflowInstanceId: string | undefined =
792+
callbackData.output?._childWorkflowInstanceId
793+
const instanceData = childWorkflowInstanceId ? { childWorkflowInstanceId } : {}
794+
790795
if (hasError) {
791796
logger.info(`[${requestId}] ✗ onBlockComplete (error) called:`, {
792797
blockId,
@@ -816,6 +821,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
816821
iterationContainerId: iterationContext.iterationContainerId,
817822
}),
818823
...childWorkflowData,
824+
...instanceData,
819825
},
820826
})
821827
} else {
@@ -824,6 +830,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
824830
blockName,
825831
blockType,
826832
})
833+
const { _childWorkflowInstanceId: _stripped, ...strippedOutput } =
834+
callbackData.output ?? {}
827835
sendEvent({
828836
type: 'block:completed',
829837
timestamp: new Date().toISOString(),
@@ -834,7 +842,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
834842
blockName,
835843
blockType,
836844
input: callbackData.input,
837-
output: callbackData.output,
845+
output: strippedOutput,
838846
durationMs: callbackData.executionTime || 0,
839847
startedAt: callbackData.startedAt,
840848
executionOrder: callbackData.executionOrder,
@@ -846,6 +854,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
846854
iterationContainerId: iterationContext.iterationContainerId,
847855
}),
848856
...childWorkflowData,
857+
...instanceData,
849858
},
850859
})
851860
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,25 @@ interface IterationGroup {
198198
* enabling correct tree construction for deeply-nested child workflows.
199199
*/
200200
function collectWorkflowDescendants(
201-
workflowBlockId: string,
201+
instanceKey: string,
202202
workflowChildGroups: Map<string, ConsoleEntry[]>,
203203
visited: Set<string> = new Set()
204204
): ConsoleEntry[] {
205-
if (visited.has(workflowBlockId)) return []
206-
visited.add(workflowBlockId)
207-
const direct = workflowChildGroups.get(workflowBlockId) ?? []
205+
if (visited.has(instanceKey)) return []
206+
visited.add(instanceKey)
207+
const direct = workflowChildGroups.get(instanceKey) ?? []
208208
const result = [...direct]
209209
for (const entry of direct) {
210210
if (isWorkflowBlockType(entry.blockType)) {
211-
result.push(...collectWorkflowDescendants(entry.blockId, workflowChildGroups, visited))
211+
// Use childWorkflowInstanceId when available (unique per-invocation) to correctly
212+
// separate children across loop iterations of the same workflow block.
213+
result.push(
214+
...collectWorkflowDescendants(
215+
entry.childWorkflowInstanceId ?? entry.blockId,
216+
workflowChildGroups,
217+
visited
218+
)
219+
)
212220
}
213221
}
214222
return result
@@ -387,11 +395,12 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
387395
// Block nodes within this iteration — workflow blocks get their full subtree
388396
const blockNodes: EntryNode[] = iterBlocks.map((block) => {
389397
if (isWorkflowBlockType(block.blockType)) {
390-
const allDescendants = collectWorkflowDescendants(block.blockId, workflowChildGroups)
398+
const instanceKey = block.childWorkflowInstanceId ?? block.blockId
399+
const allDescendants = collectWorkflowDescendants(instanceKey, workflowChildGroups)
391400
const rawChildren = allDescendants.map((c) => ({
392401
...c,
393402
childWorkflowBlockId:
394-
c.childWorkflowBlockId === block.blockId ? undefined : c.childWorkflowBlockId,
403+
c.childWorkflowBlockId === instanceKey ? undefined : c.childWorkflowBlockId,
395404
}))
396405
return {
397406
entry: block,
@@ -426,11 +435,12 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
426435

427436
for (const block of regularBlocks) {
428437
if (isWorkflowBlockType(block.blockType)) {
429-
const allDescendants = collectWorkflowDescendants(block.blockId, workflowChildGroups)
438+
const instanceKey = block.childWorkflowInstanceId ?? block.blockId
439+
const allDescendants = collectWorkflowDescendants(instanceKey, workflowChildGroups)
430440
const rawChildren = allDescendants.map((c) => ({
431441
...c,
432442
childWorkflowBlockId:
433-
c.childWorkflowBlockId === block.blockId ? undefined : c.childWorkflowBlockId,
443+
c.childWorkflowBlockId === instanceKey ? undefined : c.childWorkflowBlockId,
434444
}))
435445
const children = buildEntryTree(rawChildren)
436446
workflowNodes.push({ entry: block, children, nodeType: 'workflow' as const })

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ export function useWorkflowExecution() {
385385
iterationContainerId: data.iterationContainerId,
386386
childWorkflowBlockId: data.childWorkflowBlockId,
387387
childWorkflowName: data.childWorkflowName,
388+
childWorkflowInstanceId: data.childWorkflowInstanceId,
388389
})
389390
}
390391

@@ -410,6 +411,7 @@ export function useWorkflowExecution() {
410411
iterationContainerId: data.iterationContainerId,
411412
childWorkflowBlockId: data.childWorkflowBlockId,
412413
childWorkflowName: data.childWorkflowName,
414+
childWorkflowInstanceId: data.childWorkflowInstanceId,
413415
})
414416
}
415417

@@ -431,6 +433,7 @@ export function useWorkflowExecution() {
431433
iterationContainerId: data.iterationContainerId,
432434
childWorkflowBlockId: data.childWorkflowBlockId,
433435
childWorkflowName: data.childWorkflowName,
436+
childWorkflowInstanceId: data.childWorkflowInstanceId,
434437
},
435438
executionIdRef.current
436439
)
@@ -455,6 +458,7 @@ export function useWorkflowExecution() {
455458
iterationContainerId: data.iterationContainerId,
456459
childWorkflowBlockId: data.childWorkflowBlockId,
457460
childWorkflowName: data.childWorkflowName,
461+
childWorkflowInstanceId: data.childWorkflowInstanceId,
458462
},
459463
executionIdRef.current
460464
)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export async function executeWorkflowWithFullLogging(
175175
iterationContainerId: event.data.iterationContainerId,
176176
childWorkflowBlockId: event.data.childWorkflowBlockId,
177177
childWorkflowName: event.data.childWorkflowName,
178+
childWorkflowInstanceId: event.data.childWorkflowInstanceId,
178179
})
179180

180181
if (options.onBlockComplete) {
@@ -214,6 +215,7 @@ export async function executeWorkflowWithFullLogging(
214215
iterationContainerId: event.data.iterationContainerId,
215216
childWorkflowBlockId: event.data.childWorkflowBlockId,
216217
childWorkflowName: event.data.childWorkflowName,
218+
childWorkflowInstanceId: event.data.childWorkflowInstanceId,
217219
})
218220
break
219221
}

apps/sim/executor/errors/child-workflow-error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ interface ChildWorkflowErrorOptions {
77
childTraceSpans?: TraceSpan[]
88
executionResult?: ExecutionResult
99
childWorkflowSnapshotId?: string
10+
childWorkflowInstanceId?: string
1011
cause?: Error
1112
}
1213

@@ -18,6 +19,8 @@ export class ChildWorkflowError extends Error {
1819
readonly childWorkflowName: string
1920
readonly executionResult?: ExecutionResult
2021
readonly childWorkflowSnapshotId?: string
22+
/** Per-invocation unique ID used to correlate child block events with this workflow block. */
23+
readonly childWorkflowInstanceId?: string
2124

2225
constructor(options: ChildWorkflowErrorOptions) {
2326
super(options.message, { cause: options.cause })
@@ -26,6 +29,7 @@ export class ChildWorkflowError extends Error {
2629
this.childTraceSpans = options.childTraceSpans ?? []
2730
this.executionResult = options.executionResult
2831
this.childWorkflowSnapshotId = options.childWorkflowSnapshotId
32+
this.childWorkflowInstanceId = options.childWorkflowInstanceId
2933
}
3034

3135
static isChildWorkflowError(error: unknown): error is ChildWorkflowError {

apps/sim/executor/execution/block-executor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ export class BlockExecutor {
249249
if (error.childWorkflowSnapshotId) {
250250
errorOutput.childWorkflowSnapshotId = error.childWorkflowSnapshotId
251251
}
252+
if (error.childWorkflowInstanceId) {
253+
errorOutput._childWorkflowInstanceId = error.childWorkflowInstanceId
254+
}
252255
}
253256

254257
this.state.setBlockOutput(node.id, errorOutput, duration)

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export class WorkflowBlockHandler implements BlockHandler {
5858
const workflowMetadata = workflows[workflowId]
5959
let childWorkflowName = workflowMetadata?.name || workflowId
6060

61+
// Unique ID per invocation — used to correlate child block events with this specific
62+
// workflow block execution, preventing cross-iteration child mixing in loop contexts.
63+
const instanceId = crypto.randomUUID()
64+
6165
let childWorkflowSnapshotId: string | undefined
6266
try {
6367
const currentDepth = (ctx.workflowId?.split('_sub_').length || 1) - 1
@@ -135,7 +139,7 @@ export class WorkflowBlockHandler implements BlockHandler {
135139
onBlockComplete: ctx.onBlockComplete,
136140
onStream: ctx.onStream as ((streamingExecution: unknown) => Promise<void>) | undefined,
137141
childWorkflowContext: {
138-
parentBlockId: block.id,
142+
parentBlockId: instanceId,
139143
workflowName: childWorkflowName,
140144
workflowId,
141145
depth: childDepth,
@@ -162,6 +166,7 @@ export class WorkflowBlockHandler implements BlockHandler {
162166
workflowId,
163167
childWorkflowName,
164168
duration,
169+
instanceId,
165170
childTraceSpans,
166171
childWorkflowSnapshotId
167172
)
@@ -197,6 +202,7 @@ export class WorkflowBlockHandler implements BlockHandler {
197202
childTraceSpans,
198203
executionResult,
199204
childWorkflowSnapshotId,
205+
childWorkflowInstanceId: instanceId,
200206
cause: error instanceof Error ? error : undefined,
201207
})
202208
}
@@ -539,6 +545,7 @@ export class WorkflowBlockHandler implements BlockHandler {
539545
childWorkflowId: string,
540546
childWorkflowName: string,
541547
duration: number,
548+
instanceId: string,
542549
childTraceSpans?: WorkflowTraceSpan[],
543550
childWorkflowSnapshotId?: string
544551
): BlockOutput {
@@ -552,6 +559,7 @@ export class WorkflowBlockHandler implements BlockHandler {
552559
childWorkflowName,
553560
childTraceSpans: childTraceSpans || [],
554561
childWorkflowSnapshotId,
562+
childWorkflowInstanceId: instanceId,
555563
})
556564
}
557565

@@ -562,6 +570,7 @@ export class WorkflowBlockHandler implements BlockHandler {
562570
...(childWorkflowSnapshotId ? { childWorkflowSnapshotId } : {}),
563571
result,
564572
childTraceSpans: childTraceSpans || [],
573+
_childWorkflowInstanceId: instanceId,
565574
} as Record<string, any>
566575
}
567576
}

apps/sim/lib/workflows/executor/execution-events.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export interface BlockCompletedEvent extends BaseExecutionEvent {
109109
iterationContainerId?: string
110110
childWorkflowBlockId?: string
111111
childWorkflowName?: string
112+
/** Per-invocation unique ID for correlating child block events with this workflow block. */
113+
childWorkflowInstanceId?: string
112114
}
113115
}
114116

@@ -134,6 +136,8 @@ export interface BlockErrorEvent extends BaseExecutionEvent {
134136
iterationContainerId?: string
135137
childWorkflowBlockId?: string
136138
childWorkflowName?: string
139+
/** Per-invocation unique ID for correlating child block events with this workflow block. */
140+
childWorkflowInstanceId?: string
137141
}
138142
}
139143

@@ -287,6 +291,11 @@ export function createSSECallbacks(options: SSECallbackOptions) {
287291
}
288292
: {}
289293

294+
// Extract per-invocation instance ID and strip from user-visible output
295+
const childWorkflowInstanceId: string | undefined =
296+
callbackData.output?._childWorkflowInstanceId
297+
const instanceData = childWorkflowInstanceId ? { childWorkflowInstanceId } : {}
298+
290299
if (hasError) {
291300
sendEvent({
292301
type: 'block:error',
@@ -305,9 +314,11 @@ export function createSSECallbacks(options: SSECallbackOptions) {
305314
endedAt: callbackData.endedAt,
306315
...iterationData,
307316
...childWorkflowData,
317+
...instanceData,
308318
},
309319
})
310320
} else {
321+
const { _childWorkflowInstanceId: _stripped, ...strippedOutput } = callbackData.output ?? {}
311322
sendEvent({
312323
type: 'block:completed',
313324
timestamp: new Date().toISOString(),
@@ -318,13 +329,14 @@ export function createSSECallbacks(options: SSECallbackOptions) {
318329
blockName,
319330
blockType,
320331
input: callbackData.input,
321-
output: callbackData.output,
332+
output: strippedOutput,
322333
durationMs: callbackData.executionTime || 0,
323334
startedAt: callbackData.startedAt,
324335
executionOrder: callbackData.executionOrder,
325336
endedAt: callbackData.endedAt,
326337
...iterationData,
327338
...childWorkflowData,
339+
...instanceData,
328340
},
329341
})
330342
}

apps/sim/stores/terminal/console/store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,10 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
428428
updatedEntry.childWorkflowName = update.childWorkflowName
429429
}
430430

431+
if (update.childWorkflowInstanceId !== undefined) {
432+
updatedEntry.childWorkflowInstanceId = update.childWorkflowInstanceId
433+
}
434+
431435
return updatedEntry
432436
})
433437

apps/sim/stores/terminal/console/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface ConsoleEntry {
2828
childWorkflowBlockId?: string
2929
/** Display name of the child workflow this block belongs to */
3030
childWorkflowName?: string
31+
/** Per-invocation unique ID linking this workflow block to its child block events */
32+
childWorkflowInstanceId?: string
3133
}
3234

3335
export interface ConsoleUpdate {
@@ -50,6 +52,7 @@ export interface ConsoleUpdate {
5052
iterationContainerId?: string
5153
childWorkflowBlockId?: string
5254
childWorkflowName?: string
55+
childWorkflowInstanceId?: string
5356
}
5457

5558
export interface ConsoleStore {

0 commit comments

Comments
 (0)