From 9ba58ca80b8393c2a7572f3d43d0423054ce1e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Wed, 9 Apr 2025 10:19:58 +0200 Subject: [PATCH] refactor(core): Persist node execution order, and forward it to the frontend (#14455) --- cypress/utils/executions.ts | 4 +- packages/@n8n/api-types/src/push/execution.ts | 8 ++- .../src/js-task-runner/__tests__/test-data.ts | 1 + .../execution-lifecycle-hooks.test.ts | 13 ++-- .../execution-lifecycle-hooks.ts | 7 +- .../src/executions/execution-data.service.ts | 1 + .../executions/execution-recovery.service.ts | 1 + .../cli/src/executions/execution.service.ts | 1 + .../data-request-response-stripper.test.ts | 2 + .../src/workflow-execute-additional-data.ts | 1 + packages/cli/src/workflow-helpers.ts | 1 + ...-task-runner-execution.integration.test.ts | 1 + .../execution-lifecycle-hooks.test.ts | 11 +-- .../__tests__/workflow-execute.test.ts | 70 +++++++++---------- .../execution-lifecycle-hooks.ts | 7 +- .../supply-data-context.ts | 7 +- .../__tests__/find-start-nodes.test.ts | 1 + .../__tests__/helpers.ts | 1 + .../__tests__/to-itask-data.test.ts | 4 ++ .../src/execution-engine/workflow-execute.ts | 50 ++++++------- packages/core/test/helpers/index.ts | 6 +- .../components/CanvasChat/CanvasChat.test.ts | 1 + .../components/CanvasChat/__test__/data.ts | 12 ++-- .../composables/useChatMessaging.ts | 3 +- .../src/components/InputPanel.test.ts | 1 + .../editor-ui/src/components/RunData.test.ts | 16 +++-- .../src/components/RunDataAi/utils.test.ts | 19 ++--- .../src/components/RunDataJsonActions.test.ts | 10 +-- .../useAIAssistantHelpers.test.constants.ts | 5 ++ .../composables/useAIAssistantHelpers.test.ts | 2 + .../src/composables/useCanvasMapping.test.ts | 17 +++++ .../src/composables/useDataSchema.test.ts | 7 +- .../src/composables/useNodeDirtiness.test.ts | 2 + .../src/composables/useRunWorkflow.test.ts | 2 + .../codemirror/completions/__tests__/mock.ts | 4 ++ .../src/stores/workflows.store.test.ts | 2 + .../src/utils/pairedItemUtils.test.ts | 12 +++- packages/workflow/src/Interfaces.ts | 15 ++-- packages/workflow/src/WorkflowDataProxy.ts | 10 ++- .../workflow/test/TelemetryHelpers.test.ts | 8 +++ packages/workflow/test/Workflow.test.ts | 2 + 41 files changed, 235 insertions(+), 113 deletions(-) diff --git a/cypress/utils/executions.ts b/cypress/utils/executions.ts index 11eb5bba2c..be9a9f6d7d 100644 --- a/cypress/utils/executions.ts +++ b/cypress/utils/executions.ts @@ -16,7 +16,8 @@ export function createMockNodeExecutionData( ): Record { return { [name]: { - startTime: new Date().getTime(), + startTime: Date.now(), + executionIndex: 0, executionTime: 1, executionStatus, data: jsonData @@ -77,6 +78,7 @@ export function runMockWorkflowExecution({ cy.push('nodeExecuteBefore', { executionId, nodeName, + data: nodeRunData, }); cy.push('nodeExecuteAfter', { executionId, diff --git a/packages/@n8n/api-types/src/push/execution.ts b/packages/@n8n/api-types/src/push/execution.ts index b87bb67d0f..b16b73aeff 100644 --- a/packages/@n8n/api-types/src/push/execution.ts +++ b/packages/@n8n/api-types/src/push/execution.ts @@ -1,4 +1,9 @@ -import type { ExecutionStatus, ITaskData, WorkflowExecuteMode } from 'n8n-workflow'; +import type { + ExecutionStatus, + ITaskData, + ITaskStartedData, + WorkflowExecuteMode, +} from 'n8n-workflow'; type ExecutionStarted = { type: 'executionStarted'; @@ -43,6 +48,7 @@ type NodeExecuteBefore = { data: { executionId: string; nodeName: string; + data: ITaskStartedData; }; }; diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/test-data.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/test-data.ts index 1b630122ec..a199b9b960 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/test-data.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/test-data.ts @@ -40,6 +40,7 @@ export const newNode = (opts: Partial = {}): INode => ({ export const newTaskData = (opts: Partial & Pick): ITaskData => ({ startTime: Date.now(), executionTime: 0, + executionIndex: 0, executionStatus: 'success', ...opts, }); 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 c7e4ecd8dd..932e972470 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 @@ -17,6 +17,7 @@ import type { INode, IWorkflowBase, WorkflowExecuteMode, + ITaskStartedData, } from 'n8n-workflow'; import config from '@/config'; @@ -68,6 +69,7 @@ describe('Execution Lifecycle Hooks', () => { }; const workflow = mock(); const staticData = mock(); + const taskStartedData = mock(); const taskData = mock(); const runExecutionData = mock(); const successfulRun = mock({ @@ -146,7 +148,7 @@ describe('Execution Lifecycle Hooks', () => { const nodeEventsTests = () => { describe('nodeExecuteBefore', () => { it('should emit node-pre-execute event', async () => { - await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]); + await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName, taskStartedData]); expect(eventService.emit).toHaveBeenCalledWith('node-pre-execute', { executionId, @@ -246,10 +248,10 @@ describe('Execution Lifecycle Hooks', () => { describe('nodeExecuteBefore', () => { it('should send nodeExecuteBefore push event', async () => { - await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]); + await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName, taskStartedData]); expect(push.send).toHaveBeenCalledWith( - { type: 'nodeExecuteBefore', data: { executionId, nodeName } }, + { type: 'nodeExecuteBefore', data: { executionId, nodeName, data: taskStartedData } }, pushRef, ); }); @@ -471,8 +473,9 @@ describe('Execution Lifecycle Hooks', () => { (successfulRun.data.resultData.runData = { [nodeName]: [ { - executionTime: 1, startTime: 1, + executionIndex: 0, + executionTime: 1, source: [], data: { main: [ @@ -517,7 +520,7 @@ describe('Execution Lifecycle Hooks', () => { expect(handlers.workflowExecuteBefore).toHaveLength(2); expect(handlers.workflowExecuteAfter).toHaveLength(4); - await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]); + await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName, taskStartedData]); await lifecycleHooks.runHook('nodeExecuteAfter', [nodeName, taskData, runExecutionData]); await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]); await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]); diff --git a/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts b/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts index c7b9c63843..3ef265f7e9 100644 --- a/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts +++ b/packages/cli/src/execution-lifecycle/execution-lifecycle-hooks.ts @@ -68,7 +68,7 @@ function hookFunctionsPush( if (!pushRef) return; const logger = Container.get(Logger); const pushInstance = Container.get(Push); - hooks.addHandler('nodeExecuteBefore', function (nodeName) { + hooks.addHandler('nodeExecuteBefore', function (nodeName, data) { const { executionId } = this; // Push data to session which started workflow before each // node which starts rendering @@ -78,7 +78,10 @@ function hookFunctionsPush( workflowId: this.workflowData.id, }); - pushInstance.send({ type: 'nodeExecuteBefore', data: { executionId, nodeName } }, pushRef); + pushInstance.send( + { type: 'nodeExecuteBefore', data: { executionId, nodeName, data } }, + pushRef, + ); }); hooks.addHandler('nodeExecuteAfter', function (nodeName, data) { const { executionId } = this; diff --git a/packages/cli/src/executions/execution-data.service.ts b/packages/cli/src/executions/execution-data.service.ts index 9e23e39d1d..2cbdc64a56 100644 --- a/packages/cli/src/executions/execution-data.service.ts +++ b/packages/cli/src/executions/execution-data.service.ts @@ -37,6 +37,7 @@ export class ExecutionDataService { returnData.data.resultData.runData[node.name] = [ { startTime, + executionIndex: 0, executionTime: 0, executionStatus: 'error', error: executionError, diff --git a/packages/cli/src/executions/execution-recovery.service.ts b/packages/cli/src/executions/execution-recovery.service.ts index 3a5d5a65b1..c6224cdbd7 100644 --- a/packages/cli/src/executions/execution-recovery.service.ts +++ b/packages/cli/src/executions/execution-recovery.service.ts @@ -94,6 +94,7 @@ export class ExecutionRecoveryService { const taskData: ITaskData = { startTime: nodeStartedMessage.ts.toUnixInteger(), + executionIndex: 0, executionTime: -1, source: [null], }; diff --git a/packages/cli/src/executions/execution.service.ts b/packages/cli/src/executions/execution.service.ts index a37087ce2a..fd8762ee38 100644 --- a/packages/cli/src/executions/execution.service.ts +++ b/packages/cli/src/executions/execution.service.ts @@ -311,6 +311,7 @@ export class ExecutionService { [node.name]: [ { startTime: 0, + executionIndex: 0, executionTime: 0, error, source: [], diff --git a/packages/cli/src/task-runners/task-managers/__tests__/data-request-response-stripper.test.ts b/packages/cli/src/task-runners/task-managers/__tests__/data-request-response-stripper.test.ts index 4b27542c4d..40a7597d92 100644 --- a/packages/cli/src/task-runners/task-managers/__tests__/data-request-response-stripper.test.ts +++ b/packages/cli/src/task-runners/task-managers/__tests__/data-request-response-stripper.test.ts @@ -110,6 +110,7 @@ const taskData: DataRequestResponse = { { hints: [], startTime: 1730313407328, + executionIndex: 0, executionTime: 1, source: [], executionStatus: 'success', @@ -122,6 +123,7 @@ const taskData: DataRequestResponse = { { hints: [], startTime: 1730313407330, + executionIndex: 1, executionTime: 1, source: [ { diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 0155f985ec..5574978a72 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -373,6 +373,7 @@ export async function getBase( const eventService = Container.get(EventService); return { + currentNodeExecutionIndex: 0, credentialsHelper: Container.get(CredentialsHelper), executeWorkflow, restApiUrl: urlBaseWebhook + globalConfig.endpoints.rest, diff --git a/packages/cli/src/workflow-helpers.ts b/packages/cli/src/workflow-helpers.ts index 620664c784..42d50ef16f 100644 --- a/packages/cli/src/workflow-helpers.ts +++ b/packages/cli/src/workflow-helpers.ts @@ -39,6 +39,7 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi return { startTime: 0, + executionIndex: 0, executionTime: 0, data: { main: [itemsPerRun] }, source: lastNodeRunData.source, diff --git a/packages/cli/test/integration/task-runners/js-task-runner-execution.integration.test.ts b/packages/cli/test/integration/task-runners/js-task-runner-execution.integration.test.ts index 5633df7340..d85be1a7e2 100644 --- a/packages/cli/test/integration/task-runners/js-task-runner-execution.integration.test.ts +++ b/packages/cli/test/integration/task-runners/js-task-runner-execution.integration.test.ts @@ -108,6 +108,7 @@ describe('JS TaskRunner execution on internal mode', () => { ManualTrigger: [ { startTime: Date.now(), + executionIndex: 0, executionTime: 0, executionStatus: 'success', source: [], 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 fb85216b25..c255182e32 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 @@ -6,6 +6,7 @@ import type { IRun, IRunExecutionData, ITaskData, + ITaskStartedData, IWorkflowBase, Workflow, } from 'n8n-workflow'; @@ -52,7 +53,7 @@ describe('ExecutionLifecycleHooks', () => { hook: ExecutionLifecycleHookName; args: Parameters; }> = [ - { hook: 'nodeExecuteBefore', args: ['testNode'] }, + { hook: 'nodeExecuteBefore', args: ['testNode', mock()] }, { hook: 'nodeExecuteAfter', args: ['testNode', mock(), mock()], @@ -84,7 +85,7 @@ describe('ExecutionLifecycleHooks', () => { }); hooks.addHandler('nodeExecuteBefore', hook1, hook2); - await hooks.runHook('nodeExecuteBefore', ['testNode']); + await hooks.runHook('nodeExecuteBefore', ['testNode', mock()]); expect(executionOrder).toEqual(['hook1', 'hook2']); expect(hook1).toHaveBeenCalled(); @@ -98,7 +99,7 @@ describe('ExecutionLifecycleHooks', () => { }); hooks.addHandler('nodeExecuteBefore', hook); - await hooks.runHook('nodeExecuteBefore', ['testNode']); + await hooks.runHook('nodeExecuteBefore', ['testNode', mock()]); expect(hook).toHaveBeenCalled(); }); @@ -107,7 +108,9 @@ describe('ExecutionLifecycleHooks', () => { const errorHook = jest.fn().mockRejectedValue(new Error('Hook failed')); hooks.addHandler('nodeExecuteBefore', errorHook); - await expect(hooks.runHook('nodeExecuteBefore', ['testNode'])).rejects.toThrow('Hook failed'); + await expect(hooks.runHook('nodeExecuteBefore', ['testNode', mock()])).rejects.toThrow( + 'Hook failed', + ); }); }); }); 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 89b80d333f..3b0e1a46fe 100644 --- a/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts +++ b/packages/core/src/execution-engine/__tests__/workflow-execute.test.ts @@ -77,11 +77,7 @@ describe('WorkflowExecute', () => { }); const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData( - waitPromise, - nodeExecutionOrder, - ); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, executionMode); @@ -110,6 +106,12 @@ describe('WorkflowExecute', () => { } // Check if the nodes did execute in the correct order + const nodeExecutionOrder: string[] = []; + Object.entries(result.data.resultData.runData).forEach(([nodeName, taskDataArr]) => { + taskDataArr.forEach((taskData) => { + nodeExecutionOrder[taskData.executionIndex] = nodeName; + }); + }); expect(nodeExecutionOrder).toEqual(testData.output.nodeExecutionOrder); // Check if other data has correct value @@ -140,11 +142,7 @@ describe('WorkflowExecute', () => { }); const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData( - waitPromise, - nodeExecutionOrder, - ); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, executionMode); @@ -177,6 +175,12 @@ describe('WorkflowExecute', () => { } // Check if the nodes did execute in the correct order + const nodeExecutionOrder: string[] = []; + Object.entries(result.data.resultData.runData).forEach(([nodeName, taskDataArr]) => { + taskDataArr.forEach((taskData) => { + nodeExecutionOrder[taskData.executionIndex] = nodeName; + }); + }); expect(nodeExecutionOrder).toEqual(testData.output.nodeExecutionOrder); // Check if other data has correct value @@ -207,11 +211,7 @@ describe('WorkflowExecute', () => { }); const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData( - waitPromise, - nodeExecutionOrder, - ); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, executionMode); @@ -259,8 +259,7 @@ describe('WorkflowExecute', () => { test("deletes dirty nodes' run data", async () => { // ARRANGE const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); @@ -307,8 +306,7 @@ describe('WorkflowExecute', () => { test('deletes run data of children of dirty nodes as well', async () => { // ARRANGE const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, 'manual'); jest.spyOn(workflowExecute, 'processRunExecutionData').mockImplementationOnce(jest.fn()); @@ -369,8 +367,7 @@ describe('WorkflowExecute', () => { test('removes disabled nodes from the workflow', async () => { // ARRANGE const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); @@ -423,8 +420,7 @@ describe('WorkflowExecute', () => { test('passes filtered run data to `recreateNodeExecutionStack`', async () => { // ARRANGE const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); @@ -486,8 +482,7 @@ describe('WorkflowExecute', () => { test('passes subgraph to `cleanRunData`', async () => { // ARRANGE const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); @@ -547,8 +542,7 @@ describe('WorkflowExecute', () => { test('passes pruned dirty nodes to `cleanRunData`', async () => { // ARRANGE const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); @@ -599,8 +593,7 @@ describe('WorkflowExecute', () => { test('works with a single node', async () => { // ARRANGE const waitPromise = createDeferredPromise(); - const nodeExecutionOrder: string[] = []; - const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder); + const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise); const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const trigger = createNodeData({ name: 'trigger' }); @@ -856,6 +849,7 @@ describe('WorkflowExecute', () => { }, source: [], startTime: 0, + executionIndex: 0, executionTime: 0, }, ], @@ -1132,6 +1126,7 @@ describe('WorkflowExecute', () => { source: [], data: { main: [[], []] }, startTime: 0, + executionIndex: 0, executionTime: 0, }, ], @@ -1165,6 +1160,7 @@ describe('WorkflowExecute', () => { main: [[{ json: { data: 'test' } }], []], }, startTime: 0, + executionIndex: 0, executionTime: 0, }, ], @@ -1188,6 +1184,7 @@ describe('WorkflowExecute', () => { main: [[]], }, startTime: 0, + executionIndex: 0, executionTime: 0, }, { @@ -1196,6 +1193,7 @@ describe('WorkflowExecute', () => { main: [[{ json: { data: 'test' } }]], }, startTime: 0, + executionIndex: 1, executionTime: 0, }, ], @@ -1215,6 +1213,7 @@ describe('WorkflowExecute', () => { { source: [], startTime: 0, + executionIndex: 0, executionTime: 0, }, ], @@ -1254,7 +1253,7 @@ describe('WorkflowExecute', () => { test('should do nothing when there is no metadata', () => { runExecutionData.resultData.runData = { - node1: [{ startTime: 0, executionTime: 0, source: [] }], + node1: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 0 }], }; workflowExecute.moveNodeMetadata(); @@ -1264,7 +1263,7 @@ describe('WorkflowExecute', () => { test('should merge metadata into runData for single node', () => { runExecutionData.resultData.runData = { - node1: [{ startTime: 0, executionTime: 0, source: [] }], + node1: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 0 }], }; runExecutionData.executionData!.metadata = { node1: [{ parentExecution }], @@ -1277,8 +1276,8 @@ describe('WorkflowExecute', () => { test('should merge metadata into runData for multiple nodes', () => { runExecutionData.resultData.runData = { - node1: [{ startTime: 0, executionTime: 0, source: [] }], - node2: [{ startTime: 0, executionTime: 0, source: [] }], + node1: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 0 }], + node2: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 1 }], }; runExecutionData.executionData!.metadata = { node1: [{ parentExecution }], @@ -1297,6 +1296,7 @@ describe('WorkflowExecute', () => { node1: [ { startTime: 0, + executionIndex: 0, executionTime: 0, source: [], metadata: { subExecutionsCount: 4 }, @@ -1318,8 +1318,8 @@ describe('WorkflowExecute', () => { test('should handle multiple run indices', () => { runExecutionData.resultData.runData = { node1: [ - { startTime: 0, executionTime: 0, source: [] }, - { startTime: 0, executionTime: 0, source: [] }, + { startTime: 0, executionTime: 0, source: [], executionIndex: 0 }, + { startTime: 0, executionTime: 0, source: [], executionIndex: 1 }, ], }; runExecutionData.executionData!.metadata = { diff --git a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts index 047d5a2075..a7793c1ca9 100644 --- a/packages/core/src/execution-engine/execution-lifecycle-hooks.ts +++ b/packages/core/src/execution-engine/execution-lifecycle-hooks.ts @@ -5,6 +5,7 @@ import type { IRun, IRunExecutionData, ITaskData, + ITaskStartedData, IWorkflowBase, Workflow, WorkflowExecuteMode, @@ -12,7 +13,11 @@ import type { export type ExecutionLifecyleHookHandlers = { nodeExecuteBefore: Array< - (this: ExecutionLifecycleHooks, nodeName: string) => Promise | void + ( + this: ExecutionLifecycleHooks, + nodeName: string, + data: ITaskStartedData, + ) => Promise | void >; nodeExecuteAfter: Array< diff --git a/packages/core/src/execution-engine/node-execution-context/supply-data-context.ts b/packages/core/src/execution-engine/node-execution-context/supply-data-context.ts index 69b9bf346e..d739313fa6 100644 --- a/packages/core/src/execution-engine/node-execution-context/supply-data-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/supply-data-context.ts @@ -232,8 +232,9 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData let taskData: ITaskData | undefined; if (type === 'input') { taskData = { - startTime: new Date().getTime(), + startTime: Date.now(), executionTime: 0, + executionIndex: additionalData.currentNodeExecutionIndex++, executionStatus: 'running', source: [null], }; @@ -277,10 +278,10 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData } runExecutionData.resultData.runData[nodeName][currentNodeRunIndex] = taskData; - await additionalData.hooks?.runHook('nodeExecuteBefore', [nodeName]); + await additionalData.hooks?.runHook('nodeExecuteBefore', [nodeName, taskData]); } else { // Outputs - taskData.executionTime = new Date().getTime() - taskData.startTime; + taskData.executionTime = Date.now() - taskData.startTime; await additionalData.hooks?.runHook('nodeExecuteAfter', [ nodeName, diff --git a/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts index cb5b75db08..30da6e563b 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts @@ -557,6 +557,7 @@ describe('findStartNodes', () => { executionStatus: 'success', executionTime: 0, startTime: 0, + executionIndex: 0, source: [], data: { main: [[], [{ json: { name: 'loop' } }]] }, }, diff --git a/packages/core/src/execution-engine/partial-execution-utils/__tests__/helpers.ts b/packages/core/src/execution-engine/partial-execution-utils/__tests__/helpers.ts index 0cc8e116db..5fffd28346 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/__tests__/helpers.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/__tests__/helpers.ts @@ -38,6 +38,7 @@ export function toITaskData(taskData: TaskData[]): ITaskData { executionStatus: 'success', executionTime: 0, startTime: 0, + executionIndex: 0, source: [], data: {}, }; diff --git a/packages/core/src/execution-engine/partial-execution-utils/__tests__/to-itask-data.test.ts b/packages/core/src/execution-engine/partial-execution-utils/__tests__/to-itask-data.test.ts index 13796bcd23..451c0e5b83 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/__tests__/to-itask-data.test.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/__tests__/to-itask-data.test.ts @@ -8,6 +8,7 @@ test('toITaskData', function () { executionTime: 0, source: [], startTime: 0, + executionIndex: 0, data: { main: [[{ json: { value: 1 } }]], }, @@ -18,6 +19,7 @@ test('toITaskData', function () { executionTime: 0, source: [], startTime: 0, + executionIndex: 0, data: { main: [null, [{ json: { value: 1 } }]], }, @@ -32,6 +34,7 @@ test('toITaskData', function () { executionTime: 0, source: [], startTime: 0, + executionIndex: 0, data: { [NodeConnectionTypes.AiAgent]: [null, [{ json: { value: 1 } }]], }, @@ -46,6 +49,7 @@ test('toITaskData', function () { executionStatus: 'success', executionTime: 0, startTime: 0, + executionIndex: 0, source: [], data: { main: [ diff --git a/packages/core/src/execution-engine/workflow-execute.ts b/packages/core/src/execution-engine/workflow-execute.ts index 1b371da141..50fc4a9fe7 100644 --- a/packages/core/src/execution-engine/workflow-execute.ts +++ b/packages/core/src/execution-engine/workflow-execute.ts @@ -35,11 +35,11 @@ import type { WorkflowExecuteMode, CloseFunction, StartNodeData, - NodeExecutionHint, IRunNodeResponse, IWorkflowIssues, INodeIssues, INodeType, + ITaskStartedData, } from 'n8n-workflow'; import { LoggerProxy as Logger, @@ -1304,11 +1304,8 @@ export class WorkflowExecute { // Variables which hold temporary data for each node-execution let executionData: IExecuteData; let executionError: ExecutionBaseError | undefined; - let executionHints: NodeExecutionHint[] = []; let executionNode: INode; - let nodeSuccessData: INodeExecutionData[][] | null | undefined; let runIndex: number; - let startTime: number; if (this.runExecutionData.startData === undefined) { this.runExecutionData.startData = {}; @@ -1356,19 +1353,20 @@ export class WorkflowExecute { // Set the incoming data of the node that it can be saved correctly executionData = this.runExecutionData.executionData!.nodeExecutionStack[0]; + const taskData: ITaskData = { + startTime: Date.now(), + executionIndex: 0, + executionTime: 0, + data: { + main: executionData.data.main, + }, + source: [], + executionStatus: 'error', + hints: [], + }; this.runExecutionData.resultData = { runData: { - [executionData.node.name]: [ - { - startTime, - executionTime: new Date().getTime() - startTime, - data: { - main: executionData.data.main, - } as ITaskDataConnections, - source: [], - executionStatus: 'error', - }, - ], + [executionData.node.name]: [taskData], }, lastNodeExecuted: executionData.node.name, error: executionError, @@ -1391,13 +1389,19 @@ export class WorkflowExecute { return; } - nodeSuccessData = null; + let nodeSuccessData: INodeExecutionData[][] | null | undefined = null; executionError = undefined; - executionHints = []; executionData = this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; executionNode = executionData.node; + const taskStartedData: ITaskStartedData = { + startTime: Date.now(), + executionIndex: this.additionalData.currentNodeExecutionIndex++, + source: !executionData.source ? [] : executionData.source.main, + hints: [], + }; + // Update the pairedItem information on items const newTaskDataConnections: ITaskDataConnections = {}; for (const connectionType of Object.keys(executionData.data)) { @@ -1425,7 +1429,7 @@ export class WorkflowExecute { node: executionNode.name, workflowId: workflow.id, }); - await hooks.runHook('nodeExecuteBefore', [executionNode.name]); + await hooks.runHook('nodeExecuteBefore', [executionNode.name, taskStartedData]); // Get the index of the current run runIndex = 0; @@ -1457,8 +1461,6 @@ export class WorkflowExecute { continue executionLoop; } - startTime = new Date().getTime(); - let maxTries = 1; if (executionData.node.retryOnFail === true) { // TODO: Remove the hardcoded default-values here and also in NodeSettings.vue @@ -1534,7 +1536,7 @@ export class WorkflowExecute { } if (runNodeData.hints?.length) { - executionHints.push(...runNodeData.hints); + taskStartedData.hints!.push(...runNodeData.hints); } if (nodeSuccessData && executionData.node.onError === 'continueErrorOutput') { @@ -1631,10 +1633,8 @@ export class WorkflowExecute { } const taskData: ITaskData = { - hints: executionHints, - startTime, - executionTime: new Date().getTime() - startTime, - source: !executionData.source ? [] : executionData.source.main, + ...taskStartedData, + executionTime: Date.now() - taskStartedData.startTime, metadata: executionData.metadata, executionStatus: this.runExecutionData.waitTill ? 'waiting' : 'success', }; diff --git a/packages/core/test/helpers/index.ts b/packages/core/test/helpers/index.ts index 27008b177a..8f00694b24 100644 --- a/packages/core/test/helpers/index.ts +++ b/packages/core/test/helpers/index.ts @@ -51,14 +51,10 @@ export function NodeTypes(nodeTypes: INodeTypeData = predefinedNodesTypes): INod export function WorkflowExecuteAdditionalData( waitPromise: IDeferredPromise, - nodeExecutionOrder: string[], ): IWorkflowExecuteAdditionalData { const hooks = new ExecutionLifecycleHooks('trigger', '1', mock()); - hooks.addHandler('nodeExecuteAfter', (nodeName) => { - nodeExecutionOrder.push(nodeName); - }); hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData)); - return mock({ hooks }); + return mock({ hooks, currentNodeExecutionIndex: 0 }); } const preparePinData = (pinData: IDataObject) => { diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts index 0ea479337b..3cac570e55 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.test.ts @@ -256,6 +256,7 @@ describe('CanvasChat', () => { ], ], }, + executionIndex: 0, executionStatus: 'success', executionTime: 0, source: [null], diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts b/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts index 99e2b4ffa6..a1564f8f3f 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/__test__/data.ts @@ -71,7 +71,8 @@ export const aiChatExecutionResponse: IExecutionResponse = { 'AI Agent': [ { executionStatus: 'success', - startTime: +new Date('2025-03-26T00:00:00.002Z'), + startTime: Date.parse('2025-03-26T00:00:00.002Z'), + executionIndex: 0, executionTime: 1778, source: [], data: {}, @@ -80,7 +81,8 @@ export const aiChatExecutionResponse: IExecutionResponse = { 'AI Model': [ { executionStatus: 'error', - startTime: +new Date('2025-03-26T00:00:00.003Z'), + startTime: Date.parse('2025-03-26T00:00:00.003Z'), + executionIndex: 1, executionTime: 1777, source: [], error: new WorkflowOperationError('Test error', aiModelNode, 'Test error description'), @@ -121,7 +123,8 @@ export const aiManualExecutionResponse: IExecutionResponse = { 'AI Agent': [ { executionStatus: 'success', - startTime: +new Date('2025-03-30T00:00:00.002Z'), + startTime: Date.parse('2025-03-30T00:00:00.002Z'), + executionIndex: 0, executionTime: 12, source: [], data: {}, @@ -130,7 +133,8 @@ export const aiManualExecutionResponse: IExecutionResponse = { 'AI Model': [ { executionStatus: 'success', - startTime: +new Date('2025-03-30T00:00:00.003Z'), + startTime: Date.parse('2025-03-30T00:00:00.003Z'), + executionIndex: 1, executionTime: 3456, source: [], data: { diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts index 07ea360002..ee1a7a9b05 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts @@ -130,8 +130,9 @@ export function useChatMessaging({ inputPayload.binary = binaryData; } const nodeData: ITaskData = { - startTime: new Date().getTime(), + startTime: Date.now(), executionTime: 0, + executionIndex: 0, executionStatus: 'success', data: { main: [[inputPayload]], diff --git a/packages/frontend/editor-ui/src/components/InputPanel.test.ts b/packages/frontend/editor-ui/src/components/InputPanel.test.ts index 7211df548c..a6620125db 100644 --- a/packages/frontend/editor-ui/src/components/InputPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/InputPanel.test.ts @@ -128,6 +128,7 @@ describe('InputPanel', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], data: {}, }, diff --git a/packages/frontend/editor-ui/src/components/RunData.test.ts b/packages/frontend/editor-ui/src/components/RunData.test.ts index 6ab08c5ca4..bd28fd1b4b 100644 --- a/packages/frontend/editor-ui/src/components/RunData.test.ts +++ b/packages/frontend/editor-ui/src/components/RunData.test.ts @@ -359,8 +359,9 @@ describe('RunData', () => { const { getByTestId, queryByTestId } = render({ runs: [ { - startTime: new Date().getTime(), - executionTime: new Date().getTime(), + startTime: Date.now(), + executionIndex: 0, + executionTime: 1, data: { main: [[{ json: {} }]], }, @@ -368,8 +369,9 @@ describe('RunData', () => { metadata, }, { - startTime: new Date().getTime(), - executionTime: new Date().getTime(), + startTime: Date.now(), + executionIndex: 1, + executionTime: 1, data: { main: [[{ json: {} }]], }, @@ -413,6 +415,7 @@ describe('RunData', () => { { hints: [], startTime: 1737643696893, + executionIndex: 0, executionTime: 2, source: [ { @@ -598,8 +601,9 @@ describe('RunData', () => { runs?: ITaskData[]; }) => { const defaultRun: ITaskData = { - startTime: new Date().getTime(), - executionTime: new Date().getTime(), + startTime: Date.now(), + executionIndex: 0, + executionTime: 1, data: { main: [defaultRunItems ?? [{ json: {} }]], }, diff --git a/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts b/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts index a85f801d0f..1c4afc215a 100644 --- a/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts +++ b/packages/frontend/editor-ui/src/components/RunDataAi/utils.test.ts @@ -6,6 +6,7 @@ describe(getTreeNodeData, () => { function createTaskData(partialData: Partial): ITaskData { return { startTime: 0, + executionIndex: 0, executionTime: 1, source: [], executionStatus: 'success', @@ -29,10 +30,10 @@ describe(getTreeNodeData, () => { }, }); const taskDataByNodeName: Record = { - A: [createTaskData({ startTime: +new Date('2025-02-26T00:00:00.000Z') })], + A: [createTaskData({ startTime: Date.parse('2025-02-26T00:00:00.000Z') })], B: [ createTaskData({ - startTime: +new Date('2025-02-26T00:00:01.000Z'), + startTime: Date.parse('2025-02-26T00:00:01.000Z'), data: { main: [ [ @@ -50,7 +51,7 @@ describe(getTreeNodeData, () => { }, }), createTaskData({ - startTime: +new Date('2025-02-26T00:00:03.000Z'), + startTime: Date.parse('2025-02-26T00:00:03.000Z'), data: { main: [ [ @@ -70,7 +71,7 @@ describe(getTreeNodeData, () => { ], C: [ createTaskData({ - startTime: +new Date('2025-02-26T00:00:02.000Z'), + startTime: Date.parse('2025-02-26T00:00:02.000Z'), data: { main: [ [ @@ -87,7 +88,7 @@ describe(getTreeNodeData, () => { ], }, }), - createTaskData({ startTime: +new Date('2025-02-26T00:00:04.000Z') }), + createTaskData({ startTime: Date.parse('2025-02-26T00:00:04.000Z') }), ], }; @@ -117,7 +118,7 @@ describe(getTreeNodeData, () => { id: 'B', node: 'B', runIndex: 0, - startTime: +new Date('2025-02-26T00:00:01.000Z'), + startTime: Date.parse('2025-02-26T00:00:01.000Z'), parent: expect.objectContaining({ node: 'A' }), consumedTokens: { completionTokens: 1, @@ -132,7 +133,7 @@ describe(getTreeNodeData, () => { id: 'C', node: 'C', runIndex: 0, - startTime: +new Date('2025-02-26T00:00:02.000Z'), + startTime: Date.parse('2025-02-26T00:00:02.000Z'), parent: expect.objectContaining({ node: 'B' }), consumedTokens: { completionTokens: 7, @@ -148,7 +149,7 @@ describe(getTreeNodeData, () => { id: 'B', node: 'B', runIndex: 1, - startTime: +new Date('2025-02-26T00:00:03.000Z'), + startTime: Date.parse('2025-02-26T00:00:03.000Z'), parent: expect.objectContaining({ node: 'A' }), consumedTokens: { completionTokens: 4, @@ -163,7 +164,7 @@ describe(getTreeNodeData, () => { id: 'C', node: 'C', runIndex: 1, - startTime: +new Date('2025-02-26T00:00:04.000Z'), + startTime: Date.parse('2025-02-26T00:00:04.000Z'), parent: expect.objectContaining({ node: 'B' }), consumedTokens: { completionTokens: 0, diff --git a/packages/frontend/editor-ui/src/components/RunDataJsonActions.test.ts b/packages/frontend/editor-ui/src/components/RunDataJsonActions.test.ts index b3ab76657d..baca626d6b 100644 --- a/packages/frontend/editor-ui/src/components/RunDataJsonActions.test.ts +++ b/packages/frontend/editor-ui/src/components/RunDataJsonActions.test.ts @@ -68,8 +68,9 @@ async function createPiniaWithActiveNode() { runData: { [node.name]: [ { - startTime: new Date().getTime(), - executionTime: new Date().getTime(), + startTime: Date.now(), + executionIndex: 0, + executionTime: 1, data: { main: [ [ @@ -91,8 +92,9 @@ async function createPiniaWithActiveNode() { source: [null], }, { - startTime: new Date().getTime(), - executionTime: new Date().getTime(), + startTime: Date.now(), + executionIndex: 1, + executionTime: 1, data: { main: [ [ diff --git a/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.constants.ts b/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.constants.ts index 0973a0264a..3f29b207b2 100644 --- a/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.constants.ts +++ b/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.constants.ts @@ -278,6 +278,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = { { hints: [], startTime: 1737540693122, + executionIndex: 0, executionTime: 1, source: [], executionStatus: 'success', @@ -287,6 +288,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = { { hints: [], startTime: 1737540693124, + executionIndex: 1, executionTime: 2, source: [ { @@ -300,6 +302,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = { { hints: [], startTime: 1737540693126, + executionIndex: 2, executionTime: 0, source: [ { @@ -313,6 +316,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = { { hints: [], startTime: 1737540693127, + executionIndex: 3, executionTime: 0, source: [ { @@ -326,6 +330,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = { { hints: [], startTime: 1737540693127, + executionIndex: 4, executionTime: 28, source: [ { diff --git a/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.ts b/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.ts index 09a72fc47f..157355d2ab 100644 --- a/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.ts +++ b/packages/frontend/editor-ui/src/composables/useAIAssistantHelpers.test.ts @@ -460,6 +460,7 @@ const testExecutionData: IRunExecutionData['resultData'] = { { hints: [], startTime: 1732882780588, + executionIndex: 0, executionTime: 4, source: [], executionStatus: 'success', @@ -481,6 +482,7 @@ const testExecutionData: IRunExecutionData['resultData'] = { { hints: [], startTime: 1732882780593, + executionIndex: 1, executionTime: 0, source: [ { diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts index 3708ef81e6..2c855ceca3 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts @@ -400,6 +400,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], data: { [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], @@ -443,6 +444,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], data: { [NodeConnectionTypes.Main]: [[{ json: {} }]], @@ -455,6 +457,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], data: { [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], @@ -511,6 +514,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], data: { [NodeConnectionTypes.Main]: [[{ json: {} }]], @@ -519,6 +523,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 1, source: [], data: { [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], @@ -527,6 +532,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 2, source: [], data: { [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], @@ -722,6 +728,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], error: mock({ message: errorMessage, @@ -753,6 +760,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], error: mock({ message: errorMessage, @@ -783,6 +791,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], error: mock({ message: 'Error 1', @@ -792,6 +801,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 1, source: [], error: mock({ message: 'Error 2', @@ -855,6 +865,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], error: mock({ message: 'Execution error', @@ -894,6 +905,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], error: mock({ message: 'Execution error', @@ -948,6 +960,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], executionStatus: 'crashed', }, @@ -976,6 +989,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], executionStatus: 'error', }, @@ -1057,6 +1071,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], executionStatus: 'error', error: mock({ @@ -1096,6 +1111,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], executionStatus: 'error', }, @@ -1104,6 +1120,7 @@ describe('useCanvasMapping', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], executionStatus: 'success', }, diff --git a/packages/frontend/editor-ui/src/composables/useDataSchema.test.ts b/packages/frontend/editor-ui/src/composables/useDataSchema.test.ts index d7828e2df2..d213805466 100644 --- a/packages/frontend/editor-ui/src/composables/useDataSchema.test.ts +++ b/packages/frontend/editor-ui/src/composables/useDataSchema.test.ts @@ -553,7 +553,9 @@ describe('useDataSchema', () => { data: { resultData: { runData: { - [runDataKey ?? name]: [{ data, startTime: 0, executionTime: 0, source: [] }], + [runDataKey ?? name]: [ + { data, startTime: 0, executionTime: 0, executionIndex: 0, source: [] }, + ], }, }, }, @@ -619,17 +621,20 @@ describe('useDataSchema', () => { { startTime: 0, executionTime: 0, + executionIndex: 0, source: [], }, { startTime: 0, executionTime: 0, + executionIndex: 1, source: [], }, { data: { [Main]: [null, mockExecutionDataMarker] }, startTime: 0, executionTime: 0, + executionIndex: 2, source: [], }, ], diff --git a/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts b/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts index 76ac3e422d..f3806d468a 100644 --- a/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts +++ b/packages/frontend/editor-ui/src/composables/useNodeDirtiness.test.ts @@ -156,6 +156,7 @@ describe(useNodeDirtiness, () => { { startTime: +runAt, executionTime: 0, + executionIndex: 0, executionStatus: 'success', source: [], }, @@ -423,6 +424,7 @@ describe(useNodeDirtiness, () => { { startTime: +NODE_RUN_AT, executionTime: 0, + executionIndex: 0, executionStatus: 'success', source: [], }, diff --git a/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts b/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts index 7baca29c22..c575c23e52 100644 --- a/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts +++ b/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts @@ -354,6 +354,7 @@ describe('useRunWorkflow({ router })', () => { [parentName]: [ { startTime: 1, + executionIndex: 0, executionTime: 0, source: [], }, @@ -361,6 +362,7 @@ describe('useRunWorkflow({ router })', () => { [executeName]: [ { startTime: 1, + executionIndex: 1, executionTime: 8, source: [ { diff --git a/packages/frontend/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts b/packages/frontend/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts index 8d5d93da4e..7562640ba8 100644 --- a/packages/frontend/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts +++ b/packages/frontend/editor-ui/src/plugins/codemirror/completions/__tests__/mock.ts @@ -9,6 +9,7 @@ const runExecutionData: IRunExecutionData = { Start: [ { startTime: 1, + executionIndex: 0, executionTime: 1, data: { main: [ @@ -25,6 +26,7 @@ const runExecutionData: IRunExecutionData = { Function: [ { startTime: 1, + executionIndex: 1, executionTime: 1, data: { main: [ @@ -62,6 +64,7 @@ const runExecutionData: IRunExecutionData = { Rename: [ { startTime: 1, + executionIndex: 2, executionTime: 1, data: { main: [ @@ -99,6 +102,7 @@ const runExecutionData: IRunExecutionData = { End: [ { startTime: 1, + executionIndex: 3, executionTime: 1, data: { main: [ diff --git a/packages/frontend/editor-ui/src/stores/workflows.store.test.ts b/packages/frontend/editor-ui/src/stores/workflows.store.test.ts index 55ca261f8e..b7ad5c1161 100644 --- a/packages/frontend/editor-ui/src/stores/workflows.store.test.ts +++ b/packages/frontend/editor-ui/src/stores/workflows.store.test.ts @@ -849,6 +849,7 @@ function generateMockExecutionEvents() { data: { hints: [], startTime: 1727867966633, + executionIndex: 0, executionTime: 1, source: [], executionStatus: 'success', @@ -873,6 +874,7 @@ function generateMockExecutionEvents() { data: { hints: [], startTime: 1727869043441, + executionIndex: 0, executionTime: 2, source: [ { diff --git a/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts b/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts index b3d2220800..aaf58e1f73 100644 --- a/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts +++ b/packages/frontend/editor-ui/src/utils/pairedItemUtils.test.ts @@ -15,6 +15,7 @@ const MOCK_EXECUTION: Partial = { 'When clicking ‘Test workflow’': [ { startTime: 1706027170005, + executionIndex: 0, executionTime: 0, source: [], executionStatus: 'success', @@ -24,6 +25,7 @@ const MOCK_EXECUTION: Partial = { DebugHelper: [ { startTime: 1706027170005, + executionIndex: 1, executionTime: 1, source: [{ previousNode: 'When clicking ‘Test workflow’' }], executionStatus: 'success', @@ -58,6 +60,7 @@ const MOCK_EXECUTION: Partial = { If: [ { startTime: 1706027170006, + executionIndex: 2, executionTime: 1, source: [{ previousNode: 'DebugHelper' }], executionStatus: 'success', @@ -94,6 +97,7 @@ const MOCK_EXECUTION: Partial = { 'Edit Fields': [ { startTime: 1706027170008, + executionIndex: 3, executionTime: 0, source: [{ previousNode: 'If', previousNodeOutput: 1 }], executionStatus: 'success', @@ -116,6 +120,7 @@ const MOCK_EXECUTION: Partial = { }, { startTime: 1706027170009, + executionIndex: 3, executionTime: 0, source: [{ previousNode: 'If' }], executionStatus: 'success', @@ -140,6 +145,7 @@ const MOCK_EXECUTION: Partial = { 'Edit Fields1': [ { startTime: 1706027170008, + executionIndex: 4, executionTime: 0, source: [{ previousNode: 'Edit Fields' }], executionStatus: 'success', @@ -162,6 +168,7 @@ const MOCK_EXECUTION: Partial = { }, { startTime: 1706027170010, + executionIndex: 5, executionTime: 0, source: [{ previousNode: 'Edit Fields', previousNodeRun: 1 }], executionStatus: 'success', @@ -329,6 +336,7 @@ describe('pairedItemUtils', () => { Start: [ { startTime: 1706027170005, + executionIndex: 0, executionTime: 0, source: [], executionStatus: 'success', @@ -340,6 +348,7 @@ describe('pairedItemUtils', () => { DebugHelper: [ { startTime: 1706027170005, + executionIndex: 1, executionTime: 1, source: [{ previousNode: 'Start' }], executionStatus: 'success', @@ -409,8 +418,9 @@ describe('pairedItemUtils', () => { runData: Object.fromEntries( Array.from({ length: nodeCount }).map<[string, ITaskData[]]>((_, j) => [ `node_${j}`, - Array.from({ length: runCount }).map(() => ({ + Array.from({ length: runCount }).map((_, executionIndex) => ({ startTime: 1706027170005, + executionIndex, executionTime: 0, source: j === 0 ? [] : [{ previousNode: `node_${j - 1}` }], executionStatus: 'success', diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index e246c9f48c..91b9e5f72f 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2180,16 +2180,22 @@ export interface ITaskMetadata { subExecutionsCount?: number; } -// The data that gets returned when a node runs -export interface ITaskData { +/** The data that gets returned when a node execution starts */ +export interface ITaskStartedData { startTime: number; + /** This index tracks the order in which nodes are executed */ + executionIndex: number; + source: Array; // Is an array as nodes have multiple inputs + hints?: NodeExecutionHint[]; +} + +/** The data that gets returned when a node execution ends */ +export interface ITaskData extends ITaskStartedData { executionTime: number; executionStatus?: ExecutionStatus; data?: ITaskDataConnections; inputOverride?: ITaskDataConnections; error?: ExecutionError; - hints?: NodeExecutionHint[]; - source: Array; // Is an array as nodes have multiple inputs metadata?: ITaskMetadata; } @@ -2336,6 +2342,7 @@ export interface IWorkflowExecuteAdditionalData { ) => Promise; executionId?: string; restartExecutionId?: string; + currentNodeExecutionIndex: number; httpResponse?: express.Response; httpRequest?: express.Request; restApiUrl: string; diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 576e870a1b..3fa89764b1 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -916,7 +916,15 @@ export class WorkflowDataProxy { ); if (pinData) { - taskData = { data: { main: [pinData] }, startTime: 0, executionTime: 0, source: [] }; + taskData = { + data: { + main: [pinData], + }, + startTime: 0, + executionTime: 0, + executionIndex: 0, + source: [], + }; } } diff --git a/packages/workflow/test/TelemetryHelpers.test.ts b/packages/workflow/test/TelemetryHelpers.test.ts index 0b99a4d907..f951fe804d 100644 --- a/packages/workflow/test/TelemetryHelpers.test.ts +++ b/packages/workflow/test/TelemetryHelpers.test.ts @@ -1512,6 +1512,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340927, executionTime: 0, + executionIndex: 0, source: [], executionStatus: 'success', data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] }, @@ -1522,6 +1523,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340928, executionTime: 0, + executionIndex: 1, source: [{ previousNode: 'Execute Workflow Trigger' }], executionStatus: 'success', data: { @@ -1555,6 +1557,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340928, executionTime: 1, + executionIndex: 2, source: [{ previousNode: 'DebugHelper' }], executionStatus: 'success', data: { @@ -1586,6 +1589,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340931, executionTime: 0, + executionIndex: 3, source: [{ previousNode: 'Execute Workflow Trigger' }], executionStatus: 'success', data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] }, @@ -1596,6 +1600,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340929, executionTime: 1, + executionIndex: 4, source: [{ previousNode: 'Edit Fields' }], executionStatus: 'success', data: { @@ -1630,6 +1635,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340931, executionTime: 0, + executionIndex: 5, source: [{ previousNode: 'Edit Fields', previousNodeRun: 1 }], executionStatus: 'success', data: { main: [[], [], [{ json: {}, pairedItem: { item: 0 } }], []] }, @@ -1640,6 +1646,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340930, executionTime: 0, + executionIndex: 6, source: [{ previousNode: 'Switch', previousNodeOutput: 2 }], executionStatus: 'success', data: { @@ -1656,6 +1663,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial; r hints: [], startTime: 1727793340932, executionTime: 1, + executionIndex: 7, source: [{ previousNode: 'Switch', previousNodeOutput: 2, previousNodeRun: 1 }], executionStatus: 'success', data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] }, diff --git a/packages/workflow/test/Workflow.test.ts b/packages/workflow/test/Workflow.test.ts index 444910aa40..41c0c600f5 100644 --- a/packages/workflow/test/Workflow.test.ts +++ b/packages/workflow/test/Workflow.test.ts @@ -1603,6 +1603,7 @@ describe('Workflow', () => { ], startTime: 1, executionTime: 1, + executionIndex: 0, data: { main: [ [ @@ -1681,6 +1682,7 @@ describe('Workflow', () => { { startTime: 1, executionTime: 1, + executionIndex: 0, data: { main: [ [