Skip to content

Commit 687c125

Browse files
authored
fix(parallel): correct active state pulsing and duration display for parallel subflow blocks (#3305)
* fix(executor): resolve block ID for parallel subflow active state * fix timing for parallel block * refactor(parallel): extract shared updateActiveBlockRefCount helper * fix(parallel): error-sticky block run status to prevent branch success masking failure * Revert "fix(parallel): error-sticky block run status to prevent branch success masking failure" This reverts commit 9c087cd.
1 parent 996dc96 commit 687c125

File tree

4 files changed

+67
-14
lines changed

4 files changed

+67
-14
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,9 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
261261
...allBlocks.map((b) => new Date(b.endedAt || b.timestamp).getTime())
262262
)
263263
const totalDuration = allBlocks.reduce((sum, b) => sum + (b.durationMs || 0), 0)
264+
// Parallel branches run concurrently — use wall-clock time. Loop iterations run serially — use sum.
265+
const subflowDuration =
266+
iterationType === 'parallel' ? subflowEndMs - subflowStartMs : totalDuration
264267

265268
// Create synthetic subflow parent entry
266269
// Use the minimum executionOrder from all child blocks for proper ordering
@@ -276,7 +279,7 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
276279
startedAt: new Date(subflowStartMs).toISOString(),
277280
executionOrder: subflowExecutionOrder,
278281
endedAt: new Date(subflowEndMs).toISOString(),
279-
durationMs: totalDuration,
282+
durationMs: subflowDuration,
280283
success: !allBlocks.some((b) => b.error),
281284
}
282285

@@ -291,6 +294,9 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
291294
...iterBlocks.map((b) => new Date(b.endedAt || b.timestamp).getTime())
292295
)
293296
const iterDuration = iterBlocks.reduce((sum, b) => sum + (b.durationMs || 0), 0)
297+
// Parallel branches run concurrently — use wall-clock time. Loop iterations run serially — use sum.
298+
const iterDisplayDuration =
299+
iterationType === 'parallel' ? iterEndMs - iterStartMs : iterDuration
294300

295301
// Use the minimum executionOrder from blocks in this iteration
296302
const iterExecutionOrder = Math.min(...iterBlocks.map((b) => b.executionOrder))
@@ -305,7 +311,7 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
305311
startedAt: new Date(iterStartMs).toISOString(),
306312
executionOrder: iterExecutionOrder,
307313
endedAt: new Date(iterEndMs).toISOString(),
308-
durationMs: iterDuration,
314+
durationMs: iterDisplayDuration,
309315
success: !iterBlocks.some((b) => b.error),
310316
iterationCurrent: iterGroup.iterationCurrent,
311317
iterationTotal: iterGroup.iterationTotal,

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
TriggerUtils,
2121
} from '@/lib/workflows/triggers/triggers'
2222
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
23+
import { updateActiveBlockRefCount } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-execution-utils'
2324
import { getBlock } from '@/blocks'
2425
import type { SerializableExecutionState } from '@/executor/execution/types'
2526
import type {
@@ -63,6 +64,7 @@ interface BlockEventHandlerConfig {
6364
executionIdRef: { current: string }
6465
workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }>
6566
activeBlocksSet: Set<string>
67+
activeBlockRefCounts: Map<string, number>
6668
accumulatedBlockLogs: BlockLog[]
6769
accumulatedBlockStates: Map<string, BlockState>
6870
executedBlockIds: Set<string>
@@ -309,6 +311,7 @@ export function useWorkflowExecution() {
309311
executionIdRef,
310312
workflowEdges,
311313
activeBlocksSet,
314+
activeBlockRefCounts,
312315
accumulatedBlockLogs,
313316
accumulatedBlockStates,
314317
executedBlockIds,
@@ -327,11 +330,7 @@ export function useWorkflowExecution() {
327330

328331
const updateActiveBlocks = (blockId: string, isActive: boolean) => {
329332
if (!workflowId) return
330-
if (isActive) {
331-
activeBlocksSet.add(blockId)
332-
} else {
333-
activeBlocksSet.delete(blockId)
334-
}
333+
updateActiveBlockRefCount(activeBlockRefCounts, activeBlocksSet, blockId, isActive)
335334
setActiveBlocks(workflowId, new Set(activeBlocksSet))
336335
}
337336

@@ -1280,6 +1279,7 @@ export function useWorkflowExecution() {
12801279
}
12811280

12821281
const activeBlocksSet = new Set<string>()
1282+
const activeBlockRefCounts = new Map<string, number>()
12831283
const streamedContent = new Map<string, string>()
12841284
const accumulatedBlockLogs: BlockLog[] = []
12851285
const accumulatedBlockStates = new Map<string, BlockState>()
@@ -1292,6 +1292,7 @@ export function useWorkflowExecution() {
12921292
executionIdRef,
12931293
workflowEdges,
12941294
activeBlocksSet,
1295+
activeBlockRefCounts,
12951296
accumulatedBlockLogs,
12961297
accumulatedBlockStates,
12971298
executedBlockIds,
@@ -1902,13 +1903,15 @@ export function useWorkflowExecution() {
19021903
const accumulatedBlockStates = new Map<string, BlockState>()
19031904
const executedBlockIds = new Set<string>()
19041905
const activeBlocksSet = new Set<string>()
1906+
const activeBlockRefCounts = new Map<string, number>()
19051907

19061908
try {
19071909
const blockHandlers = buildBlockEventHandlers({
19081910
workflowId,
19091911
executionIdRef,
19101912
workflowEdges,
19111913
activeBlocksSet,
1914+
activeBlockRefCounts,
19121915
accumulatedBlockLogs,
19131916
accumulatedBlockStates,
19141917
executedBlockIds,
@@ -2104,6 +2107,7 @@ export function useWorkflowExecution() {
21042107

21052108
const workflowEdges = useWorkflowStore.getState().edges
21062109
const activeBlocksSet = new Set<string>()
2110+
const activeBlockRefCounts = new Map<string, number>()
21072111
const accumulatedBlockLogs: BlockLog[] = []
21082112
const accumulatedBlockStates = new Map<string, BlockState>()
21092113
const executedBlockIds = new Set<string>()
@@ -2115,6 +2119,7 @@ export function useWorkflowExecution() {
21152119
executionIdRef,
21162120
workflowEdges,
21172121
activeBlocksSet,
2122+
activeBlockRefCounts,
21182123
accumulatedBlockLogs,
21192124
accumulatedBlockStates,
21202125
executedBlockIds,

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,30 @@ import { useTerminalConsoleStore } from '@/stores/terminal'
55
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
66
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
77

8+
/**
9+
* Updates the active blocks set and ref counts for a single block.
10+
* Ref counting ensures a block stays active until all parallel branches for it complete.
11+
*/
12+
export function updateActiveBlockRefCount(
13+
refCounts: Map<string, number>,
14+
activeSet: Set<string>,
15+
blockId: string,
16+
isActive: boolean
17+
): void {
18+
if (isActive) {
19+
refCounts.set(blockId, (refCounts.get(blockId) ?? 0) + 1)
20+
activeSet.add(blockId)
21+
} else {
22+
const next = (refCounts.get(blockId) ?? 1) - 1
23+
if (next <= 0) {
24+
refCounts.delete(blockId)
25+
activeSet.delete(blockId)
26+
} else {
27+
refCounts.set(blockId, next)
28+
}
29+
}
30+
}
31+
832
export interface WorkflowExecutionOptions {
933
workflowInput?: any
1034
onStream?: (se: StreamingExecution) => Promise<void>
@@ -39,6 +63,7 @@ export async function executeWorkflowWithFullLogging(
3963
const workflowEdges = useWorkflowStore.getState().edges
4064

4165
const activeBlocksSet = new Set<string>()
66+
const activeBlockRefCounts = new Map<string, number>()
4267

4368
const payload: any = {
4469
input: options.workflowInput,
@@ -103,7 +128,12 @@ export async function executeWorkflowWithFullLogging(
103128

104129
switch (event.type) {
105130
case 'block:started': {
106-
activeBlocksSet.add(event.data.blockId)
131+
updateActiveBlockRefCount(
132+
activeBlockRefCounts,
133+
activeBlocksSet,
134+
event.data.blockId,
135+
true
136+
)
107137
setActiveBlocks(wfId, new Set(activeBlocksSet))
108138

109139
const incomingEdges = workflowEdges.filter(
@@ -115,8 +145,13 @@ export async function executeWorkflowWithFullLogging(
115145
break
116146
}
117147

118-
case 'block:completed':
119-
activeBlocksSet.delete(event.data.blockId)
148+
case 'block:completed': {
149+
updateActiveBlockRefCount(
150+
activeBlockRefCounts,
151+
activeBlocksSet,
152+
event.data.blockId,
153+
false
154+
)
120155
setActiveBlocks(wfId, new Set(activeBlocksSet))
121156

122157
setBlockRunStatus(wfId, event.data.blockId, 'success')
@@ -144,9 +179,15 @@ export async function executeWorkflowWithFullLogging(
144179
options.onBlockComplete(event.data.blockId, event.data.output).catch(() => {})
145180
}
146181
break
182+
}
147183

148-
case 'block:error':
149-
activeBlocksSet.delete(event.data.blockId)
184+
case 'block:error': {
185+
updateActiveBlockRefCount(
186+
activeBlockRefCounts,
187+
activeBlocksSet,
188+
event.data.blockId,
189+
false
190+
)
150191
setActiveBlocks(wfId, new Set(activeBlocksSet))
151192

152193
setBlockRunStatus(wfId, event.data.blockId, 'error')
@@ -171,6 +212,7 @@ export async function executeWorkflowWithFullLogging(
171212
iterationContainerId: event.data.iterationContainerId,
172213
})
173214
break
215+
}
174216

175217
case 'execution:completed':
176218
executionResult = {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ export class BlockExecutor {
428428
block: SerializedBlock,
429429
executionOrder: number
430430
): void {
431-
const blockId = node.id
431+
const blockId = node.metadata?.originalBlockId ?? node.id
432432
const blockName = block.metadata?.name ?? blockId
433433
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
434434

@@ -456,7 +456,7 @@ export class BlockExecutor {
456456
executionOrder: number,
457457
endedAt: string
458458
): void {
459-
const blockId = node.id
459+
const blockId = node.metadata?.originalBlockId ?? node.id
460460
const blockName = block.metadata?.name ?? blockId
461461
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
462462

0 commit comments

Comments
 (0)