diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts index f0352c9a31..df4fe3e933 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.test.ts @@ -1,5 +1,5 @@ import { mock } from 'jest-mock-extended'; -import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow'; +import type { IExecuteFunctions, IWorkflowDataProxyData, INode } from 'n8n-workflow'; import { ExecuteWorkflow } from './ExecuteWorkflow.node'; import { getWorkflowInfo } from './GenericFunctions'; @@ -81,13 +81,15 @@ describe('ExecuteWorkflow', () => { expect(result).toEqual([[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 } }]]); }); - test('should handle errors and continue on fail', async () => { + test('should handle errors and continue on fail, no items, < 1.3 version', async () => { executeFunctions.getNodeParameter .mockReturnValueOnce('database') // source .mockReturnValueOnce('each') // mode .mockReturnValueOnce(true) // waitForSubWorkflow .mockReturnValueOnce([]); // workflowInputs.schema + executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode); + (getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error')); (executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true); @@ -96,6 +98,77 @@ describe('ExecuteWorkflow', () => { expect(result).toEqual([[{ json: { error: 'Test error' }, pairedItem: { item: 0 } }]]); }); + test('should handle errors and continue on fail, multiple items, < 1.3 version', async () => { + executeFunctions.getNodeParameter + .mockReturnValueOnce('database') // source + .mockReturnValueOnce('each') // mode + .mockReturnValueOnce(true) // waitForSubWorkflow + .mockReturnValue([]); // workflowInputs.schema + + executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode); + executeFunctions.getInputData.mockReturnValueOnce([ + { json: { key: '1' } }, + { json: { key: '2' } }, + { json: { key: '3' } }, + ]); + + (getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error')); + (executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true); + + const result = await executeWorkflow.execute.call(executeFunctions); + + expect(result).toEqual([ + [{ json: { error: 'Test error' }, pairedItem: { item: 0 }, metadata: undefined }], + [{ json: { error: 'Test error' }, pairedItem: { item: 1 }, metadata: undefined }], + [{ json: { error: 'Test error' }, pairedItem: { item: 2 }, metadata: undefined }], + ]); + }); + + test('should handle errors and continue on fail, no items, >= 1.3 version', async () => { + executeFunctions.getNodeParameter + .mockReturnValueOnce('database') // source + .mockReturnValueOnce('each') // mode + .mockReturnValueOnce(true) // waitForSubWorkflow + .mockReturnValueOnce([]); // workflowInputs.schema + + executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode); + + (getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error')); + (executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true); + + const result = await executeWorkflow.execute.call(executeFunctions); + + expect(result).toEqual([[{ json: { error: 'Test error' }, pairedItem: { item: 0 } }]]); + }); + + test('should handle errors and continue on fail, multiple items, >= 1.3 version', async () => { + executeFunctions.getNodeParameter + .mockReturnValueOnce('database') // source + .mockReturnValueOnce('each') // mode + .mockReturnValueOnce(true) // waitForSubWorkflow + .mockReturnValueOnce([]); // workflowInputs.schema + + executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode); + executeFunctions.getInputData.mockReturnValueOnce([ + { json: { key: '1' } }, + { json: { key: '2' } }, + { json: { key: '3' } }, + ]); + + (getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error')); + (executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true); + + const result = await executeWorkflow.execute.call(executeFunctions); + + expect(result).toEqual([ + [ + { json: { error: 'Test error' }, pairedItem: { item: 0 }, metadata: undefined }, + { json: { error: 'Test error' }, pairedItem: { item: 1 }, metadata: undefined }, + { json: { error: 'Test error' }, pairedItem: { item: 2 }, metadata: undefined }, + ], + ]); + }); + test('should throw error if not continuing on fail', async () => { executeFunctions.getNodeParameter .mockReturnValueOnce('database') // source diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts index fd09b0df4c..c45b15fef2 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts @@ -20,7 +20,7 @@ export class ExecuteWorkflow implements INodeType { icon: 'fa:sign-in-alt', iconColor: 'orange-red', group: ['transform'], - version: [1, 1.1, 1.2], + version: [1, 1.1, 1.2, 1.3], subtitle: '={{"Workflow: " + $parameter["workflowId"]}}', description: 'Execute another workflow', defaults: { @@ -369,11 +369,16 @@ export class ExecuteWorkflow implements INodeType { } } catch (error) { if (this.continueOnFail()) { - if (returnData[i] === undefined) { - returnData[i] = []; - } + const nodeVersion = this.getNode().typeVersion; + // In versions < 1.3 using the "Continue (using error output)" mode + // the node would return items in extra "error branches" instead of + // returning an array of items on the error output. These branches weren't really shown correctly on the UI. + // In the fixed >= 1.3 versions the errors are now all output into the single error output as an array of error items. + const outputIndex = nodeVersion >= 1.3 ? 0 : i; + + returnData[outputIndex] ??= []; const metadata = parseErrorMetadata(error); - returnData[i].push({ + returnData[outputIndex].push({ json: { error: error.message }, pairedItem: { item: i }, metadata,