@@ -28,35 +28,31 @@ export class DynamicNode implements INodeType {
2828 default : { } ,
2929 description : 'Paste in your exported node JSON here' ,
3030 } ,
31- {
32- displayName : 'Execute Individually Per Item?' ,
33- name : 'executeIndividually' ,
34- type : 'boolean' ,
35- default : true ,
36- description : 'Whether to execute the sub-workflow once per input item. If false, all items are passed in together.' ,
37- } ,
38- {
39- displayName : 'Disable Waiting for Child Workflow(s) to Finish?' ,
40- name : 'doNotWaitToFinish' ,
41- type : 'boolean' ,
42- default : false ,
43- description : 'Whether to return immediately after starting the sub-workflow. Advanced: disables result collection.' ,
44- } ,
4531 ] ,
4632 } ;
4733
4834 async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [ ] [ ] > {
49- const inputItems = this . getInputData ( ) ;
50- const executeIndividually = this . getNodeParameter ( 'executeIndividually' , 0 ) as boolean ;
51- const doNotWaitToFinish = this . getNodeParameter ( 'doNotWaitToFinish' , 0 ) as boolean ;
35+ const items = this . getInputData ( ) ;
36+
37+ // ✅ Enforce: exactly 1 item per call
38+ if ( items . length !== 1 ) {
39+ throw new NodeOperationError (
40+ this . getNode ( ) ,
41+ `Dynamic Node must be called with exactly 1 item. Received ${ items . length } .` ,
42+ ) ;
43+ }
5244
5345 const rawParam = this . getNodeParameter ( 'nodeJson' , 0 ) as any ;
5446 let raw : any ;
47+
5548 if ( typeof rawParam === 'string' ) {
5649 try {
5750 raw = JSON . parse ( rawParam ) ;
5851 } catch {
59- throw new NodeOperationError ( this . getNode ( ) , 'Node JSON must be valid JSON' ) ;
52+ throw new NodeOperationError (
53+ this . getNode ( ) ,
54+ 'Node JSON must be a valid JSON object or a parseable JSON string' ,
55+ ) ;
6056 }
6157 } else {
6258 raw = rawParam ;
@@ -66,144 +62,55 @@ export class DynamicNode implements INodeType {
6662 throw new NodeOperationError ( this . getNode ( ) , 'Node JSON must be an object' ) ;
6763 }
6864
69- const baseNode = Array . isArray ( raw . nodes ) && raw . nodes . length > 0 ? raw . nodes [ 0 ] : raw ;
70- delete baseNode . connections ;
71- delete baseNode . pinData ;
72- delete baseNode . meta ;
65+ const nodeJson = Array . isArray ( raw . nodes ) && raw . nodes . length > 0 ? raw . nodes [ 0 ] : raw ;
7366
74- if ( ! baseNode . name ) {
67+ delete nodeJson . connections ;
68+ delete nodeJson . pinData ;
69+ delete nodeJson . meta ;
70+
71+ if ( ! nodeJson . name ) {
7572 throw new NodeOperationError ( this . getNode ( ) , 'Your JSON must include a `name` field' ) ;
7673 }
7774
78- const allResults : INodeExecutionData [ ] = [ ] ;
79-
80- const processItem = async ( item : INodeExecutionData , index : number ) : Promise < void > => {
81- const template = JSON . parse ( JSON . stringify ( subWorkflowTemplate ) ) ;
82- const nodeClone = JSON . parse ( JSON . stringify ( baseNode ) ) ;
83-
84- nodeClone . name = `${ baseNode . name } - Dynamic Node [${ index + 1 } ]` ;
85- nodeClone . id = `dynamic-${ uuidv4 ( ) } ` ;
86- nodeClone . position = Array . isArray ( baseNode . position ) && baseNode . position . length === 2
87- ? baseNode . position
88- : [ 240 , 0 ] ;
89-
90- // Evaluate top-level parameters
91- for ( const key of Object . keys ( nodeClone . parameters ) ) {
92- const value = nodeClone . parameters [ key ] ;
93- if ( typeof value === 'string' && value . includes ( '{{' ) ) {
94- try {
95- nodeClone . parameters [ key ] = await this . evaluateExpression ( value , index ) ;
96- } catch ( err ) {
97- this . logger . warn ( `DynamicNode: Failed to evaluate parameter '${ key } ': ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
98- }
99- }
100- }
75+ nodeJson . name = `${ nodeJson . name } - Dynamic Node` ;
76+ nodeJson . id = `dynamic-${ uuidv4 ( ) } ` ;
77+ nodeJson . position = [ 240 , 0 ] ;
10178
102- // Evaluate nested body parameters
103- const bodyParams = nodeClone . parameters ?. bodyParameters ?. parameters ;
104- if ( Array . isArray ( bodyParams ) ) {
105- for ( const param of bodyParams ) {
106- if ( typeof param . value === 'string' && param . value . includes ( '{{' ) ) {
107- try {
108- param . value = await this . evaluateExpression ( param . value , index ) ;
109- } catch ( err ) {
110- this . logger . warn ( `DynamicNode: Failed to evaluate body param '${ param . name } ': ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
111- }
112- }
113- }
114- }
79+ const template = JSON . parse ( JSON . stringify ( subWorkflowTemplate ) ) ;
80+ template . nodes . push ( nodeJson ) ;
81+ template . connections . Start . main [ 0 ] [ 0 ] . node = nodeJson . name ;
11582
116- template . nodes . push ( nodeClone ) ;
117- template . connections . Start . main [ 0 ] [ 0 ] . node = nodeClone . name ;
118-
119- const workflowProxy = this . getWorkflowDataProxy ( index ) ;
120-
121- const execResult = await this . executeWorkflow (
122- { code : template } ,
123- [ item ] ,
124- { } ,
125- {
126- parentExecution : {
127- executionId : workflowProxy . $execution . id ,
128- workflowId : workflowProxy . $workflow . id ,
129- } ,
130- doNotWaitToFinish,
131- additionalData : {
132- itemIndex : index ,
133- } ,
134- } as any
135- ) ;
83+ const workflowProxy = this . getWorkflowDataProxy ( 0 ) ;
13684
137- if ( ! doNotWaitToFinish && execResult ) {
138- const resultArray = Array . isArray ( execResult )
139- ? execResult
140- : Array . isArray ( ( execResult as any ) . data )
141- ? ( execResult as any ) . data
142- : [ ] ;
143-
144- allResults . push (
145- ...resultArray . flat ( ) . filter ( ( entry : unknown ) : entry is INodeExecutionData =>
146- entry !== null && typeof entry === 'object' ,
147- ) ,
148- ) ;
149- }
150- } ;
151-
152- if ( executeIndividually ) {
153- for ( let i = 0 ; i < inputItems . length ; i ++ ) {
154- try {
155- await processItem ( inputItems [ i ] , i ) ;
156- } catch ( err ) {
157- this . logger . warn ( `DynamicNode: Error processing item #${ i + 1 } : ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
158- }
159- }
160- } else {
161- // One execution for all items (rare)
162- const template = JSON . parse ( JSON . stringify ( subWorkflowTemplate ) ) ;
163- const nodeClone = JSON . parse ( JSON . stringify ( baseNode ) ) ;
164-
165- nodeClone . name = `${ baseNode . name } - Dynamic Node [all]` ;
166- nodeClone . id = `dynamic-${ uuidv4 ( ) } ` ;
167- nodeClone . position = Array . isArray ( baseNode . position ) && baseNode . position . length === 2
168- ? baseNode . position
169- : [ 240 , 0 ] ;
170-
171- template . nodes . push ( nodeClone ) ;
172- template . connections . Start . main [ 0 ] [ 0 ] . node = nodeClone . name ;
173-
174- const workflowProxy = this . getWorkflowDataProxy ( 0 ) ;
175-
176- const execResult = await this . executeWorkflow (
177- { code : template } ,
178- inputItems ,
179- { } ,
180- {
181- parentExecution : {
182- executionId : workflowProxy . $execution . id ,
183- workflowId : workflowProxy . $workflow . id ,
184- } ,
185- doNotWaitToFinish,
186- additionalData : {
187- itemIndex : 0 ,
188- } ,
189- } as any ,
190- ) ;
85+ const executionResult : any = await this . executeWorkflow (
86+ { code : template } ,
87+ items ,
88+ { } ,
89+ {
90+ parentExecution : {
91+ executionId : workflowProxy . $execution . id ,
92+ workflowId : workflowProxy . $workflow . id ,
93+ } ,
94+ doNotWaitToFinish : false ,
95+ } ,
96+ ) ;
19197
192- if ( ! doNotWaitToFinish && execResult ) {
193- const resultArray = Array . isArray ( execResult )
194- ? execResult
195- : Array . isArray ( ( execResult as any ) . data )
196- ? ( execResult as any ) . data
197- : [ ] ;
198-
199- allResults . push (
200- ...resultArray . flat ( ) . filter ( ( entry : unknown ) : entry is INodeExecutionData =>
201- entry !== null && typeof entry === 'object' ,
202- ) ,
203- ) ;
98+ let returnedData : INodeExecutionData [ ] [ ] = [ ] ;
99+
100+ if ( Array . isArray ( executionResult ) ) {
101+ returnedData = executionResult as INodeExecutionData [ ] [ ] ;
102+ } else if ( executionResult && typeof executionResult === 'object' && 'data' in executionResult ) {
103+ if ( Array . isArray ( ( executionResult as any ) . data ) ) {
104+ returnedData = ( executionResult as any ) . data as INodeExecutionData [ ] [ ] ;
105+ } else {
106+ this . logger . warn ( 'DynamicNode: Sub-workflow executionResult.data was not an array. Returning empty data.' ) ;
204107 }
108+ } else if ( executionResult === null || executionResult === undefined ) {
109+ this . logger . warn ( 'DynamicNode: Sub-workflow executionResult was null or undefined. Returning empty data.' ) ;
110+ } else {
111+ this . logger . warn ( `DynamicNode: Unexpected structure from sub-workflow execution. Type: ${ typeof executionResult } . Returning empty data.` ) ;
205112 }
206113
207- return [ allResults ] ;
114+ return returnedData ;
208115 }
209116}
0 commit comments