77 NodeConnectionType ,
88} from 'n8n-workflow' ;
99import { v4 as uuidv4 } from 'uuid' ;
10-
11- // JSON import requires "resolveJsonModule" in tsconfig
1210import subWorkflowTemplate from './subWorkflowTemplate.json' ;
1311
1412export class DynamicNode implements INodeType {
@@ -30,108 +28,107 @@ export class DynamicNode implements INodeType {
3028 default : { } ,
3129 description : 'Paste in your exported node JSON here' ,
3230 } ,
31+ {
32+ displayName : 'Execute individually per item?' ,
33+ name : 'executeIndividually' ,
34+ type : 'boolean' ,
35+ default : true ,
36+ description :
37+ 'If enabled, each input item will run in its own sub-workflow. Disable only if your node can handle bulk items safely.' ,
38+ } ,
39+ {
40+ displayName : 'Disable waiting for child workflow(s) to finish?' ,
41+ name : 'doNotWaitToFinish' ,
42+ type : 'boolean' ,
43+ default : false ,
44+ description :
45+ '⚠️ Advanced: If enabled, the parent will not wait for results from the sub-workflow. This may break downstream logic or lose returned data.' ,
46+ } ,
3347 ] ,
3448 } ;
3549
3650 async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [ ] [ ] > {
37- // 1) Pull in the incoming items
38- const items = this . getInputData ( ) ;
51+ const inputItems = this . getInputData ( ) ;
52+ const executeIndividually = this . getNodeParameter ( 'executeIndividually' , 0 ) as boolean ;
53+ const doNotWaitToFinish = this . getNodeParameter ( 'doNotWaitToFinish' , 0 ) as boolean ;
3954
40- // 2) Get the user-provided node JSON (string or object)
4155 const rawParam = this . getNodeParameter ( 'nodeJson' , 0 ) as any ;
4256 let raw : any ;
4357 if ( typeof rawParam === 'string' ) {
4458 try {
4559 raw = JSON . parse ( rawParam ) ;
4660 } catch {
47- throw new NodeOperationError (
48- this . getNode ( ) ,
49- 'Node JSON must be a valid JSON object or a parseable JSON string' ,
50- ) ;
61+ throw new NodeOperationError ( this . getNode ( ) , 'Node JSON must be valid JSON' ) ;
5162 }
5263 } else {
5364 raw = rawParam ;
5465 }
66+
5567 if ( typeof raw !== 'object' || raw === null ) {
5668 throw new NodeOperationError ( this . getNode ( ) , 'Node JSON must be an object' ) ;
5769 }
5870
59- // —— UNWRAP & CLEANUP ——
60- // 3) If it’s a full export, pull out the first node
61- let nodeJson : any ;
62- if ( Array . isArray ( raw . nodes ) && raw . nodes . length > 0 ) {
63- nodeJson = raw . nodes [ 0 ] ;
64- } else {
65- nodeJson = raw ;
66- }
67- // 4) Remove export-only keys
68- delete nodeJson . connections ;
69- delete nodeJson . pinData ;
70- delete nodeJson . meta ;
71- // 5) Validate it still has a name
72- if ( ! nodeJson . name ) {
71+ const baseNode = Array . isArray ( raw . nodes ) && raw . nodes . length > 0 ? raw . nodes [ 0 ] : raw ;
72+ delete baseNode . connections ;
73+ delete baseNode . pinData ;
74+ delete baseNode . meta ;
75+
76+ if ( ! baseNode . name ) {
7377 throw new NodeOperationError ( this . getNode ( ) , 'Your JSON must include a `name` field' ) ;
7478 }
7579
76- // —— AVOID COLLISIONS ——
77- // 6) Suffix the name and assign a fresh ID
78- nodeJson . name = `${ nodeJson . name } - Dynamic Node` ;
79- nodeJson . id = `dynamic-${ uuidv4 ( ) } ` ;
80+ const allResults : INodeExecutionData [ ] = [ ] ;
8081
81- // —— DEFAULT POSITION ——
82- // 7) Ensure a valid position array
83- if (
84- ! nodeJson . position ||
85- ! Array . isArray ( nodeJson . position ) ||
86- nodeJson . position . length !== 2 ||
87- typeof nodeJson . position [ 0 ] !== 'number' ||
88- typeof nodeJson . position [ 1 ] !== 'number'
89- ) {
90- nodeJson . position = [ 240 , 0 ] ;
91- }
82+ const processItem = async ( item : INodeExecutionData , index : number ) : Promise < void > => {
83+ const template = JSON . parse ( JSON . stringify ( subWorkflowTemplate ) ) ;
84+ const nodeClone = JSON . parse ( JSON . stringify ( baseNode ) ) ;
9285
93- // 8) Clone the sub-workflow template
94- const template = JSON . parse ( JSON . stringify ( subWorkflowTemplate ) ) as any ;
86+ nodeClone . name = `${ baseNode . name } - Dynamic Node [${ index + 1 } ]` ;
87+ nodeClone . id = `dynamic-${ uuidv4 ( ) } ` ;
88+ nodeClone . position = Array . isArray ( baseNode . position ) && baseNode . position . length === 2
89+ ? baseNode . position
90+ : [ 240 , 0 ] ;
9591
96- // 9) Inject & wire
97- template . nodes . push ( nodeJson ) ;
98- template . connections . Start . main [ 0 ] [ 0 ] . node = nodeJson . name ;
92+ template . nodes . push ( nodeClone ) ;
93+ template . connections . Start . main [ 0 ] [ 0 ] . node = nodeClone . name ;
9994
100- // 10) Execute the mini-workflow to completion
101- const workflowProxy = this . getWorkflowDataProxy ( 0 ) ;
102- const executionResult : any = await this . executeWorkflow (
103- { code : template } ,
104- items ,
105- { } ,
106- {
107- parentExecution : {
108- executionId : workflowProxy . $execution . id ,
109- workflowId : workflowProxy . $workflow . id ,
95+ const workflowProxy = this . getWorkflowDataProxy ( index ) ;
96+
97+ const execResult = await this . executeWorkflow (
98+ { code : template } ,
99+ [ item ] ,
100+ { } ,
101+ {
102+ parentExecution : {
103+ executionId : workflowProxy . $execution . id ,
104+ workflowId : workflowProxy . $workflow . id ,
105+ } ,
106+ doNotWaitToFinish,
110107 } ,
111- doNotWaitToFinish : false ,
112- } ,
113- ) ;
108+ ) ;
114109
115- // 11) Process executionResult
116- let returnedData : INodeExecutionData [ ] [ ] = [ ] ;
117- if ( Array . isArray ( executionResult ) ) {
118- returnedData = executionResult as INodeExecutionData [ ] [ ] ;
119- } else if ( executionResult && typeof executionResult === 'object' && 'data' in executionResult ) {
120- if ( Array . isArray ( ( executionResult as any ) . data ) ) {
121- returnedData = ( executionResult as any ) . data as INodeExecutionData [ ] [ ] ;
122- } else {
123- this . logger . warn ( 'DynamicNode: Sub-workflow executionResult.data was not an array. Returning empty data.' ) ;
124- returnedData = [ ] ;
125- }
126- } else if ( executionResult === null || executionResult === undefined ) {
127- this . logger . warn ( 'DynamicNode: Sub-workflow executionResult was null or undefined. Returning empty data.' ) ;
128- returnedData = [ ] ;
129- } else {
130- // Catch-all for other unexpected structures from executeWorkflow
131- this . logger . warn ( `DynamicNode: Unexpected structure from sub-workflow execution. Type: ${ typeof executionResult } . Returning empty data.` ) ;
132- returnedData = [ ] ;
133- }
110+ if ( ! doNotWaitToFinish && execResult ) {
111+ if ( Array . isArray ( execResult ) ) {
112+ allResults . push ( ...execResult . flat ( ) ) ;
113+ } else if ( typeof execResult === 'object' && 'data' in execResult && Array . isArray ( execResult . data ) ) {
114+ allResults . push ( ...execResult . data . flat ( ) ) ;
115+ }
116+ }
117+ } ;
118+
119+ if ( executeIndividually ) {
120+ for ( let i = 0 ; i < inputItems . length ; i ++ ) {
121+ try {
122+ await processItem ( inputItems [ i ] , i ) ;
123+ } catch ( err ) {
124+ this . logger . warn ( `DynamicNode: Error processing item #${ i + 1 } : ${ err . message } ` ) ;
125+ }
126+ }
127+ } else {
128+ // Run one sub-workflow with all items
129+ await processItem ( { json : { } } , 0 ) ;
130+ }
134131
135- return returnedData ;
132+ return [ allResults ] ;
136133 }
137134}
0 commit comments