From d98545d5549372d7db6b0c8ff06cd2d84bad4408 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 25 Feb 2026 19:02:44 -0800 Subject: [PATCH] fix(terminal): thread executionOrder through child workflow SSE events for loop support (#3346) * fix(terminal): thread executionOrder through child workflow SSE events for loop support * ran lint * fix(terminal): render iteration children through EntryNodeRow for workflow block expansion IterationNodeRow was rendering all children as flat BlockRow components, ignoring nodeType. Workflow blocks inside loop iterations were never rendered as WorkflowNodeRow, so they had no expand chevron or child tree. * fix(terminal): add childWorkflowBlockId to matchesEntryForUpdate Sub-executors reset executionOrderCounter, so child blocks across loop iterations share the same blockId + executionOrder. Without checking childWorkflowBlockId, updateConsole for iteration N overwrites entries from iterations 0..N-1, causing all child blocks to be grouped under the last iteration's workflow instance. --- .../app/api/workflows/[id]/execute/route.ts | 4 +++- .../components/terminal/terminal.tsx | 18 ++++++++++++++---- .../hooks/use-workflow-execution.ts | 2 ++ apps/sim/executor/execution/block-executor.ts | 5 ++++- apps/sim/executor/execution/types.ts | 6 ++++-- .../handlers/workflow/workflow-handler.ts | 9 ++++++++- apps/sim/executor/types.ts | 4 +++- .../lib/workflows/executor/execution-events.ts | 5 ++++- apps/sim/stores/terminal/console/store.ts | 7 +++++++ 9 files changed, 49 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index cf8431f9dc..b393ae492a 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -987,7 +987,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: const onChildWorkflowInstanceReady = ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => { sendEvent({ type: 'block:childWorkflowStarted', @@ -1001,6 +1002,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: iterationCurrent: iterationContext.iterationCurrent, iterationContainerId: iterationContext.iterationContainerId, }), + ...(executionOrder !== undefined && { executionOrder }), }, }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 9d9b206b79..204ca166c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -160,12 +160,16 @@ const IterationNodeRow = memo(function IterationNodeRow({ onSelectEntry, isExpanded, onToggle, + expandedNodes, + onToggleNode, }: { node: EntryNode selectedEntryId: string | null onSelectEntry: (entry: ConsoleEntry) => void isExpanded: boolean onToggle: () => void + expandedNodes: Set + onToggleNode: (nodeId: string) => void }) { const { entry, children, iterationInfo } = node const hasError = Boolean(entry.error) || children.some((c) => c.entry.error) @@ -226,11 +230,13 @@ const IterationNodeRow = memo(function IterationNodeRow({ {isExpanded && hasChildren && (
{children.map((child) => ( - ))}
@@ -346,6 +352,8 @@ const SubflowNodeRow = memo(function SubflowNodeRow({ onSelectEntry={onSelectEntry} isExpanded={expandedNodes.has(iterNode.entry.id)} onToggle={() => onToggleNode(iterNode.entry.id)} + expandedNodes={expandedNodes} + onToggleNode={onToggleNode} /> ))} @@ -520,6 +528,8 @@ const EntryNodeRow = memo(function EntryNodeRow({ onSelectEntry={onSelectEntry} isExpanded={expandedNodes.has(node.entry.id)} onToggle={() => onToggleNode(node.entry.id)} + expandedNodes={expandedNodes} + onToggleNode={onToggleNode} /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index f1bcca15b7..1af79967c8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -554,6 +554,7 @@ export function useWorkflowExecution() { childWorkflowInstanceId: string iterationCurrent?: number iterationContainerId?: string + executionOrder?: number }) => { if (isStaleExecution()) return updateConsole( @@ -564,6 +565,7 @@ export function useWorkflowExecution() { ...(data.iterationContainerId !== undefined && { iterationContainerId: data.iterationContainerId, }), + ...(data.executionOrder !== undefined && { executionOrder: data.executionOrder }), }, executionIdRef.current ) diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 6d7f746d4e..9325aa2861 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -80,7 +80,10 @@ export class BlockExecutor { const startTime = performance.now() let resolvedInputs: Record = {} - const nodeMetadata = this.buildNodeMetadata(node) + const nodeMetadata = { + ...this.buildNodeMetadata(node), + executionOrder: blockLog?.executionOrder, + } let cleanupSelfReference: (() => void) | undefined if (block.metadata?.id === BlockType.HUMAN_IN_THE_LOOP) { diff --git a/apps/sim/executor/execution/types.ts b/apps/sim/executor/execution/types.ts index d3ac877b89..bea082fe8a 100644 --- a/apps/sim/executor/execution/types.ts +++ b/apps/sim/executor/execution/types.ts @@ -89,7 +89,8 @@ export interface ExecutionCallbacks { onChildWorkflowInstanceReady?: ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => void } @@ -155,7 +156,8 @@ export interface ContextExtensions { onChildWorkflowInstanceReady?: ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => void /** diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 596e879683..b0123d7844 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -62,6 +62,7 @@ export class WorkflowBlockHandler implements BlockHandler { branchTotal?: number originalBlockId?: string isLoopNode?: boolean + executionOrder?: number } ): Promise { return this._executeCore(ctx, block, inputs, nodeMetadata) @@ -79,6 +80,7 @@ export class WorkflowBlockHandler implements BlockHandler { branchTotal?: number originalBlockId?: string isLoopNode?: boolean + executionOrder?: number } ): Promise { logger.info(`Executing workflow block: ${block.id}`) @@ -169,7 +171,12 @@ export class WorkflowBlockHandler implements BlockHandler { const iterationContext = nodeMetadata ? this.getIterationContext(ctx, nodeMetadata) : undefined - ctx.onChildWorkflowInstanceReady?.(effectiveBlockId, instanceId, iterationContext) + ctx.onChildWorkflowInstanceReady?.( + effectiveBlockId, + instanceId, + iterationContext, + nodeMetadata?.executionOrder + ) } const subExecutor = new Executor({ diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index b30dba2e1b..bf672dfd94 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -264,7 +264,8 @@ export interface ExecutionContext { onChildWorkflowInstanceReady?: ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => void /** @@ -377,6 +378,7 @@ export interface BlockHandler { branchTotal?: number originalBlockId?: string isLoopNode?: boolean + executionOrder?: number } ) => Promise } diff --git a/apps/sim/lib/workflows/executor/execution-events.ts b/apps/sim/lib/workflows/executor/execution-events.ts index 09044acdc5..a9324d1087 100644 --- a/apps/sim/lib/workflows/executor/execution-events.ts +++ b/apps/sim/lib/workflows/executor/execution-events.ts @@ -155,6 +155,7 @@ export interface BlockChildWorkflowStartedEvent extends BaseExecutionEvent { childWorkflowInstanceId: string iterationCurrent?: number iterationContainerId?: string + executionOrder?: number } } @@ -396,7 +397,8 @@ export function createSSECallbacks(options: SSECallbackOptions) { const onChildWorkflowInstanceReady = ( blockId: string, childWorkflowInstanceId: string, - iterationContext?: IterationContext + iterationContext?: IterationContext, + executionOrder?: number ) => { sendEvent({ type: 'block:childWorkflowStarted', @@ -410,6 +412,7 @@ export function createSSECallbacks(options: SSECallbackOptions) { iterationCurrent: iterationContext.iterationCurrent, iterationContainerId: iterationContext.iterationContainerId, }), + ...(executionOrder !== undefined && { executionOrder }), }, }) } diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index e3b7f3ea10..3468e4d212 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -91,6 +91,13 @@ const matchesEntryForUpdate = ( return false } + if ( + update.childWorkflowBlockId !== undefined && + entry.childWorkflowBlockId !== update.childWorkflowBlockId + ) { + return false + } + return true }