@@ -6,13 +6,15 @@ import type { DAG } from '@/executor/dag/builder'
66import type { EdgeManager } from '@/executor/execution/edge-manager'
77import type { LoopScope } from '@/executor/execution/state'
88import type { BlockStateController , ContextExtensions } from '@/executor/execution/types'
9- import type { BlockLog , ExecutionContext , NormalizedBlockOutput } from '@/executor/types'
9+ import type { ExecutionContext , NormalizedBlockOutput } from '@/executor/types'
1010import type { LoopConfigWithNodes } from '@/executor/types/loop'
1111import { replaceValidReferences } from '@/executor/utils/reference-validation'
1212import {
13+ addSubflowErrorLog ,
1314 buildSentinelEndId ,
1415 buildSentinelStartId ,
1516 extractBaseBlockId ,
17+ resolveArrayInput ,
1618} from '@/executor/utils/subflow-utils'
1719import type { VariableResolver } from '@/executor/variables/resolver'
1820import 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