import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import type {
ExecuteWorkflowData,
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { generatePairedItemData } from '../../utils/utilities';
import { getWorkflowInfo } from './GenericFunctions';
export class ExecuteWorkflow implements INodeType {
description: INodeTypeDescription = {
displayName: 'Execute Workflow',
name: 'executeWorkflow',
icon: 'fa:sign-in-alt',
iconColor: 'orange-red',
group: ['transform'],
version: [1, 1.1],
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
description: 'Execute another workflow',
defaults: {
name: 'Execute Workflow',
color: '#ff6d5a',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'hidden',
noDataExpression: true,
default: 'call_workflow',
options: [
{
name: 'Call Another Workflow',
value: 'call_workflow',
},
],
},
{
displayName: 'Source',
name: 'source',
type: 'options',
options: [
{
name: 'Database',
value: 'database',
description: 'Load the workflow from the database by ID',
},
{
name: 'Local File',
value: 'localFile',
description: 'Load the workflow from a locally saved file',
},
{
name: 'Parameter',
value: 'parameter',
description: 'Load the workflow from a parameter',
},
{
name: 'URL',
value: 'url',
description: 'Load the workflow from an URL',
},
],
default: 'database',
description: 'Where to get the workflow to execute from',
},
// ----------------------------------
// source:database
// ----------------------------------
{
displayName: 'Workflow ID',
name: 'workflowId',
type: 'string',
displayOptions: {
show: {
source: ['database'],
'@version': [1],
},
},
default: '',
required: true,
hint: 'Can be found in the URL of the workflow',
description:
"Note on using an expression here: if this node is set to run once with all items, they will all be sent to the same workflow. That workflow's ID will be calculated by evaluating the expression for the first input item.",
},
{
displayName: 'Workflow',
name: 'workflowId',
type: 'workflowSelector',
displayOptions: {
show: {
source: ['database'],
'@version': [{ _cnd: { gte: 1.1 } }],
},
},
default: '',
required: true,
hint: "Note on using an expression here: if this node is set to run once with all items, they will all be sent to the same workflow. That workflow's ID will be calculated by evaluating the expression for the first input item.",
},
// ----------------------------------
// source:localFile
// ----------------------------------
{
displayName: 'Workflow Path',
name: 'workflowPath',
type: 'string',
displayOptions: {
show: {
source: ['localFile'],
},
},
default: '',
placeholder: '/data/workflow.json',
required: true,
description: 'The path to local JSON workflow file to execute',
},
// ----------------------------------
// source:parameter
// ----------------------------------
{
displayName: 'Workflow JSON',
name: 'workflowJson',
type: 'json',
typeOptions: {
rows: 10,
},
displayOptions: {
show: {
source: ['parameter'],
},
},
default: '\n\n\n',
required: true,
description: 'The workflow JSON code to execute',
},
// ----------------------------------
// source:url
// ----------------------------------
{
displayName: 'Workflow URL',
name: 'workflowUrl',
type: 'string',
displayOptions: {
show: {
source: ['url'],
},
},
default: '',
placeholder: 'https://example.com/workflow.json',
required: true,
description: 'The URL from which to load the workflow from',
},
{
displayName:
'Any data you pass into this node will be output by the Execute Workflow Trigger. More info',
name: 'executeWorkflowNotice',
type: 'notice',
default: '',
},
{
displayName: 'Mode',
name: 'mode',
type: 'options',
noDataExpression: true,
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Run once with all items',
value: 'once',
description: 'Pass all items into a single execution of the sub-workflow',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Run once for each item',
value: 'each',
description: 'Call the sub-workflow individually for each item',
},
],
default: 'once',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add option',
options: [
{
displayName: 'Wait For Sub-Workflow Completion',
name: 'waitForSubWorkflow',
type: 'boolean',
default: true,
description:
'Whether the main workflow should wait for the sub-workflow to complete its execution before proceeding',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise {
const source = this.getNodeParameter('source', 0) as string;
const mode = this.getNodeParameter('mode', 0, false) as string;
const items = this.getInputData();
const workflowProxy = this.getWorkflowDataProxy(0);
const currentWorkflowId = workflowProxy.$workflow.id as string;
if (mode === 'each') {
const returnData: INodeExecutionData[][] = [];
for (let i = 0; i < items.length; i++) {
try {
const waitForSubWorkflow = this.getNodeParameter(
'options.waitForSubWorkflow',
i,
true,
) as boolean;
const workflowInfo = await getWorkflowInfo.call(this, source, i);
if (waitForSubWorkflow) {
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
workflowInfo,
[items[i]],
undefined,
{
parentExecution: {
executionId: workflowProxy.$execution.id,
workflowId: workflowProxy.$workflow.id,
},
},
);
const workflowResult = executionResult.data as INodeExecutionData[][];
for (const [outputIndex, outputData] of workflowResult.entries()) {
for (const item of outputData) {
item.pairedItem = { item: i };
item.metadata = {
subExecution: {
executionId: executionResult.executionId,
workflowId: workflowInfo.id ?? currentWorkflowId,
},
};
}
if (returnData[outputIndex] === undefined) {
returnData[outputIndex] = [];
}
returnData[outputIndex].push(...outputData);
}
} else {
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
workflowInfo,
[items[i]],
undefined,
{
doNotWaitToFinish: true,
parentExecution: {
executionId: workflowProxy.$execution.id,
workflowId: workflowProxy.$workflow.id,
},
},
);
if (returnData.length === 0) {
returnData.push([]);
}
returnData[0].push({
...items[i],
metadata: {
subExecution: {
workflowId: workflowInfo.id ?? currentWorkflowId,
executionId: executionResult.executionId,
},
},
});
}
} catch (error) {
if (this.continueOnFail()) {
if (returnData[i] === undefined) {
returnData[i] = [];
}
returnData[i].push({ json: { error: error.message }, pairedItem: { item: i } });
continue;
}
throw new NodeOperationError(this.getNode(), error, {
message: `Error executing workflow with item at index ${i}`,
description: error.message,
itemIndex: i,
});
}
}
this.setMetadata({
subExecutionsCount: items.length,
});
return returnData;
} else {
try {
const waitForSubWorkflow = this.getNodeParameter(
'options.waitForSubWorkflow',
0,
true,
) as boolean;
const workflowInfo = await getWorkflowInfo.call(this, source);
const executionResult: ExecuteWorkflowData = await this.executeWorkflow(
workflowInfo,
items,
undefined,
{
doNotWaitToFinish: !waitForSubWorkflow,
parentExecution: {
executionId: workflowProxy.$execution.id,
workflowId: workflowProxy.$workflow.id,
},
},
);
this.setMetadata({
subExecution: {
executionId: executionResult.executionId,
workflowId: workflowInfo.id ?? (workflowProxy.$workflow.id as string),
},
subExecutionsCount: 1,
});
if (!waitForSubWorkflow) {
return [items];
}
const workflowResult = executionResult.data as INodeExecutionData[][];
const fallbackPairedItemData = generatePairedItemData(items.length);
for (const output of workflowResult) {
const sameLength = output.length === items.length;
for (const [itemIndex, item] of output.entries()) {
if (item.pairedItem) continue;
if (sameLength) {
item.pairedItem = { item: itemIndex };
} else {
item.pairedItem = fallbackPairedItemData;
}
}
}
return workflowResult;
} catch (error) {
const pairedItem = generatePairedItemData(items.length);
if (this.continueOnFail()) {
return [[{ json: { error: error.message }, pairedItem }]];
}
throw error;
}
}
}
}