Skip to content

Commit 39444fa

Browse files
author
priyanshu.solanki
committed
fixed for empty loop and paralle blocks and showing input on dashboard
1 parent 45ca926 commit 39444fa

File tree

4 files changed

+226
-203
lines changed

4 files changed

+226
-203
lines changed

apps/sim/executor/dag/builder.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { LoopConstructor } from '@/executor/dag/construction/loops'
44
import { NodeConstructor } from '@/executor/dag/construction/nodes'
55
import { PathConstructor } from '@/executor/dag/construction/paths'
66
import type { DAGEdge, NodeMetadata } from '@/executor/dag/types'
7+
import { buildSentinelStartId, extractBaseBlockId } from '@/executor/utils/subflow-utils'
78
import type {
89
SerializedBlock,
910
SerializedLoop,
@@ -79,6 +80,9 @@ export class DAGBuilder {
7980
}
8081
}
8182

83+
// Validate loop and parallel structure
84+
this.validateSubflowStructure(dag)
85+
8286
logger.info('DAG built', {
8387
totalNodes: dag.nodes.size,
8488
loopCount: dag.loopConfigs.size,
@@ -105,4 +109,43 @@ export class DAGBuilder {
105109
}
106110
}
107111
}
112+
113+
/**
114+
* Validates that loops and parallels have proper internal structure.
115+
* Throws an error if a loop/parallel has no blocks inside or no connections from start.
116+
*/
117+
private validateSubflowStructure(dag: DAG): void {
118+
for (const [id, config] of dag.loopConfigs) {
119+
this.validateSubflow(dag, id, config.nodes, 'Loop')
120+
}
121+
for (const [id, config] of dag.parallelConfigs) {
122+
this.validateSubflow(dag, id, config.nodes, 'Parallel')
123+
}
124+
}
125+
126+
private validateSubflow(
127+
dag: DAG,
128+
id: string,
129+
nodes: string[] | undefined,
130+
type: 'Loop' | 'Parallel'
131+
): void {
132+
if (!nodes || nodes.length === 0) {
133+
throw new Error(
134+
`${type} has no blocks inside. Add at least one block to the ${type.toLowerCase()}.`
135+
)
136+
}
137+
138+
const sentinelStartNode = dag.nodes.get(buildSentinelStartId(id))
139+
if (!sentinelStartNode) return
140+
141+
const hasConnections = Array.from(sentinelStartNode.outgoingEdges.values()).some((edge) =>
142+
nodes.includes(extractBaseBlockId(edge.target))
143+
)
144+
145+
if (!hasConnections) {
146+
throw new Error(
147+
`${type} start is not connected to any blocks. Connect a block to the ${type.toLowerCase()} start.`
148+
)
149+
}
150+
}
108151
}

apps/sim/executor/orchestrators/loop.ts

Lines changed: 43 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import type { DAG } from '@/executor/dag/builder'
66
import type { EdgeManager } from '@/executor/execution/edge-manager'
77
import type { LoopScope } from '@/executor/execution/state'
88
import type { BlockStateController, ContextExtensions } from '@/executor/execution/types'
9-
import type { BlockLog, ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
9+
import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
1010
import type { LoopConfigWithNodes } from '@/executor/types/loop'
1111
import { replaceValidReferences } from '@/executor/utils/reference-validation'
1212
import {
13+
addSubflowErrorLog,
1314
buildSentinelEndId,
1415
buildSentinelStartId,
1516
extractBaseBlockId,
17+
resolveArrayInput,
1618
} from '@/executor/utils/subflow-utils'
1719
import type { VariableResolver } from '@/executor/variables/resolver'
1820
import type { SerializedLoop } from '@/serializer/types'
@@ -53,7 +55,6 @@ export class LoopOrchestrator {
5355
if (!loopConfig) {
5456
throw new Error(`Loop config not found: ${loopId}`)
5557
}
56-
5758
const scope: LoopScope = {
5859
iteration: 0,
5960
currentIterationOutputs: new Map(),
@@ -70,24 +71,24 @@ export class LoopOrchestrator {
7071
if (requestedIterations > DEFAULTS.MAX_LOOP_ITERATIONS) {
7172
const errorMessage = `For loop iterations (${requestedIterations}) exceeds maximum allowed (${DEFAULTS.MAX_LOOP_ITERATIONS}). Loop execution blocked.`
7273
logger.error(errorMessage, { loopId, requestedIterations })
73-
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage)
74-
// Set to 0 iterations to prevent loop from running
74+
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {
75+
iterations: requestedIterations,
76+
})
7577
scope.maxIterations = 0
7678
scope.validationError = errorMessage
77-
} else {
78-
scope.maxIterations = requestedIterations
79+
scope.condition = buildLoopIndexCondition(0)
80+
ctx.loopExecutions?.set(loopId, scope)
81+
throw new Error(errorMessage)
7982
}
8083

84+
scope.maxIterations = requestedIterations
8185
scope.condition = buildLoopIndexCondition(scope.maxIterations)
8286
break
8387
}
8488

8589
case 'forEach': {
8690
scope.loopType = 'forEach'
87-
// Resolve items first - forEachItems can be a string, reference, array, or object
8891
const items = this.resolveForEachItems(ctx, loopConfig.forEachItems)
89-
90-
// Check if resolution failed (empty result from non-empty input)
9192
const hasInput =
9293
loopConfig.forEachItems !== undefined &&
9394
loopConfig.forEachItems !== null &&
@@ -96,28 +97,36 @@ export class LoopOrchestrator {
9697
const errorMessage =
9798
'ForEach loop collection is not a valid array. Loop execution blocked.'
9899
logger.error(errorMessage, { loopId, forEachItems: loopConfig.forEachItems })
99-
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage)
100+
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {
101+
forEachItems: loopConfig.forEachItems,
102+
})
100103
scope.items = []
101104
scope.maxIterations = 0
102105
scope.validationError = errorMessage
103106
scope.condition = buildLoopIndexCondition(0)
104-
break
107+
ctx.loopExecutions?.set(loopId, scope)
108+
throw new Error(errorMessage)
105109
}
106-
const originalLength = items.length
107110

111+
const originalLength = items.length
108112
if (originalLength > DEFAULTS.MAX_FOREACH_ITEMS) {
109113
const errorMessage = `ForEach loop collection size (${originalLength}) exceeds maximum allowed (${DEFAULTS.MAX_FOREACH_ITEMS}). Loop execution blocked.`
110114
logger.error(errorMessage, { loopId, originalLength })
111-
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage)
115+
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {
116+
forEachItems: loopConfig.forEachItems,
117+
collectionSize: originalLength,
118+
})
112119
scope.items = []
113120
scope.maxIterations = 0
114121
scope.validationError = errorMessage
115-
} else {
116-
scope.items = items
117-
scope.maxIterations = items.length
118-
scope.item = items[0]
122+
scope.condition = buildLoopIndexCondition(0)
123+
ctx.loopExecutions?.set(loopId, scope)
124+
throw new Error(errorMessage)
119125
}
120126

127+
scope.items = items
128+
scope.maxIterations = items.length
129+
scope.item = items[0]
121130
scope.condition = buildLoopIndexCondition(scope.maxIterations)
122131
break
123132
}
@@ -137,14 +146,17 @@ export class LoopOrchestrator {
137146
if (requestedIterations > DEFAULTS.MAX_LOOP_ITERATIONS) {
138147
const errorMessage = `Do-While loop iterations (${requestedIterations}) exceeds maximum allowed (${DEFAULTS.MAX_LOOP_ITERATIONS}). Loop execution blocked.`
139148
logger.error(errorMessage, { loopId, requestedIterations })
140-
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage)
141-
// Set to 0 iterations to prevent loop from running
149+
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {
150+
iterations: requestedIterations,
151+
})
142152
scope.maxIterations = 0
143153
scope.validationError = errorMessage
144-
} else {
145-
scope.maxIterations = requestedIterations
154+
scope.condition = buildLoopIndexCondition(0)
155+
ctx.loopExecutions?.set(loopId, scope)
156+
throw new Error(errorMessage)
146157
}
147158

159+
scope.maxIterations = requestedIterations
148160
scope.condition = buildLoopIndexCondition(scope.maxIterations)
149161
}
150162
break
@@ -161,45 +173,21 @@ export class LoopOrchestrator {
161173
return scope
162174
}
163175

164-
/**
165-
* Adds an error log entry for loop validation errors.
166-
* These errors appear in the block console on the logs dashboard.
167-
*/
168176
private addLoopErrorLog(
169177
ctx: ExecutionContext,
170178
loopId: string,
171179
loopType: string,
172-
errorMessage: string
180+
errorMessage: string,
181+
inputData?: any
173182
): void {
174-
const now = new Date().toISOString()
175-
176-
// Get the actual loop block name from the workflow
177-
const loopBlock = ctx.workflow?.blocks?.find((b) => b.id === loopId)
178-
const blockName = loopBlock?.metadata?.name || `Loop`
179-
180-
const blockLog: BlockLog = {
181-
blockId: loopId,
182-
blockName,
183-
blockType: 'loop',
184-
startedAt: now,
185-
endedAt: now,
186-
durationMs: 0,
187-
success: false,
188-
error: errorMessage,
189-
input: {},
190-
output: { error: errorMessage },
183+
addSubflowErrorLog(
184+
ctx,
191185
loopId,
192-
}
193-
ctx.blockLogs.push(blockLog)
194-
195-
// Emit the error through onBlockComplete callback so it appears in the UI console
196-
if (this.contextExtensions?.onBlockComplete) {
197-
this.contextExtensions.onBlockComplete(loopId, blockName, 'loop', {
198-
input: {},
199-
output: { error: errorMessage },
200-
executionTime: 0,
201-
})
202-
}
186+
'loop',
187+
errorMessage,
188+
{ loopType, ...inputData },
189+
this.contextExtensions
190+
)
203191
}
204192

205193
storeLoopNodeOutput(
@@ -514,54 +502,6 @@ export class LoopOrchestrator {
514502
}
515503

516504
private resolveForEachItems(ctx: ExecutionContext, items: any): any[] {
517-
if (Array.isArray(items)) {
518-
return items
519-
}
520-
521-
if (typeof items === 'object' && items !== null) {
522-
return Object.entries(items)
523-
}
524-
525-
if (typeof items === 'string') {
526-
if (items.startsWith('<') && items.endsWith('>')) {
527-
const resolved = this.resolver.resolveSingleReference(ctx, '', items)
528-
if (Array.isArray(resolved)) {
529-
return resolved
530-
}
531-
return []
532-
}
533-
534-
try {
535-
const normalized = items.replace(/'/g, '"')
536-
const parsed = JSON.parse(normalized)
537-
if (Array.isArray(parsed)) {
538-
return parsed
539-
}
540-
return []
541-
} catch (error) {
542-
logger.error('Failed to parse forEach items', { items, error })
543-
return []
544-
}
545-
}
546-
547-
try {
548-
const resolved = this.resolver.resolveInputs(ctx, 'loop_foreach_items', { items }).items
549-
550-
if (Array.isArray(resolved)) {
551-
return resolved
552-
}
553-
554-
logger.warn('ForEach items did not resolve to array', {
555-
items,
556-
resolved,
557-
})
558-
559-
return []
560-
} catch (error: any) {
561-
logger.error('Error resolving forEach items, returning empty array:', {
562-
error: error.message,
563-
})
564-
return []
565-
}
505+
return resolveArrayInput(ctx, items, this.resolver)
566506
}
567507
}

0 commit comments

Comments
 (0)