fix(core): Only resume waiting parent workflows (#20342)

This commit is contained in:
mfsiega
2025-10-07 11:58:04 +02:00
committed by GitHub
parent 8f7f48043b
commit bebccfdb93
19 changed files with 545 additions and 80 deletions
+98 -51
View File
@@ -145,64 +145,111 @@ describe('WaitTracker', () => {
);
});
it('should also resume parent execution once sub-workflow finishes', async () => {
const parentExecution = mock<IExecutionResponse>({
id: 'parent_execution_id',
finished: false,
});
parentExecution.workflowData = mock<IWorkflowBase>({ id: 'parent_workflow_id' });
execution.data.parentExecution = {
executionId: parentExecution.id,
workflowId: parentExecution.workflowData.id,
describe('parent execution restart behavior', () => {
const setupParentExecutionTest = (shouldResume: boolean | undefined) => {
const parentExecution = mock<IExecutionResponse>({
id: 'parent_execution_id',
finished: false,
});
parentExecution.workflowData = mock<IWorkflowBase>({ id: 'parent_workflow_id' });
execution.data.parentExecution = {
executionId: parentExecution.id,
workflowId: parentExecution.workflowData.id,
shouldResume,
};
executionRepository.findSingleExecution
.calledWith(parentExecution.id)
.mockResolvedValue(parentExecution);
const postExecutePromise = createDeferredPromise<IRun | undefined>();
activeExecutions.getPostExecutePromise
.calledWith(execution.id)
.mockReturnValue(postExecutePromise.promise);
return { parentExecution, postExecutePromise };
};
executionRepository.findSingleExecution
.calledWith(parentExecution.id)
.mockResolvedValue(parentExecution);
const postExecutePromise = createDeferredPromise<IRun | undefined>();
activeExecutions.getPostExecutePromise
.calledWith(execution.id)
.mockReturnValue(postExecutePromise.promise);
await waitTracker.startExecution(execution.id);
it('should resume parent execution once sub-workflow finishes by default', async () => {
const { parentExecution, postExecutePromise } = setupParentExecutionTest(undefined);
expect(executionRepository.findSingleExecution).toHaveBeenNthCalledWith(1, execution.id, {
includeData: true,
unflattenData: true,
await waitTracker.startExecution(execution.id);
expect(executionRepository.findSingleExecution).toHaveBeenNthCalledWith(1, execution.id, {
includeData: true,
unflattenData: true,
});
expect(workflowRunner.run).toHaveBeenCalledTimes(1);
expect(workflowRunner.run).toHaveBeenNthCalledWith(
1,
{
executionMode: execution.mode,
executionData: execution.data,
workflowData: execution.workflowData,
projectId: project.id,
pushRef: execution.data.pushRef,
},
false,
false,
execution.id,
);
postExecutePromise.resolve(mock<IRun>());
await jest.advanceTimersByTimeAsync(100);
expect(workflowRunner.run).toHaveBeenCalledTimes(2);
expect(workflowRunner.run).toHaveBeenNthCalledWith(
2,
{
executionMode: parentExecution.mode,
executionData: parentExecution.data,
workflowData: parentExecution.workflowData,
projectId: project.id,
pushRef: parentExecution.data.pushRef,
startedAt: parentExecution.startedAt,
},
false,
false,
parentExecution.id,
);
});
expect(workflowRunner.run).toHaveBeenCalledTimes(1);
expect(workflowRunner.run).toHaveBeenNthCalledWith(
1,
{
executionMode: execution.mode,
executionData: execution.data,
workflowData: execution.workflowData,
projectId: project.id,
pushRef: execution.data.pushRef,
},
false,
false,
execution.id,
);
it('should not resume parent execution when shouldResume is false', async () => {
const { postExecutePromise } = setupParentExecutionTest(false);
postExecutePromise.resolve(mock<IRun>());
await jest.advanceTimersByTimeAsync(100);
await waitTracker.startExecution(execution.id);
expect(workflowRunner.run).toHaveBeenCalledTimes(2);
expect(workflowRunner.run).toHaveBeenNthCalledWith(
2,
{
executionMode: parentExecution.mode,
executionData: parentExecution.data,
workflowData: parentExecution.workflowData,
projectId: project.id,
pushRef: parentExecution.data.pushRef,
startedAt: parentExecution.startedAt,
},
false,
false,
parentExecution.id,
);
expect(workflowRunner.run).toHaveBeenCalledTimes(1);
postExecutePromise.resolve(mock<IRun>());
await jest.advanceTimersByTimeAsync(100);
// Parent execution should NOT be started
expect(workflowRunner.run).toHaveBeenCalledTimes(1);
});
it('should resume parent execution when shouldResume is true', async () => {
const { parentExecution, postExecutePromise } = setupParentExecutionTest(true);
await waitTracker.startExecution(execution.id);
expect(workflowRunner.run).toHaveBeenCalledTimes(1);
postExecutePromise.resolve(mock<IRun>());
await jest.advanceTimersByTimeAsync(100);
// Parent execution SHOULD be started
expect(workflowRunner.run).toHaveBeenCalledTimes(2);
expect(workflowRunner.run).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
executionMode: parentExecution.mode,
projectId: project.id,
}),
false,
false,
parentExecution.id,
);
});
});
});
@@ -4,6 +4,7 @@ import type { Project, Variables } from '@n8n/db';
import { VariablesService } from '@/environments.ee/variables/variables.service.ee';
import { OwnershipService } from '@/services/ownership.service';
import { getVariables } from '@/workflow-helpers';
import { shouldRestartParentExecution } from '@/workflow-helpers';
describe('workflow-helpers', () => {
beforeAll(() => {
@@ -63,3 +64,36 @@ describe('workflow-helpers', () => {
});
});
});
describe('shouldRestartParentExecution', () => {
it('should return false when parentExecution is undefined', () => {
expect(shouldRestartParentExecution(undefined)).toBe(false);
});
it('should return false when shouldResume is explicitly false', () => {
const parentExecution = {
executionId: 'parent-exec-id',
workflowId: 'parent-workflow-id',
shouldResume: false,
};
expect(shouldRestartParentExecution(parentExecution)).toBe(false);
});
it('should return true when shouldResume is undefined', () => {
const parentExecution = {
executionId: 'parent-exec-id',
workflowId: 'parent-workflow-id',
shouldResume: undefined,
};
expect(shouldRestartParentExecution(parentExecution)).toBe(true);
});
it('should return true when shouldResume is true', () => {
const parentExecution = {
executionId: 'parent-exec-id',
workflowId: 'parent-workflow-id',
shouldResume: true,
};
expect(shouldRestartParentExecution(parentExecution)).toBe(true);
});
});
+2 -1
View File
@@ -8,6 +8,7 @@ import { UnexpectedError, type IWorkflowExecutionDataProcess } from 'n8n-workflo
import { ActiveExecutions } from '@/active-executions';
import { OwnershipService } from '@/services/ownership.service';
import { WorkflowRunner } from '@/workflow-runner';
import { shouldRestartParentExecution } from './workflow-helpers';
@Service()
export class WaitTracker {
@@ -126,7 +127,7 @@ export class WaitTracker {
await this.workflowRunner.run(data, false, false, executionId);
const { parentExecution } = fullExecutionData.data;
if (parentExecution) {
if (shouldRestartParentExecution(parentExecution)) {
// on child execution completion, resume parent execution
void this.activeExecutions.getPostExecutePromise(executionId).then(() => {
void this.startExecution(parentExecution.executionId);
+1 -1
View File
@@ -650,7 +650,7 @@ export async function executeWebhook(
const executePromise = activeExecutions.getPostExecutePromise(executionId);
const { parentExecution } = runExecutionData;
if (parentExecution) {
if (WorkflowHelpers.shouldRestartParentExecution(parentExecution)) {
// on child execution completion, resume parent execution
void executePromise.then(() => {
const waitTracker = Container.get(WaitTracker);
+19
View File
@@ -6,6 +6,7 @@ import type {
IRun,
ITaskData,
IWorkflowBase,
RelatedExecution,
} from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
@@ -196,3 +197,21 @@ export async function getVariables(workflowId?: string, projectId?: string): Pro
}, {} as IDataObject),
);
}
/**
* Determines if a parent execution should be restarted when a child execution completes.
*
* @param parentExecution - The parent execution metadata, if any
* @returns true if the parent should be restarted, false otherwise
*/
export function shouldRestartParentExecution(
parentExecution: RelatedExecution | undefined,
): parentExecution is RelatedExecution {
if (parentExecution === undefined) {
return false;
}
if (parentExecution.shouldResume === undefined) {
return true; // Preserve existing behavior for executions started before the flag was introduced for backward compatibility.
}
return parentExecution.shouldResume;
}
@@ -32,8 +32,9 @@ describe('ExecuteWorkflow', () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema
.mockReturnValueOnce({}) // workflowInputs.value
.mockReturnValueOnce([]) // workflowInputs.schema
.mockReturnValueOnce(true); // waitForSubWorkflow
executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]);
executeFunctions.getWorkflowDataProxy.mockReturnValue({
@@ -51,8 +52,7 @@ describe('ExecuteWorkflow', () => {
expect(result).toEqual([
[
{
json: { key: 'value' },
index: 0,
json: { key: 'subValue' },
pairedItem: { item: 0 },
metadata: {
subExecution: { workflowId: 'subWorkflowId', executionId: 'subExecutionId' },
@@ -60,16 +60,32 @@ describe('ExecuteWorkflow', () => {
},
],
]);
// Verify shouldResume is set correctly
expect(executeFunctions.executeWorkflow).toHaveBeenCalledWith(
{ id: 'subWorkflowId' },
[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 }, binary: undefined }],
undefined,
{
parentExecution: {
executionId: 'executionId',
workflowId: 'workflowId',
shouldResume: true,
},
},
);
});
test('should execute workflow in "once" mode and not wait for sub-workflow completion', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('once') // mode
.mockReturnValueOnce(false) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema
.mockReturnValueOnce({}) // workflowInputs.value
.mockReturnValueOnce([]) // workflowInputs.schema
.mockReturnValueOnce(false); // waitForSubWorkflow
executeFunctions.getInputData.mockReturnValue([{ json: { key: 'value' } }]);
(getWorkflowInfo as jest.Mock).mockResolvedValue({ id: 'subWorkflowId' });
executeFunctions.executeWorkflow.mockResolvedValue({
executionId: 'subExecutionId',
@@ -78,15 +94,33 @@ describe('ExecuteWorkflow', () => {
const result = await executeWorkflow.execute.call(executeFunctions);
expect(result).toEqual([[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 } }]]);
expect(result).toEqual([
[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 }, binary: undefined }],
]);
// Verify shouldResume is set to false
expect(executeFunctions.executeWorkflow).toHaveBeenCalledWith(
{ id: 'subWorkflowId' },
[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 }, binary: undefined }],
undefined,
{
doNotWaitToFinish: true,
parentExecution: {
executionId: 'executionId',
workflowId: 'workflowId',
shouldResume: false,
},
},
);
});
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
.mockReturnValueOnce({}) // workflowInputs.value
.mockReturnValueOnce([]) // workflowInputs.schema
.mockReturnValueOnce(true); // waitForSubWorkflow
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode);
@@ -102,8 +136,13 @@ describe('ExecuteWorkflow', () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValue([]); // workflowInputs.schema
.mockReturnValueOnce({}) // workflowInputs.value (item 0)
.mockReturnValueOnce({}) // workflowInputs.value (item 1)
.mockReturnValueOnce({}) // workflowInputs.value (item 2)
.mockReturnValueOnce([]) // workflowInputs.schema
.mockReturnValueOnce(true) // waitForSubWorkflow (item 0)
.mockReturnValueOnce(true) // waitForSubWorkflow (item 1)
.mockReturnValueOnce(true); // waitForSubWorkflow (item 2)
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode);
executeFunctions.getInputData.mockReturnValueOnce([
@@ -128,8 +167,9 @@ describe('ExecuteWorkflow', () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema
.mockReturnValueOnce({}) // workflowInputs.value
.mockReturnValueOnce([]) // workflowInputs.schema
.mockReturnValueOnce(true); // waitForSubWorkflow
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode);
@@ -145,8 +185,13 @@ describe('ExecuteWorkflow', () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema
.mockReturnValueOnce({}) // workflowInputs.value (item 0)
.mockReturnValueOnce({}) // workflowInputs.value (item 1)
.mockReturnValueOnce({}) // workflowInputs.value (item 2)
.mockReturnValueOnce([]) // workflowInputs.schema
.mockReturnValueOnce(true) // waitForSubWorkflow (item 0)
.mockReturnValueOnce(true) // waitForSubWorkflow (item 1)
.mockReturnValueOnce(true); // waitForSubWorkflow (item 2)
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode);
executeFunctions.getInputData.mockReturnValueOnce([
@@ -173,8 +218,9 @@ describe('ExecuteWorkflow', () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema
.mockReturnValueOnce({}) // workflowInputs.value
.mockReturnValueOnce([]) // workflowInputs.schema
.mockReturnValueOnce(true); // waitForSubWorkflow
(getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error'));
(executeFunctions.continueOnFail as jest.Mock).mockReturnValue(false);
@@ -317,6 +317,7 @@ export class ExecuteWorkflow implements INodeType {
parentExecution: {
executionId: workflowProxy.$execution.id,
workflowId: workflowProxy.$workflow.id,
shouldResume: waitForSubWorkflow,
},
},
);
@@ -349,6 +350,7 @@ export class ExecuteWorkflow implements INodeType {
parentExecution: {
executionId: workflowProxy.$execution.id,
workflowId: workflowProxy.$workflow.id,
shouldResume: waitForSubWorkflow,
},
},
);
@@ -416,6 +418,7 @@ export class ExecuteWorkflow implements INodeType {
parentExecution: {
executionId: workflowProxy.$execution.id,
workflowId: workflowProxy.$workflow.id,
shouldResume: waitForSubWorkflow,
},
},
);
@@ -58,7 +58,7 @@ export class TestEntryComposer {
* Returns the workflow import result for use in the test
*/
async fromImportedWorkflow(workflowFile: string) {
const workflowImportResult = await this.n8n.api.workflows.importWorkflow(workflowFile);
const workflowImportResult = await this.n8n.api.workflows.importWorkflowFromFile(workflowFile);
await this.n8n.page.goto(`workflow/${workflowImportResult.workflowId}`);
return workflowImportResult;
}
@@ -155,14 +155,21 @@ export class WorkflowApiHelper {
* The workflow will be created with its original active state from the JSON file.
* Returns detailed information about what was imported, including webhook info if present.
*/
async importWorkflow(
async importWorkflowFromFile(
fileName: string,
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
): Promise<WorkflowImportResult> {
const workflowDefinition: IWorkflowBase = JSON.parse(
readFileSync(resolveFromRoot('workflows', fileName), 'utf8'),
);
const filePath = resolveFromRoot('workflows', fileName);
const fileContent = readFileSync(filePath, 'utf8');
const workflowDefinition = JSON.parse(fileContent) as IWorkflowBase;
return await this.importWorkflowFromDefinition(workflowDefinition, options);
}
async importWorkflowFromDefinition(
workflowDefinition: IWorkflowBase,
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
): Promise<WorkflowImportResult> {
const result = await this.createWorkflowFromDefinition(workflowDefinition, options);
// Ensure the workflow is in the correct active state as specified in the JSON
@@ -177,7 +184,6 @@ export class WorkflowApiHelper {
const params = new URLSearchParams();
if (workflowId) params.set('workflowId', workflowId);
params.set('limit', limit.toString());
const response = await this.api.request.get('/rest/executions', { params });
if (!response.ok()) {
@@ -14,7 +14,7 @@ test.describe('Workflow Selector Parameter @db:reset', () => {
];
for (const { file } of subWorkflows) {
await n8n.api.workflows.importWorkflow(file);
await n8n.api.workflows.importWorkflowFromFile(file);
}
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
@@ -0,0 +1,78 @@
import { readFileSync } from 'fs';
import type { IWorkflowBase } from 'n8n-workflow';
import { test, expect } from '../../fixtures/base';
import { resolveFromRoot } from '../../utils/path-helper';
import { retryUntil } from '../../utils/retry-utils';
test.describe('Parent that does not wait for sub-workflow', () => {
test('should not wait for the sub-workflow', async ({ api }) => {
const childWorkflowId = (
await api.workflows.importWorkflowFromFile('subworkflow-wait-child.json')
).workflowId;
const filePath = resolveFromRoot('workflows', 'subworkflow-parent-no-wait.json');
const fileContent = readFileSync(filePath, 'utf8');
const workflowDefinition = JSON.parse(fileContent) as IWorkflowBase;
expect(workflowDefinition?.nodes[0]?.parameters).toBeDefined();
// Replace the placeholder workflow ID with the actual child workflow ID
workflowDefinition.nodes[0].parameters.workflowId = {
value: childWorkflowId,
mode: 'list',
};
const { webhookPath, workflowId } =
await api.workflows.importWorkflowFromDefinition(workflowDefinition);
const response = await api.request.get(`/webhook/${webhookPath}`);
expect(response.ok()).toBe(true);
const execution = await api.workflows.waitForExecution(workflowId, 5000);
expect(execution.status).toBe('success');
// The child workflow should still be running or waiting, since it's configured to wait 120s.
const getExecutionsResponse = await api.workflows.getExecutions(childWorkflowId);
// TODO: figure out why the filtering in `getExecutions` isn't working.
const childExecutions = getExecutionsResponse.filter((e) => e.workflowId === childWorkflowId);
expect(childExecutions.length).toBe(1);
expect(childExecutions[0].status).toMatch(/running|waiting/);
});
test('CAT-1445 should not be restarted by the child workflow finishing', async ({ api }) => {
// The child is a no-op that returns immediately.
const childWorkflowId = (
await api.workflows.importWorkflowFromFile('subworkflow-noop-child.json')
).workflowId;
// This is a parent that does NOT wait for the child to finish, but it has its own separate Wait node.
// We want to verify that the parent is NOT restarted when the child finishes. This was fixed in CAT-1445.
const filePath = resolveFromRoot('workflows', 'subworkflow-waiting-parent-no-child-wait.json');
const fileContent = readFileSync(filePath, 'utf8');
const workflowDefinition = JSON.parse(fileContent) as IWorkflowBase;
expect(workflowDefinition?.nodes[1]?.parameters).toBeDefined();
// Replace the placeholder workflow ID with the actual child workflow ID
workflowDefinition.nodes[1].parameters.workflowId = {
value: childWorkflowId,
mode: 'list',
};
const { webhookPath, workflowId } =
await api.workflows.importWorkflowFromDefinition(workflowDefinition);
const response = await api.request.get(`/webhook/${webhookPath}`);
expect(response.ok()).toBe(true);
// First, wait for the child to finish.
const childExecution = await api.workflows.waitForExecution(childWorkflowId, 5000);
expect(childExecution.status).toBe('success');
// Verify that the parent didn't get resumed. We might need to give it a moment to reach the waiting state.
await retryUntil(
async () => {
const getExecutionsResponse = await api.workflows.getExecutions(workflowId);
// TODO: figure out why the filtering in `getExecutions` isn't working.
const parentExecutions = getExecutionsResponse.filter((e) => e.workflowId === workflowId);
expect(parentExecutions.length).toBe(1);
expect(parentExecutions[0].status).toBe('waiting');
},
{ timeoutMs: 2000, intervalMs: 100 },
);
});
});
@@ -4,7 +4,7 @@ test.describe('External Webhook Triggering', () => {
test('should create workflow via API, activate it, trigger webhook externally, and verify execution', async ({
api,
}) => {
const { webhookPath, workflowId } = await api.workflows.importWorkflow(
const { webhookPath, workflowId } = await api.workflows.importWorkflowFromFile(
'simple-webhook-test.json',
);
@@ -24,7 +24,7 @@ test.describe('External Webhook Triggering', () => {
});
test('should surface workflow configuration errors to the caller', async ({ api }) => {
const { webhookPath } = await api.workflows.importWorkflow(
const { webhookPath } = await api.workflows.importWorkflowFromFile(
'webhook-misconfiguration-test.json',
);
@@ -2,7 +2,9 @@ import { test, expect } from '../../fixtures/base';
test.describe('Webhook Origin Isolation', () => {
test.beforeAll(async ({ api }) => {
await api.workflows.importWorkflow('webhook-origin-isolation.json', { makeUnique: false });
await api.workflows.importWorkflowFromFile('webhook-origin-isolation.json', {
makeUnique: false,
});
});
const webhookPaths = [
@@ -0,0 +1,32 @@
/**
* Retries the given assertion until it passes or the timeout is reached
*
* @example
* await retryUntil(
* () => expect(service.someState).toBe(true)
* );
*/
export const retryUntil = async (
assertion: () => Promise<void> | void,
{ intervalMs = 200, timeoutMs = 5000 } = {},
) => {
return await new Promise((resolve, reject) => {
const startTime = Date.now();
const tryAgain = () => {
setTimeout(async () => {
try {
resolve(await assertion());
} catch (error) {
if (Date.now() - startTime > timeoutMs) {
reject(error instanceof Error ? error : new Error(String(error)));
} else {
tryAgain();
}
}
}, intervalMs);
};
tryAgain();
});
};
@@ -0,0 +1,21 @@
{
"nodes": [
{
"parameters": {
"inputSource": "passthrough"
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [0, 0],
"id": "e3fa278e-1cb1-48fb-88e4-301c7d04d22c",
"name": "When Executed by Another Workflow"
}
],
"connections": {
"When Executed by Another Workflow": {
"main": [[]]
}
},
"pinData": {},
"meta": {}
}
@@ -0,0 +1,55 @@
{
"active": true,
"nodes": [
{
"parameters": {
"workflowId": {
"value": "PLACEHOLDER_SUBWORKFLOW_ID_TO_BE_REPLACED",
"mode": "list"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {}
},
"options": {
"waitForSubWorkflow": false
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [208, 0],
"id": "c27d9002-4bbe-4c13-b1a0-728b77a6ff98",
"name": "Call Sub-workflow"
},
{
"parameters": {
"path": "1eef9e1a-1639-48cb-a6f0-69ce704ddb35",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [0, 0],
"id": "ab4ea759-d26b-4b1b-8c66-89252df1a56a",
"name": "Webhook",
"webhookId": "1eef9e1a-1639-48cb-a6f0-69ce704ddb35"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Call Sub-workflow",
"type": "main",
"index": 0
}
]
]
},
"Call Sub-workflow": {
"main": [[]]
}
},
"pinData": {},
"meta": {}
}
@@ -0,0 +1,45 @@
{
"active": true,
"nodes": [
{
"parameters": {
"inputSource": "passthrough"
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [0, 0],
"id": "e3fa278e-1cb1-48fb-88e4-301c7d04d22c",
"name": "When Executed by Another Workflow"
},
{
"parameters": {
"amount": 120
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [224, 0],
"id": "1d7ee221-49ac-4744-970e-3ba78220b455",
"name": "Wait"
}
],
"connections": {
"When Executed by Another Workflow": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [[]]
}
},
"pinData": {},
"meta": {
"instanceId": "c0eda2ee62dc0e7e8288317ffba83079dda1ea0d80d2eaa966e6d217327a763e"
}
}
@@ -0,0 +1,74 @@
{
"active": true,
"nodes": [
{
"parameters": {
"path": "1eef9e1a-1639-48cb-a6f0-69ce704ddb35",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [0, 0],
"id": "ab4ea759-d26b-4b1b-8c66-89252df1a56a",
"name": "Webhook",
"webhookId": "1eef9e1a-1639-48cb-a6f0-69ce704ddb35"
},
{
"parameters": {
"workflowId": {
"value": "YJLfzxxKx2YBUjzH",
"mode": "list"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {}
},
"options": {
"waitForSubWorkflow": false
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [272, 0],
"id": "c27d9002-4bbe-4c13-b1a0-728b77a6ff98",
"name": "Call Sub-workflow"
},
{
"parameters": {
"amount": 120
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [480, 0],
"id": "273af435-67ac-419d-9208-26a6c83440bd",
"name": "Wait",
"webhookId": "1de20343-d792-49e1-922f-22a5364424a2"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Call Sub-workflow",
"type": "main",
"index": 0
}
]
]
},
"Call Sub-workflow": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {}
}
+2
View File
@@ -2446,6 +2446,8 @@ export interface ITaskSubRunMetadata {
export interface RelatedExecution {
executionId: string;
workflowId: string;
// In the case of a parent execution, whether the parent should be resumed when the sub execution finishes.
shouldResume?: boolean;
}
type SubNodeExecutionDataAction = {