diff --git a/packages/@n8n/decorators/src/execution-lifecycle/__tests__/on-lifecycle-event.test.ts b/packages/@n8n/decorators/src/execution-lifecycle/__tests__/on-lifecycle-event.test.ts index 1c366c68e4f..f6bd8c6270d 100644 --- a/packages/@n8n/decorators/src/execution-lifecycle/__tests__/on-lifecycle-event.test.ts +++ b/packages/@n8n/decorators/src/execution-lifecycle/__tests__/on-lifecycle-event.test.ts @@ -43,9 +43,12 @@ describe('OnLifecycleEvent', () => { @OnLifecycleEvent('workflowExecuteAfter') async handleWorkflowExecuteAfter() {} + + @OnLifecycleEvent('workflowExecuteResume') + async handleWorkflowExecuteResume() {} } - expect(lifecycleMetadata.register).toHaveBeenCalledTimes(4); + expect(lifecycleMetadata.register).toHaveBeenCalledTimes(5); expect(lifecycleMetadata.register).toHaveBeenCalledWith( expect.objectContaining({ eventName: 'nodeExecuteBefore' }), ); @@ -58,6 +61,9 @@ describe('OnLifecycleEvent', () => { expect(lifecycleMetadata.register).toHaveBeenCalledWith( expect.objectContaining({ eventName: 'workflowExecuteAfter' }), ); + expect(lifecycleMetadata.register).toHaveBeenCalledWith( + expect.objectContaining({ eventName: 'workflowExecuteResume' }), + ); }); it('should register multiple handlers in the same class', () => { diff --git a/packages/@n8n/decorators/src/execution-lifecycle/index.ts b/packages/@n8n/decorators/src/execution-lifecycle/index.ts index f33d8597d38..abec0163a89 100644 --- a/packages/@n8n/decorators/src/execution-lifecycle/index.ts +++ b/packages/@n8n/decorators/src/execution-lifecycle/index.ts @@ -5,5 +5,6 @@ export type { NodeExecuteAfterContext, WorkflowExecuteBeforeContext, WorkflowExecuteAfterContext, + WorkflowExecuteResumeContext, } from './lifecycle-metadata'; export { LifecycleMetadata } from './lifecycle-metadata'; diff --git a/packages/@n8n/decorators/src/execution-lifecycle/lifecycle-metadata.ts b/packages/@n8n/decorators/src/execution-lifecycle/lifecycle-metadata.ts index b199575ae8b..9431abd40e2 100644 --- a/packages/@n8n/decorators/src/execution-lifecycle/lifecycle-metadata.ts +++ b/packages/@n8n/decorators/src/execution-lifecycle/lifecycle-metadata.ts @@ -35,6 +35,7 @@ export type WorkflowExecuteBeforeContext = { workflow: IWorkflowBase; workflowInstance: Workflow; executionData?: IRunExecutionData; + executionId: string; }; export type WorkflowExecuteAfterContext = { @@ -42,6 +43,15 @@ export type WorkflowExecuteAfterContext = { workflow: IWorkflowBase; runData: IRun; newStaticData: IDataObject; + executionId: string; +}; + +export type WorkflowExecuteResumeContext = { + type: 'workflowExecuteResume'; + workflow: IWorkflowBase; + workflowInstance: Workflow; + executionData: IRunExecutionData; + executionId: string; }; /** Context arg passed to a lifecycle event handler method. */ @@ -49,7 +59,8 @@ export type LifecycleContext = | NodeExecuteBeforeContext | NodeExecuteAfterContext | WorkflowExecuteBeforeContext - | WorkflowExecuteAfterContext; + | WorkflowExecuteAfterContext + | WorkflowExecuteResumeContext; type LifecycleHandler = { /** Class holding the method to call on a lifecycle event. */ diff --git a/packages/cli/src/execution-lifecycle/__tests__/execution-lifecycle-hooks.test.ts b/packages/cli/src/execution-lifecycle/__tests__/execution-lifecycle-hooks.test.ts index b24e5850617..1090c0cadb5 100644 --- a/packages/cli/src/execution-lifecycle/__tests__/execution-lifecycle-hooks.test.ts +++ b/packages/cli/src/execution-lifecycle/__tests__/execution-lifecycle-hooks.test.ts @@ -326,6 +326,7 @@ describe('Execution Lifecycle Hooks', () => { expect(handlers.nodeExecuteAfter).toHaveLength(2); expect(handlers.workflowExecuteBefore).toHaveLength(3); expect(handlers.workflowExecuteAfter).toHaveLength(5); + expect(handlers.workflowExecuteResume).toHaveLength(0); expect(handlers.nodeFetchedData).toHaveLength(1); expect(handlers.sendResponse).toHaveLength(0); expect(handlers.sendChunk).toHaveLength(0); @@ -733,6 +734,7 @@ describe('Execution Lifecycle Hooks', () => { expect(handlers.nodeExecuteAfter).toHaveLength(0); expect(handlers.workflowExecuteBefore).toHaveLength(2); expect(handlers.workflowExecuteAfter).toHaveLength(4); + expect(handlers.workflowExecuteResume).toHaveLength(0); expect(handlers.nodeFetchedData).toHaveLength(0); expect(handlers.sendResponse).toHaveLength(0); expect(handlers.sendChunk).toHaveLength(0); @@ -868,6 +870,7 @@ describe('Execution Lifecycle Hooks', () => { expect(handlers.nodeExecuteAfter).toHaveLength(2); expect(handlers.workflowExecuteBefore).toHaveLength(2); expect(handlers.workflowExecuteAfter).toHaveLength(4); + expect(handlers.workflowExecuteResume).toHaveLength(0); expect(handlers.nodeFetchedData).toHaveLength(1); expect(handlers.sendResponse).toHaveLength(0); expect(handlers.sendChunk).toHaveLength(0); @@ -992,6 +995,7 @@ describe('Execution Lifecycle Hooks', () => { expect(handlers.nodeExecuteAfter).toHaveLength(1); expect(handlers.workflowExecuteBefore).toHaveLength(2); expect(handlers.workflowExecuteAfter).toHaveLength(4); + expect(handlers.workflowExecuteResume).toHaveLength(0); expect(handlers.nodeFetchedData).toHaveLength(1); expect(handlers.sendResponse).toHaveLength(0); expect(handlers.sendChunk).toHaveLength(0); diff --git a/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts b/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts index 679a619dbf9..11820baefa1 100644 --- a/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts +++ b/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts @@ -57,6 +57,7 @@ class ModulesHooksRegistry { workflow: this.workflowData, runData, newStaticData, + executionId: this.executionId, }; // eslint-disable-next-line @typescript-eslint/no-unsafe-return return await instance[methodName].call(instance, context); @@ -97,6 +98,21 @@ class ModulesHooksRegistry { workflow: this.workflowData, workflowInstance, executionData, + executionId: this.executionId, + }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await instance[methodName].call(instance, context); + }); + break; + + case 'workflowExecuteResume': + hooks.addHandler(eventName, async function (workflowInstance, executionData) { + const context = { + type: 'workflowExecuteResume', + workflow: this.workflowData, + workflowInstance, + executionData, + executionId: this.executionId, }; // eslint-disable-next-line @typescript-eslint/no-unsafe-return return await instance[methodName].call(instance, context); diff --git a/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts b/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts index 9bfe9de80d7..3c6e4183a6c 100644 --- a/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts +++ b/packages/core/src/execution-engine/__tests__/execution-lifecycle-hooks.test.ts @@ -39,6 +39,7 @@ describe('ExecutionLifecycleHooks', () => { sendResponse: [], workflowExecuteAfter: [], workflowExecuteBefore: [], + workflowExecuteResume: [], sendChunk: [], }); }); @@ -63,6 +64,7 @@ describe('ExecutionLifecycleHooks', () => { }, { hook: 'workflowExecuteBefore', args: [mock(), mock()] }, { hook: 'workflowExecuteAfter', args: [mock(), mock()] }, + { hook: 'workflowExecuteResume', args: [mock(), mock()] }, { hook: 'sendResponse', args: [mock()] }, { hook: 'nodeFetchedData', args: ['workflow123', mock()] }, ]; diff --git a/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts b/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts index 487011c82fc..76c1ba64f7f 100644 --- a/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts +++ b/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts @@ -464,6 +464,79 @@ describe('WorkflowExecute', () => { }); }); + describe('workflowExecuteResume hook', () => { + const executionMode = 'manual'; + const executionOrder = 'v1'; + const nodeTypes = Helpers.NodeTypes(); + + test('should call workflowExecuteResume instead of workflowExecuteBefore when restartExecutionId is set', async () => { + // ARRANGE + const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); + const node1 = createNodeData({ name: 'node1' }); + const workflowInstance = new DirectedGraph() + .addNodes(trigger, node1) + .addConnections({ from: trigger, to: node1 }) + .toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder } }); + + const additionalData = Helpers.WorkflowExecuteAdditionalData(createDeferredPromise()); + // Set restartExecutionId to simulate a resumed execution + additionalData.restartExecutionId = 'previous-execution-id'; + const runHookSpy = jest.spyOn(additionalData.hooks!, 'runHook'); + + const workflowExecute = new WorkflowExecute(additionalData, executionMode); + + // ACT + await workflowExecute.run({ workflow: workflowInstance, startNode: trigger }); + + // ASSERT + const workflowHooks = runHookSpy.mock.calls.filter( + (call) => + call[0] === 'workflowExecuteBefore' || + call[0] === 'workflowExecuteAfter' || + call[0] === 'workflowExecuteResume', + ); + + // Should have workflowExecuteResume instead of workflowExecuteBefore + expect(workflowHooks.map((hook) => hook[0])).toEqual([ + 'workflowExecuteResume', + 'workflowExecuteAfter', + ]); + }); + + test('should call workflowExecuteBefore when restartExecutionId is not set', async () => { + // ARRANGE + const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); + const node1 = createNodeData({ name: 'node1' }); + const workflowInstance = new DirectedGraph() + .addNodes(trigger, node1) + .addConnections({ from: trigger, to: node1 }) + .toWorkflow({ name: '', active: false, nodeTypes, settings: { executionOrder } }); + + const additionalData = Helpers.WorkflowExecuteAdditionalData(createDeferredPromise()); + // restartExecutionId is undefined by default + const runHookSpy = jest.spyOn(additionalData.hooks!, 'runHook'); + + const workflowExecute = new WorkflowExecute(additionalData, executionMode); + + // ACT + await workflowExecute.run({ workflow: workflowInstance, startNode: trigger }); + + // ASSERT + const workflowHooks = runHookSpy.mock.calls.filter( + (call) => + call[0] === 'workflowExecuteBefore' || + call[0] === 'workflowExecuteAfter' || + call[0] === 'workflowExecuteResume', + ); + + // Should have workflowExecuteBefore, not workflowExecuteResume + expect(workflowHooks.map((hook) => hook[0])).toEqual([ + 'workflowExecuteBefore', + 'workflowExecuteAfter', + ]); + }); + }); + //run tests on json files from specified directory, default 'workflows' //workflows must have pinned data that would be used to test output after execution describe('run test workflows', () => { diff --git a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts index 09ec0306458..3568ec5f6a8 100644 --- a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts +++ b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts @@ -38,6 +38,14 @@ export type ExecutionLifecycleHookHandlers = { ) => Promise | void >; + workflowExecuteResume: Array< + ( + this: ExecutionLifecycleHooks, + workflow: Workflow, + data?: IRunExecutionData, + ) => Promise | void + >; + workflowExecuteAfter: Array< (this: ExecutionLifecycleHooks, data: IRun, newStaticData: IDataObject) => Promise | void >; @@ -88,6 +96,7 @@ export class ExecutionLifecycleHooks { sendResponse: [], workflowExecuteAfter: [], workflowExecuteBefore: [], + workflowExecuteResume: [], sendChunk: [], }; diff --git a/packages/core/src/execution-engine/workflow-execute.ts b/packages/core/src/execution-engine/workflow-execute.ts index f59096fb305..623fa7d944a 100644 --- a/packages/core/src/execution-engine/workflow-execute.ts +++ b/packages/core/src/execution-engine/workflow-execute.ts @@ -1438,6 +1438,8 @@ export class WorkflowExecute { if (!this.additionalData.restartExecutionId) { await hooks.runHook('workflowExecuteBefore', [workflow, this.runExecutionData]); + } else { + await hooks.runHook('workflowExecuteResume', [workflow, this.runExecutionData]); } } catch (error) { const e = error as unknown as ExecutionBaseError;