@@ -5,8 +5,8 @@ import { buildLoopIndexCondition, DEFAULTS, EDGE } from '@/executor/constants'
55import type { DAG } from '@/executor/dag/builder'
66import type { EdgeManager } from '@/executor/execution/edge-manager'
77import type { LoopScope } from '@/executor/execution/state'
8- import type { BlockStateController } from '@/executor/execution/types'
9- import type { ExecutionContext , NormalizedBlockOutput } from '@/executor/types'
8+ import type { BlockStateController , ContextExtensions } from '@/executor/execution/types'
9+ import type { BlockLog , ExecutionContext , NormalizedBlockOutput } from '@/executor/types'
1010import type { LoopConfigWithNodes } from '@/executor/types/loop'
1111import { replaceValidReferences } from '@/executor/utils/reference-validation'
1212import {
@@ -32,13 +32,18 @@ export interface LoopContinuationResult {
3232
3333export class LoopOrchestrator {
3434 private edgeManager : EdgeManager | null = null
35+ private contextExtensions : ContextExtensions | null = null
3536
3637 constructor (
3738 private dag : DAG ,
3839 private state : BlockStateController ,
3940 private resolver : VariableResolver
4041 ) { }
4142
43+ setContextExtensions ( contextExtensions : ContextExtensions ) : void {
44+ this . contextExtensions = contextExtensions
45+ }
46+
4247 setEdgeManager ( edgeManager : EdgeManager ) : void {
4348 this . edgeManager = edgeManager
4449 }
@@ -58,18 +63,54 @@ export class LoopOrchestrator {
5863 const loopType = loopConfig . loopType
5964
6065 switch ( loopType ) {
61- case 'for' :
66+ case 'for' : {
6267 scope . loopType = 'for'
63- scope . maxIterations = loopConfig . iterations || DEFAULTS . MAX_LOOP_ITERATIONS
68+ const requestedIterations = loopConfig . iterations || DEFAULTS . MAX_LOOP_ITERATIONS
69+
70+ if ( requestedIterations > DEFAULTS . MAX_LOOP_ITERATIONS ) {
71+ const errorMessage = `For loop iterations (${ requestedIterations } ) exceeds maximum allowed (${ DEFAULTS . MAX_LOOP_ITERATIONS } ). Loop execution blocked.`
72+ logger . error ( errorMessage , { loopId, requestedIterations } )
73+ this . addLoopErrorLog ( ctx , loopId , loopType , errorMessage )
74+ // Set to 0 iterations to prevent loop from running
75+ scope . maxIterations = 0
76+ scope . validationError = errorMessage
77+ } else {
78+ scope . maxIterations = requestedIterations
79+ }
80+
6481 scope . condition = buildLoopIndexCondition ( scope . maxIterations )
6582 break
83+ }
6684
6785 case 'forEach' : {
6886 scope . loopType = 'forEach'
87+ if ( ! Array . isArray ( loopConfig . forEachItems ) ) {
88+ const errorMessage =
89+ 'ForEach loop collection is not a valid array. Loop execution blocked.'
90+ logger . error ( errorMessage , { loopId, forEachItems : loopConfig . forEachItems } )
91+ this . addLoopErrorLog ( ctx , loopId , loopType , errorMessage )
92+ scope . items = [ ]
93+ scope . maxIterations = 0
94+ scope . validationError = errorMessage
95+ scope . condition = buildLoopIndexCondition ( 0 )
96+ break
97+ }
6998 const items = this . resolveForEachItems ( ctx , loopConfig . forEachItems )
70- scope . items = items
71- scope . maxIterations = items . length
72- scope . item = items [ 0 ]
99+ const originalLength = items . length
100+
101+ if ( originalLength > DEFAULTS . MAX_FOREACH_ITEMS ) {
102+ const errorMessage = `ForEach loop collection size (${ originalLength } ) exceeds maximum allowed (${ DEFAULTS . MAX_FOREACH_ITEMS } ). Loop execution blocked.`
103+ logger . error ( errorMessage , { loopId, originalLength } )
104+ this . addLoopErrorLog ( ctx , loopId , loopType , errorMessage )
105+ scope . items = [ ]
106+ scope . maxIterations = 0
107+ scope . validationError = errorMessage
108+ } else {
109+ scope . items = items
110+ scope . maxIterations = items . length
111+ scope . item = items [ 0 ]
112+ }
113+
73114 scope . condition = buildLoopIndexCondition ( scope . maxIterations )
74115 break
75116 }
@@ -79,15 +120,28 @@ export class LoopOrchestrator {
79120 scope . condition = loopConfig . whileCondition
80121 break
81122
82- case 'doWhile' :
123+ case 'doWhile' : {
83124 scope . loopType = 'doWhile'
84125 if ( loopConfig . doWhileCondition ) {
85126 scope . condition = loopConfig . doWhileCondition
86127 } else {
87- scope . maxIterations = loopConfig . iterations || DEFAULTS . MAX_LOOP_ITERATIONS
128+ const requestedIterations = loopConfig . iterations || DEFAULTS . MAX_LOOP_ITERATIONS
129+
130+ if ( requestedIterations > DEFAULTS . MAX_LOOP_ITERATIONS ) {
131+ const errorMessage = `Do-While loop iterations (${ requestedIterations } ) exceeds maximum allowed (${ DEFAULTS . MAX_LOOP_ITERATIONS } ). Loop execution blocked.`
132+ logger . error ( errorMessage , { loopId, requestedIterations } )
133+ this . addLoopErrorLog ( ctx , loopId , loopType , errorMessage )
134+ // Set to 0 iterations to prevent loop from running
135+ scope . maxIterations = 0
136+ scope . validationError = errorMessage
137+ } else {
138+ scope . maxIterations = requestedIterations
139+ }
140+
88141 scope . condition = buildLoopIndexCondition ( scope . maxIterations )
89142 }
90143 break
144+ }
91145
92146 default :
93147 throw new Error ( `Unknown loop type: ${ loopType } ` )
@@ -100,6 +154,47 @@ export class LoopOrchestrator {
100154 return scope
101155 }
102156
157+ /**
158+ * Adds an error log entry for loop validation errors.
159+ * These errors appear in the block console on the logs dashboard.
160+ */
161+ private addLoopErrorLog (
162+ ctx : ExecutionContext ,
163+ loopId : string ,
164+ loopType : string ,
165+ errorMessage : string
166+ ) : void {
167+ const now = new Date ( ) . toISOString ( )
168+
169+ // Get the actual loop block name from the workflow
170+ const loopBlock = ctx . workflow ?. blocks ?. find ( ( b ) => b . id === loopId )
171+ const blockName = loopBlock ?. metadata ?. name || `Loop`
172+
173+ const blockLog : BlockLog = {
174+ blockId : loopId ,
175+ blockName,
176+ blockType : 'loop' ,
177+ startedAt : now ,
178+ endedAt : now ,
179+ durationMs : 0 ,
180+ success : false ,
181+ error : errorMessage ,
182+ input : { } ,
183+ output : { error : errorMessage } ,
184+ loopId,
185+ }
186+ ctx . blockLogs . push ( blockLog )
187+
188+ // Emit the error through onBlockComplete callback so it appears in the UI console
189+ if ( this . contextExtensions ?. onBlockComplete ) {
190+ this . contextExtensions . onBlockComplete ( loopId , blockName , 'loop' , {
191+ input : { } ,
192+ output : { error : errorMessage } ,
193+ executionTime : 0 ,
194+ } )
195+ }
196+ }
197+
103198 storeLoopNodeOutput (
104199 ctx : ExecutionContext ,
105200 loopId : string ,
0 commit comments