refactor(core): Persist node execution order, and forward it to the frontend (#14455)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-04-09 10:19:58 +02:00
committed by GitHub
parent 707ecb63ae
commit 9ba58ca80b
41 changed files with 235 additions and 113 deletions

View File

@@ -16,7 +16,8 @@ export function createMockNodeExecutionData(
): Record<string, ITaskData> { ): Record<string, ITaskData> {
return { return {
[name]: { [name]: {
startTime: new Date().getTime(), startTime: Date.now(),
executionIndex: 0,
executionTime: 1, executionTime: 1,
executionStatus, executionStatus,
data: jsonData data: jsonData
@@ -77,6 +78,7 @@ export function runMockWorkflowExecution({
cy.push('nodeExecuteBefore', { cy.push('nodeExecuteBefore', {
executionId, executionId,
nodeName, nodeName,
data: nodeRunData,
}); });
cy.push('nodeExecuteAfter', { cy.push('nodeExecuteAfter', {
executionId, executionId,

View File

@@ -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 = {
type: 'executionStarted'; type: 'executionStarted';
@@ -43,6 +48,7 @@ type NodeExecuteBefore = {
data: { data: {
executionId: string; executionId: string;
nodeName: string; nodeName: string;
data: ITaskStartedData;
}; };
}; };

View File

@@ -40,6 +40,7 @@ export const newNode = (opts: Partial<INode> = {}): INode => ({
export const newTaskData = (opts: Partial<ITaskData> & Pick<ITaskData, 'source'>): ITaskData => ({ export const newTaskData = (opts: Partial<ITaskData> & Pick<ITaskData, 'source'>): ITaskData => ({
startTime: Date.now(), startTime: Date.now(),
executionTime: 0, executionTime: 0,
executionIndex: 0,
executionStatus: 'success', executionStatus: 'success',
...opts, ...opts,
}); });

View File

@@ -17,6 +17,7 @@ import type {
INode, INode,
IWorkflowBase, IWorkflowBase,
WorkflowExecuteMode, WorkflowExecuteMode,
ITaskStartedData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
@@ -68,6 +69,7 @@ describe('Execution Lifecycle Hooks', () => {
}; };
const workflow = mock<Workflow>(); const workflow = mock<Workflow>();
const staticData = mock<IDataObject>(); const staticData = mock<IDataObject>();
const taskStartedData = mock<ITaskStartedData>();
const taskData = mock<ITaskData>(); const taskData = mock<ITaskData>();
const runExecutionData = mock<IRunExecutionData>(); const runExecutionData = mock<IRunExecutionData>();
const successfulRun = mock<IRun>({ const successfulRun = mock<IRun>({
@@ -146,7 +148,7 @@ describe('Execution Lifecycle Hooks', () => {
const nodeEventsTests = () => { const nodeEventsTests = () => {
describe('nodeExecuteBefore', () => { describe('nodeExecuteBefore', () => {
it('should emit node-pre-execute event', async () => { 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', { expect(eventService.emit).toHaveBeenCalledWith('node-pre-execute', {
executionId, executionId,
@@ -246,10 +248,10 @@ describe('Execution Lifecycle Hooks', () => {
describe('nodeExecuteBefore', () => { describe('nodeExecuteBefore', () => {
it('should send nodeExecuteBefore push event', async () => { it('should send nodeExecuteBefore push event', async () => {
await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName]); await lifecycleHooks.runHook('nodeExecuteBefore', [nodeName, taskStartedData]);
expect(push.send).toHaveBeenCalledWith( expect(push.send).toHaveBeenCalledWith(
{ type: 'nodeExecuteBefore', data: { executionId, nodeName } }, { type: 'nodeExecuteBefore', data: { executionId, nodeName, data: taskStartedData } },
pushRef, pushRef,
); );
}); });
@@ -471,8 +473,9 @@ describe('Execution Lifecycle Hooks', () => {
(successfulRun.data.resultData.runData = { (successfulRun.data.resultData.runData = {
[nodeName]: [ [nodeName]: [
{ {
executionTime: 1,
startTime: 1, startTime: 1,
executionIndex: 0,
executionTime: 1,
source: [], source: [],
data: { data: {
main: [ main: [
@@ -517,7 +520,7 @@ describe('Execution Lifecycle Hooks', () => {
expect(handlers.workflowExecuteBefore).toHaveLength(2); expect(handlers.workflowExecuteBefore).toHaveLength(2);
expect(handlers.workflowExecuteAfter).toHaveLength(4); 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('nodeExecuteAfter', [nodeName, taskData, runExecutionData]);
await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]); await lifecycleHooks.runHook('workflowExecuteBefore', [workflow, runExecutionData]);
await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]); await lifecycleHooks.runHook('workflowExecuteAfter', [successfulRun, {}]);

View File

@@ -68,7 +68,7 @@ function hookFunctionsPush(
if (!pushRef) return; if (!pushRef) return;
const logger = Container.get(Logger); const logger = Container.get(Logger);
const pushInstance = Container.get(Push); const pushInstance = Container.get(Push);
hooks.addHandler('nodeExecuteBefore', function (nodeName) { hooks.addHandler('nodeExecuteBefore', function (nodeName, data) {
const { executionId } = this; const { executionId } = this;
// Push data to session which started workflow before each // Push data to session which started workflow before each
// node which starts rendering // node which starts rendering
@@ -78,7 +78,10 @@ function hookFunctionsPush(
workflowId: this.workflowData.id, 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) { hooks.addHandler('nodeExecuteAfter', function (nodeName, data) {
const { executionId } = this; const { executionId } = this;

View File

@@ -37,6 +37,7 @@ export class ExecutionDataService {
returnData.data.resultData.runData[node.name] = [ returnData.data.resultData.runData[node.name] = [
{ {
startTime, startTime,
executionIndex: 0,
executionTime: 0, executionTime: 0,
executionStatus: 'error', executionStatus: 'error',
error: executionError, error: executionError,

View File

@@ -94,6 +94,7 @@ export class ExecutionRecoveryService {
const taskData: ITaskData = { const taskData: ITaskData = {
startTime: nodeStartedMessage.ts.toUnixInteger(), startTime: nodeStartedMessage.ts.toUnixInteger(),
executionIndex: 0,
executionTime: -1, executionTime: -1,
source: [null], source: [null],
}; };

View File

@@ -311,6 +311,7 @@ export class ExecutionService {
[node.name]: [ [node.name]: [
{ {
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
error, error,
source: [], source: [],

View File

@@ -110,6 +110,7 @@ const taskData: DataRequestResponse = {
{ {
hints: [], hints: [],
startTime: 1730313407328, startTime: 1730313407328,
executionIndex: 0,
executionTime: 1, executionTime: 1,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
@@ -122,6 +123,7 @@ const taskData: DataRequestResponse = {
{ {
hints: [], hints: [],
startTime: 1730313407330, startTime: 1730313407330,
executionIndex: 1,
executionTime: 1, executionTime: 1,
source: [ source: [
{ {

View File

@@ -373,6 +373,7 @@ export async function getBase(
const eventService = Container.get(EventService); const eventService = Container.get(EventService);
return { return {
currentNodeExecutionIndex: 0,
credentialsHelper: Container.get(CredentialsHelper), credentialsHelper: Container.get(CredentialsHelper),
executeWorkflow, executeWorkflow,
restApiUrl: urlBaseWebhook + globalConfig.endpoints.rest, restApiUrl: urlBaseWebhook + globalConfig.endpoints.rest,

View File

@@ -39,6 +39,7 @@ export function getDataLastExecutedNodeData(inputData: IRun): ITaskData | undefi
return { return {
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
data: { main: [itemsPerRun] }, data: { main: [itemsPerRun] },
source: lastNodeRunData.source, source: lastNodeRunData.source,

View File

@@ -108,6 +108,7 @@ describe('JS TaskRunner execution on internal mode', () => {
ManualTrigger: [ ManualTrigger: [
{ {
startTime: Date.now(), startTime: Date.now(),
executionIndex: 0,
executionTime: 0, executionTime: 0,
executionStatus: 'success', executionStatus: 'success',
source: [], source: [],

View File

@@ -6,6 +6,7 @@ import type {
IRun, IRun,
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
ITaskStartedData,
IWorkflowBase, IWorkflowBase,
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
@@ -52,7 +53,7 @@ describe('ExecutionLifecycleHooks', () => {
hook: ExecutionLifecycleHookName; hook: ExecutionLifecycleHookName;
args: Parameters<ExecutionLifecyleHookHandlers[keyof ExecutionLifecyleHookHandlers][number]>; args: Parameters<ExecutionLifecyleHookHandlers[keyof ExecutionLifecyleHookHandlers][number]>;
}> = [ }> = [
{ hook: 'nodeExecuteBefore', args: ['testNode'] }, { hook: 'nodeExecuteBefore', args: ['testNode', mock<ITaskStartedData>()] },
{ {
hook: 'nodeExecuteAfter', hook: 'nodeExecuteAfter',
args: ['testNode', mock<ITaskData>(), mock<IRunExecutionData>()], args: ['testNode', mock<ITaskData>(), mock<IRunExecutionData>()],
@@ -84,7 +85,7 @@ describe('ExecutionLifecycleHooks', () => {
}); });
hooks.addHandler('nodeExecuteBefore', hook1, hook2); hooks.addHandler('nodeExecuteBefore', hook1, hook2);
await hooks.runHook('nodeExecuteBefore', ['testNode']); await hooks.runHook('nodeExecuteBefore', ['testNode', mock()]);
expect(executionOrder).toEqual(['hook1', 'hook2']); expect(executionOrder).toEqual(['hook1', 'hook2']);
expect(hook1).toHaveBeenCalled(); expect(hook1).toHaveBeenCalled();
@@ -98,7 +99,7 @@ describe('ExecutionLifecycleHooks', () => {
}); });
hooks.addHandler('nodeExecuteBefore', hook); hooks.addHandler('nodeExecuteBefore', hook);
await hooks.runHook('nodeExecuteBefore', ['testNode']); await hooks.runHook('nodeExecuteBefore', ['testNode', mock()]);
expect(hook).toHaveBeenCalled(); expect(hook).toHaveBeenCalled();
}); });
@@ -107,7 +108,9 @@ describe('ExecutionLifecycleHooks', () => {
const errorHook = jest.fn().mockRejectedValue(new Error('Hook failed')); const errorHook = jest.fn().mockRejectedValue(new Error('Hook failed'));
hooks.addHandler('nodeExecuteBefore', errorHook); 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',
);
}); });
}); });
}); });

View File

@@ -77,11 +77,7 @@ describe('WorkflowExecute', () => {
}); });
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(
waitPromise,
nodeExecutionOrder,
);
const workflowExecute = new WorkflowExecute(additionalData, executionMode); const workflowExecute = new WorkflowExecute(additionalData, executionMode);
@@ -110,6 +106,12 @@ describe('WorkflowExecute', () => {
} }
// Check if the nodes did execute in the correct order // 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); expect(nodeExecutionOrder).toEqual(testData.output.nodeExecutionOrder);
// Check if other data has correct value // Check if other data has correct value
@@ -140,11 +142,7 @@ describe('WorkflowExecute', () => {
}); });
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(
waitPromise,
nodeExecutionOrder,
);
const workflowExecute = new WorkflowExecute(additionalData, executionMode); const workflowExecute = new WorkflowExecute(additionalData, executionMode);
@@ -177,6 +175,12 @@ describe('WorkflowExecute', () => {
} }
// Check if the nodes did execute in the correct order // 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); expect(nodeExecutionOrder).toEqual(testData.output.nodeExecutionOrder);
// Check if other data has correct value // Check if other data has correct value
@@ -207,11 +211,7 @@ describe('WorkflowExecute', () => {
}); });
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(
waitPromise,
nodeExecutionOrder,
);
const workflowExecute = new WorkflowExecute(additionalData, executionMode); const workflowExecute = new WorkflowExecute(additionalData, executionMode);
@@ -259,8 +259,7 @@ describe('WorkflowExecute', () => {
test("deletes dirty nodes' run data", async () => { test("deletes dirty nodes' run data", async () => {
// ARRANGE // ARRANGE
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const workflowExecute = new WorkflowExecute(additionalData, 'manual');
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); 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 () => { test('deletes run data of children of dirty nodes as well', async () => {
// ARRANGE // ARRANGE
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const workflowExecute = new WorkflowExecute(additionalData, 'manual');
jest.spyOn(workflowExecute, 'processRunExecutionData').mockImplementationOnce(jest.fn()); jest.spyOn(workflowExecute, 'processRunExecutionData').mockImplementationOnce(jest.fn());
@@ -369,8 +367,7 @@ describe('WorkflowExecute', () => {
test('removes disabled nodes from the workflow', async () => { test('removes disabled nodes from the workflow', async () => {
// ARRANGE // ARRANGE
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const workflowExecute = new WorkflowExecute(additionalData, 'manual');
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' });
@@ -423,8 +420,7 @@ describe('WorkflowExecute', () => {
test('passes filtered run data to `recreateNodeExecutionStack`', async () => { test('passes filtered run data to `recreateNodeExecutionStack`', async () => {
// ARRANGE // ARRANGE
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const workflowExecute = new WorkflowExecute(additionalData, 'manual');
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' });
@@ -486,8 +482,7 @@ describe('WorkflowExecute', () => {
test('passes subgraph to `cleanRunData`', async () => { test('passes subgraph to `cleanRunData`', async () => {
// ARRANGE // ARRANGE
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const workflowExecute = new WorkflowExecute(additionalData, 'manual');
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' });
@@ -547,8 +542,7 @@ describe('WorkflowExecute', () => {
test('passes pruned dirty nodes to `cleanRunData`', async () => { test('passes pruned dirty nodes to `cleanRunData`', async () => {
// ARRANGE // ARRANGE
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const workflowExecute = new WorkflowExecute(additionalData, 'manual');
const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' }); const trigger = createNodeData({ name: 'trigger', type: 'n8n-nodes-base.manualTrigger' });
@@ -599,8 +593,7 @@ describe('WorkflowExecute', () => {
test('works with a single node', async () => { test('works with a single node', async () => {
// ARRANGE // ARRANGE
const waitPromise = createDeferredPromise<IRun>(); const waitPromise = createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise, nodeExecutionOrder);
const workflowExecute = new WorkflowExecute(additionalData, 'manual'); const workflowExecute = new WorkflowExecute(additionalData, 'manual');
const trigger = createNodeData({ name: 'trigger' }); const trigger = createNodeData({ name: 'trigger' });
@@ -856,6 +849,7 @@ describe('WorkflowExecute', () => {
}, },
source: [], source: [],
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
}, },
], ],
@@ -1132,6 +1126,7 @@ describe('WorkflowExecute', () => {
source: [], source: [],
data: { main: [[], []] }, data: { main: [[], []] },
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
}, },
], ],
@@ -1165,6 +1160,7 @@ describe('WorkflowExecute', () => {
main: [[{ json: { data: 'test' } }], []], main: [[{ json: { data: 'test' } }], []],
}, },
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
}, },
], ],
@@ -1188,6 +1184,7 @@ describe('WorkflowExecute', () => {
main: [[]], main: [[]],
}, },
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
}, },
{ {
@@ -1196,6 +1193,7 @@ describe('WorkflowExecute', () => {
main: [[{ json: { data: 'test' } }]], main: [[{ json: { data: 'test' } }]],
}, },
startTime: 0, startTime: 0,
executionIndex: 1,
executionTime: 0, executionTime: 0,
}, },
], ],
@@ -1215,6 +1213,7 @@ describe('WorkflowExecute', () => {
{ {
source: [], source: [],
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
}, },
], ],
@@ -1254,7 +1253,7 @@ describe('WorkflowExecute', () => {
test('should do nothing when there is no metadata', () => { test('should do nothing when there is no metadata', () => {
runExecutionData.resultData.runData = { runExecutionData.resultData.runData = {
node1: [{ startTime: 0, executionTime: 0, source: [] }], node1: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 0 }],
}; };
workflowExecute.moveNodeMetadata(); workflowExecute.moveNodeMetadata();
@@ -1264,7 +1263,7 @@ describe('WorkflowExecute', () => {
test('should merge metadata into runData for single node', () => { test('should merge metadata into runData for single node', () => {
runExecutionData.resultData.runData = { runExecutionData.resultData.runData = {
node1: [{ startTime: 0, executionTime: 0, source: [] }], node1: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 0 }],
}; };
runExecutionData.executionData!.metadata = { runExecutionData.executionData!.metadata = {
node1: [{ parentExecution }], node1: [{ parentExecution }],
@@ -1277,8 +1276,8 @@ describe('WorkflowExecute', () => {
test('should merge metadata into runData for multiple nodes', () => { test('should merge metadata into runData for multiple nodes', () => {
runExecutionData.resultData.runData = { runExecutionData.resultData.runData = {
node1: [{ startTime: 0, executionTime: 0, source: [] }], node1: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 0 }],
node2: [{ startTime: 0, executionTime: 0, source: [] }], node2: [{ startTime: 0, executionTime: 0, source: [], executionIndex: 1 }],
}; };
runExecutionData.executionData!.metadata = { runExecutionData.executionData!.metadata = {
node1: [{ parentExecution }], node1: [{ parentExecution }],
@@ -1297,6 +1296,7 @@ describe('WorkflowExecute', () => {
node1: [ node1: [
{ {
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 0, executionTime: 0,
source: [], source: [],
metadata: { subExecutionsCount: 4 }, metadata: { subExecutionsCount: 4 },
@@ -1318,8 +1318,8 @@ describe('WorkflowExecute', () => {
test('should handle multiple run indices', () => { test('should handle multiple run indices', () => {
runExecutionData.resultData.runData = { runExecutionData.resultData.runData = {
node1: [ node1: [
{ startTime: 0, executionTime: 0, source: [] }, { startTime: 0, executionTime: 0, source: [], executionIndex: 0 },
{ startTime: 0, executionTime: 0, source: [] }, { startTime: 0, executionTime: 0, source: [], executionIndex: 1 },
], ],
}; };
runExecutionData.executionData!.metadata = { runExecutionData.executionData!.metadata = {

View File

@@ -5,6 +5,7 @@ import type {
IRun, IRun,
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
ITaskStartedData,
IWorkflowBase, IWorkflowBase,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
@@ -12,7 +13,11 @@ import type {
export type ExecutionLifecyleHookHandlers = { export type ExecutionLifecyleHookHandlers = {
nodeExecuteBefore: Array< nodeExecuteBefore: Array<
(this: ExecutionLifecycleHooks, nodeName: string) => Promise<void> | void (
this: ExecutionLifecycleHooks,
nodeName: string,
data: ITaskStartedData,
) => Promise<void> | void
>; >;
nodeExecuteAfter: Array< nodeExecuteAfter: Array<

View File

@@ -232,8 +232,9 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
let taskData: ITaskData | undefined; let taskData: ITaskData | undefined;
if (type === 'input') { if (type === 'input') {
taskData = { taskData = {
startTime: new Date().getTime(), startTime: Date.now(),
executionTime: 0, executionTime: 0,
executionIndex: additionalData.currentNodeExecutionIndex++,
executionStatus: 'running', executionStatus: 'running',
source: [null], source: [null],
}; };
@@ -277,10 +278,10 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
} }
runExecutionData.resultData.runData[nodeName][currentNodeRunIndex] = taskData; runExecutionData.resultData.runData[nodeName][currentNodeRunIndex] = taskData;
await additionalData.hooks?.runHook('nodeExecuteBefore', [nodeName]); await additionalData.hooks?.runHook('nodeExecuteBefore', [nodeName, taskData]);
} else { } else {
// Outputs // Outputs
taskData.executionTime = new Date().getTime() - taskData.startTime; taskData.executionTime = Date.now() - taskData.startTime;
await additionalData.hooks?.runHook('nodeExecuteAfter', [ await additionalData.hooks?.runHook('nodeExecuteAfter', [
nodeName, nodeName,

View File

@@ -557,6 +557,7 @@ describe('findStartNodes', () => {
executionStatus: 'success', executionStatus: 'success',
executionTime: 0, executionTime: 0,
startTime: 0, startTime: 0,
executionIndex: 0,
source: [], source: [],
data: { main: [[], [{ json: { name: 'loop' } }]] }, data: { main: [[], [{ json: { name: 'loop' } }]] },
}, },

View File

@@ -38,6 +38,7 @@ export function toITaskData(taskData: TaskData[]): ITaskData {
executionStatus: 'success', executionStatus: 'success',
executionTime: 0, executionTime: 0,
startTime: 0, startTime: 0,
executionIndex: 0,
source: [], source: [],
data: {}, data: {},
}; };

View File

@@ -8,6 +8,7 @@ test('toITaskData', function () {
executionTime: 0, executionTime: 0,
source: [], source: [],
startTime: 0, startTime: 0,
executionIndex: 0,
data: { data: {
main: [[{ json: { value: 1 } }]], main: [[{ json: { value: 1 } }]],
}, },
@@ -18,6 +19,7 @@ test('toITaskData', function () {
executionTime: 0, executionTime: 0,
source: [], source: [],
startTime: 0, startTime: 0,
executionIndex: 0,
data: { data: {
main: [null, [{ json: { value: 1 } }]], main: [null, [{ json: { value: 1 } }]],
}, },
@@ -32,6 +34,7 @@ test('toITaskData', function () {
executionTime: 0, executionTime: 0,
source: [], source: [],
startTime: 0, startTime: 0,
executionIndex: 0,
data: { data: {
[NodeConnectionTypes.AiAgent]: [null, [{ json: { value: 1 } }]], [NodeConnectionTypes.AiAgent]: [null, [{ json: { value: 1 } }]],
}, },
@@ -46,6 +49,7 @@ test('toITaskData', function () {
executionStatus: 'success', executionStatus: 'success',
executionTime: 0, executionTime: 0,
startTime: 0, startTime: 0,
executionIndex: 0,
source: [], source: [],
data: { data: {
main: [ main: [

View File

@@ -35,11 +35,11 @@ import type {
WorkflowExecuteMode, WorkflowExecuteMode,
CloseFunction, CloseFunction,
StartNodeData, StartNodeData,
NodeExecutionHint,
IRunNodeResponse, IRunNodeResponse,
IWorkflowIssues, IWorkflowIssues,
INodeIssues, INodeIssues,
INodeType, INodeType,
ITaskStartedData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
LoggerProxy as Logger, LoggerProxy as Logger,
@@ -1304,11 +1304,8 @@ export class WorkflowExecute {
// Variables which hold temporary data for each node-execution // Variables which hold temporary data for each node-execution
let executionData: IExecuteData; let executionData: IExecuteData;
let executionError: ExecutionBaseError | undefined; let executionError: ExecutionBaseError | undefined;
let executionHints: NodeExecutionHint[] = [];
let executionNode: INode; let executionNode: INode;
let nodeSuccessData: INodeExecutionData[][] | null | undefined;
let runIndex: number; let runIndex: number;
let startTime: number;
if (this.runExecutionData.startData === undefined) { if (this.runExecutionData.startData === undefined) {
this.runExecutionData.startData = {}; this.runExecutionData.startData = {};
@@ -1356,19 +1353,20 @@ export class WorkflowExecute {
// Set the incoming data of the node that it can be saved correctly // Set the incoming data of the node that it can be saved correctly
executionData = this.runExecutionData.executionData!.nodeExecutionStack[0]; 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 = { this.runExecutionData.resultData = {
runData: { runData: {
[executionData.node.name]: [ [executionData.node.name]: [taskData],
{
startTime,
executionTime: new Date().getTime() - startTime,
data: {
main: executionData.data.main,
} as ITaskDataConnections,
source: [],
executionStatus: 'error',
},
],
}, },
lastNodeExecuted: executionData.node.name, lastNodeExecuted: executionData.node.name,
error: executionError, error: executionError,
@@ -1391,13 +1389,19 @@ export class WorkflowExecute {
return; return;
} }
nodeSuccessData = null; let nodeSuccessData: INodeExecutionData[][] | null | undefined = null;
executionError = undefined; executionError = undefined;
executionHints = [];
executionData = executionData =
this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node; 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 // Update the pairedItem information on items
const newTaskDataConnections: ITaskDataConnections = {}; const newTaskDataConnections: ITaskDataConnections = {};
for (const connectionType of Object.keys(executionData.data)) { for (const connectionType of Object.keys(executionData.data)) {
@@ -1425,7 +1429,7 @@ export class WorkflowExecute {
node: executionNode.name, node: executionNode.name,
workflowId: workflow.id, workflowId: workflow.id,
}); });
await hooks.runHook('nodeExecuteBefore', [executionNode.name]); await hooks.runHook('nodeExecuteBefore', [executionNode.name, taskStartedData]);
// Get the index of the current run // Get the index of the current run
runIndex = 0; runIndex = 0;
@@ -1457,8 +1461,6 @@ export class WorkflowExecute {
continue executionLoop; continue executionLoop;
} }
startTime = new Date().getTime();
let maxTries = 1; let maxTries = 1;
if (executionData.node.retryOnFail === true) { if (executionData.node.retryOnFail === true) {
// TODO: Remove the hardcoded default-values here and also in NodeSettings.vue // TODO: Remove the hardcoded default-values here and also in NodeSettings.vue
@@ -1534,7 +1536,7 @@ export class WorkflowExecute {
} }
if (runNodeData.hints?.length) { if (runNodeData.hints?.length) {
executionHints.push(...runNodeData.hints); taskStartedData.hints!.push(...runNodeData.hints);
} }
if (nodeSuccessData && executionData.node.onError === 'continueErrorOutput') { if (nodeSuccessData && executionData.node.onError === 'continueErrorOutput') {
@@ -1631,10 +1633,8 @@ export class WorkflowExecute {
} }
const taskData: ITaskData = { const taskData: ITaskData = {
hints: executionHints, ...taskStartedData,
startTime, executionTime: Date.now() - taskStartedData.startTime,
executionTime: new Date().getTime() - startTime,
source: !executionData.source ? [] : executionData.source.main,
metadata: executionData.metadata, metadata: executionData.metadata,
executionStatus: this.runExecutionData.waitTill ? 'waiting' : 'success', executionStatus: this.runExecutionData.waitTill ? 'waiting' : 'success',
}; };

View File

@@ -51,14 +51,10 @@ export function NodeTypes(nodeTypes: INodeTypeData = predefinedNodesTypes): INod
export function WorkflowExecuteAdditionalData( export function WorkflowExecuteAdditionalData(
waitPromise: IDeferredPromise<IRun>, waitPromise: IDeferredPromise<IRun>,
nodeExecutionOrder: string[],
): IWorkflowExecuteAdditionalData { ): IWorkflowExecuteAdditionalData {
const hooks = new ExecutionLifecycleHooks('trigger', '1', mock()); const hooks = new ExecutionLifecycleHooks('trigger', '1', mock());
hooks.addHandler('nodeExecuteAfter', (nodeName) => {
nodeExecutionOrder.push(nodeName);
});
hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData)); hooks.addHandler('workflowExecuteAfter', (fullRunData) => waitPromise.resolve(fullRunData));
return mock<IWorkflowExecuteAdditionalData>({ hooks }); return mock<IWorkflowExecuteAdditionalData>({ hooks, currentNodeExecutionIndex: 0 });
} }
const preparePinData = (pinData: IDataObject) => { const preparePinData = (pinData: IDataObject) => {

View File

@@ -256,6 +256,7 @@ describe('CanvasChat', () => {
], ],
], ],
}, },
executionIndex: 0,
executionStatus: 'success', executionStatus: 'success',
executionTime: 0, executionTime: 0,
source: [null], source: [null],

View File

@@ -71,7 +71,8 @@ export const aiChatExecutionResponse: IExecutionResponse = {
'AI Agent': [ 'AI Agent': [
{ {
executionStatus: 'success', executionStatus: 'success',
startTime: +new Date('2025-03-26T00:00:00.002Z'), startTime: Date.parse('2025-03-26T00:00:00.002Z'),
executionIndex: 0,
executionTime: 1778, executionTime: 1778,
source: [], source: [],
data: {}, data: {},
@@ -80,7 +81,8 @@ export const aiChatExecutionResponse: IExecutionResponse = {
'AI Model': [ 'AI Model': [
{ {
executionStatus: 'error', executionStatus: 'error',
startTime: +new Date('2025-03-26T00:00:00.003Z'), startTime: Date.parse('2025-03-26T00:00:00.003Z'),
executionIndex: 1,
executionTime: 1777, executionTime: 1777,
source: [], source: [],
error: new WorkflowOperationError('Test error', aiModelNode, 'Test error description'), error: new WorkflowOperationError('Test error', aiModelNode, 'Test error description'),
@@ -121,7 +123,8 @@ export const aiManualExecutionResponse: IExecutionResponse = {
'AI Agent': [ 'AI Agent': [
{ {
executionStatus: 'success', executionStatus: 'success',
startTime: +new Date('2025-03-30T00:00:00.002Z'), startTime: Date.parse('2025-03-30T00:00:00.002Z'),
executionIndex: 0,
executionTime: 12, executionTime: 12,
source: [], source: [],
data: {}, data: {},
@@ -130,7 +133,8 @@ export const aiManualExecutionResponse: IExecutionResponse = {
'AI Model': [ 'AI Model': [
{ {
executionStatus: 'success', executionStatus: 'success',
startTime: +new Date('2025-03-30T00:00:00.003Z'), startTime: Date.parse('2025-03-30T00:00:00.003Z'),
executionIndex: 1,
executionTime: 3456, executionTime: 3456,
source: [], source: [],
data: { data: {

View File

@@ -130,8 +130,9 @@ export function useChatMessaging({
inputPayload.binary = binaryData; inputPayload.binary = binaryData;
} }
const nodeData: ITaskData = { const nodeData: ITaskData = {
startTime: new Date().getTime(), startTime: Date.now(),
executionTime: 0, executionTime: 0,
executionIndex: 0,
executionStatus: 'success', executionStatus: 'success',
data: { data: {
main: [[inputPayload]], main: [[inputPayload]],

View File

@@ -128,6 +128,7 @@ describe('InputPanel', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
data: {}, data: {},
}, },

View File

@@ -359,8 +359,9 @@ describe('RunData', () => {
const { getByTestId, queryByTestId } = render({ const { getByTestId, queryByTestId } = render({
runs: [ runs: [
{ {
startTime: new Date().getTime(), startTime: Date.now(),
executionTime: new Date().getTime(), executionIndex: 0,
executionTime: 1,
data: { data: {
main: [[{ json: {} }]], main: [[{ json: {} }]],
}, },
@@ -368,8 +369,9 @@ describe('RunData', () => {
metadata, metadata,
}, },
{ {
startTime: new Date().getTime(), startTime: Date.now(),
executionTime: new Date().getTime(), executionIndex: 1,
executionTime: 1,
data: { data: {
main: [[{ json: {} }]], main: [[{ json: {} }]],
}, },
@@ -413,6 +415,7 @@ describe('RunData', () => {
{ {
hints: [], hints: [],
startTime: 1737643696893, startTime: 1737643696893,
executionIndex: 0,
executionTime: 2, executionTime: 2,
source: [ source: [
{ {
@@ -598,8 +601,9 @@ describe('RunData', () => {
runs?: ITaskData[]; runs?: ITaskData[];
}) => { }) => {
const defaultRun: ITaskData = { const defaultRun: ITaskData = {
startTime: new Date().getTime(), startTime: Date.now(),
executionTime: new Date().getTime(), executionIndex: 0,
executionTime: 1,
data: { data: {
main: [defaultRunItems ?? [{ json: {} }]], main: [defaultRunItems ?? [{ json: {} }]],
}, },

View File

@@ -6,6 +6,7 @@ describe(getTreeNodeData, () => {
function createTaskData(partialData: Partial<ITaskData>): ITaskData { function createTaskData(partialData: Partial<ITaskData>): ITaskData {
return { return {
startTime: 0, startTime: 0,
executionIndex: 0,
executionTime: 1, executionTime: 1,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
@@ -29,10 +30,10 @@ describe(getTreeNodeData, () => {
}, },
}); });
const taskDataByNodeName: Record<string, ITaskData[]> = { const taskDataByNodeName: Record<string, ITaskData[]> = {
A: [createTaskData({ startTime: +new Date('2025-02-26T00:00:00.000Z') })], A: [createTaskData({ startTime: Date.parse('2025-02-26T00:00:00.000Z') })],
B: [ B: [
createTaskData({ createTaskData({
startTime: +new Date('2025-02-26T00:00:01.000Z'), startTime: Date.parse('2025-02-26T00:00:01.000Z'),
data: { data: {
main: [ main: [
[ [
@@ -50,7 +51,7 @@ describe(getTreeNodeData, () => {
}, },
}), }),
createTaskData({ createTaskData({
startTime: +new Date('2025-02-26T00:00:03.000Z'), startTime: Date.parse('2025-02-26T00:00:03.000Z'),
data: { data: {
main: [ main: [
[ [
@@ -70,7 +71,7 @@ describe(getTreeNodeData, () => {
], ],
C: [ C: [
createTaskData({ createTaskData({
startTime: +new Date('2025-02-26T00:00:02.000Z'), startTime: Date.parse('2025-02-26T00:00:02.000Z'),
data: { data: {
main: [ 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', id: 'B',
node: 'B', node: 'B',
runIndex: 0, 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' }), parent: expect.objectContaining({ node: 'A' }),
consumedTokens: { consumedTokens: {
completionTokens: 1, completionTokens: 1,
@@ -132,7 +133,7 @@ describe(getTreeNodeData, () => {
id: 'C', id: 'C',
node: 'C', node: 'C',
runIndex: 0, 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' }), parent: expect.objectContaining({ node: 'B' }),
consumedTokens: { consumedTokens: {
completionTokens: 7, completionTokens: 7,
@@ -148,7 +149,7 @@ describe(getTreeNodeData, () => {
id: 'B', id: 'B',
node: 'B', node: 'B',
runIndex: 1, 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' }), parent: expect.objectContaining({ node: 'A' }),
consumedTokens: { consumedTokens: {
completionTokens: 4, completionTokens: 4,
@@ -163,7 +164,7 @@ describe(getTreeNodeData, () => {
id: 'C', id: 'C',
node: 'C', node: 'C',
runIndex: 1, 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' }), parent: expect.objectContaining({ node: 'B' }),
consumedTokens: { consumedTokens: {
completionTokens: 0, completionTokens: 0,

View File

@@ -68,8 +68,9 @@ async function createPiniaWithActiveNode() {
runData: { runData: {
[node.name]: [ [node.name]: [
{ {
startTime: new Date().getTime(), startTime: Date.now(),
executionTime: new Date().getTime(), executionIndex: 0,
executionTime: 1,
data: { data: {
main: [ main: [
[ [
@@ -91,8 +92,9 @@ async function createPiniaWithActiveNode() {
source: [null], source: [null],
}, },
{ {
startTime: new Date().getTime(), startTime: Date.now(),
executionTime: new Date().getTime(), executionIndex: 1,
executionTime: 1,
data: { data: {
main: [ main: [
[ [

View File

@@ -278,6 +278,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = {
{ {
hints: [], hints: [],
startTime: 1737540693122, startTime: 1737540693122,
executionIndex: 0,
executionTime: 1, executionTime: 1,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
@@ -287,6 +288,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = {
{ {
hints: [], hints: [],
startTime: 1737540693124, startTime: 1737540693124,
executionIndex: 1,
executionTime: 2, executionTime: 2,
source: [ source: [
{ {
@@ -300,6 +302,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = {
{ {
hints: [], hints: [],
startTime: 1737540693126, startTime: 1737540693126,
executionIndex: 2,
executionTime: 0, executionTime: 0,
source: [ source: [
{ {
@@ -313,6 +316,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = {
{ {
hints: [], hints: [],
startTime: 1737540693127, startTime: 1737540693127,
executionIndex: 3,
executionTime: 0, executionTime: 0,
source: [ source: [
{ {
@@ -326,6 +330,7 @@ export const SUPPORT_CHAT_TEST_PAYLOAD: ChatRequest.RequestPayload = {
{ {
hints: [], hints: [],
startTime: 1737540693127, startTime: 1737540693127,
executionIndex: 4,
executionTime: 28, executionTime: 28,
source: [ source: [
{ {

View File

@@ -460,6 +460,7 @@ const testExecutionData: IRunExecutionData['resultData'] = {
{ {
hints: [], hints: [],
startTime: 1732882780588, startTime: 1732882780588,
executionIndex: 0,
executionTime: 4, executionTime: 4,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
@@ -481,6 +482,7 @@ const testExecutionData: IRunExecutionData['resultData'] = {
{ {
hints: [], hints: [],
startTime: 1732882780593, startTime: 1732882780593,
executionIndex: 1,
executionTime: 0, executionTime: 0,
source: [ source: [
{ {

View File

@@ -400,6 +400,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
data: { data: {
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]],
@@ -443,6 +444,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
data: { data: {
[NodeConnectionTypes.Main]: [[{ json: {} }]], [NodeConnectionTypes.Main]: [[{ json: {} }]],
@@ -455,6 +457,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
data: { data: {
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
@@ -511,6 +514,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
data: { data: {
[NodeConnectionTypes.Main]: [[{ json: {} }]], [NodeConnectionTypes.Main]: [[{ json: {} }]],
@@ -519,6 +523,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 1,
source: [], source: [],
data: { data: {
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
@@ -527,6 +532,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 2,
source: [], source: [],
data: { data: {
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]],
@@ -722,6 +728,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
error: mock<NodeApiError>({ error: mock<NodeApiError>({
message: errorMessage, message: errorMessage,
@@ -753,6 +760,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
error: mock<NodeApiError>({ error: mock<NodeApiError>({
message: errorMessage, message: errorMessage,
@@ -783,6 +791,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
error: mock<NodeApiError>({ error: mock<NodeApiError>({
message: 'Error 1', message: 'Error 1',
@@ -792,6 +801,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 1,
source: [], source: [],
error: mock<NodeApiError>({ error: mock<NodeApiError>({
message: 'Error 2', message: 'Error 2',
@@ -855,6 +865,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
error: mock<NodeApiError>({ error: mock<NodeApiError>({
message: 'Execution error', message: 'Execution error',
@@ -894,6 +905,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
error: mock<NodeApiError>({ error: mock<NodeApiError>({
message: 'Execution error', message: 'Execution error',
@@ -948,6 +960,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
executionStatus: 'crashed', executionStatus: 'crashed',
}, },
@@ -976,6 +989,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
executionStatus: 'error', executionStatus: 'error',
}, },
@@ -1057,6 +1071,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
executionStatus: 'error', executionStatus: 'error',
error: mock<NodeApiError>({ error: mock<NodeApiError>({
@@ -1096,6 +1111,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
executionStatus: 'error', executionStatus: 'error',
}, },
@@ -1104,6 +1120,7 @@ describe('useCanvasMapping', () => {
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
}, },

View File

@@ -553,7 +553,9 @@ describe('useDataSchema', () => {
data: { data: {
resultData: { resultData: {
runData: { 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, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
}, },
{ {
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 1,
source: [], source: [],
}, },
{ {
data: { [Main]: [null, mockExecutionDataMarker] }, data: { [Main]: [null, mockExecutionDataMarker] },
startTime: 0, startTime: 0,
executionTime: 0, executionTime: 0,
executionIndex: 2,
source: [], source: [],
}, },
], ],

View File

@@ -156,6 +156,7 @@ describe(useNodeDirtiness, () => {
{ {
startTime: +runAt, startTime: +runAt,
executionTime: 0, executionTime: 0,
executionIndex: 0,
executionStatus: 'success', executionStatus: 'success',
source: [], source: [],
}, },
@@ -423,6 +424,7 @@ describe(useNodeDirtiness, () => {
{ {
startTime: +NODE_RUN_AT, startTime: +NODE_RUN_AT,
executionTime: 0, executionTime: 0,
executionIndex: 0,
executionStatus: 'success', executionStatus: 'success',
source: [], source: [],
}, },

View File

@@ -354,6 +354,7 @@ describe('useRunWorkflow({ router })', () => {
[parentName]: [ [parentName]: [
{ {
startTime: 1, startTime: 1,
executionIndex: 0,
executionTime: 0, executionTime: 0,
source: [], source: [],
}, },
@@ -361,6 +362,7 @@ describe('useRunWorkflow({ router })', () => {
[executeName]: [ [executeName]: [
{ {
startTime: 1, startTime: 1,
executionIndex: 1,
executionTime: 8, executionTime: 8,
source: [ source: [
{ {

View File

@@ -9,6 +9,7 @@ const runExecutionData: IRunExecutionData = {
Start: [ Start: [
{ {
startTime: 1, startTime: 1,
executionIndex: 0,
executionTime: 1, executionTime: 1,
data: { data: {
main: [ main: [
@@ -25,6 +26,7 @@ const runExecutionData: IRunExecutionData = {
Function: [ Function: [
{ {
startTime: 1, startTime: 1,
executionIndex: 1,
executionTime: 1, executionTime: 1,
data: { data: {
main: [ main: [
@@ -62,6 +64,7 @@ const runExecutionData: IRunExecutionData = {
Rename: [ Rename: [
{ {
startTime: 1, startTime: 1,
executionIndex: 2,
executionTime: 1, executionTime: 1,
data: { data: {
main: [ main: [
@@ -99,6 +102,7 @@ const runExecutionData: IRunExecutionData = {
End: [ End: [
{ {
startTime: 1, startTime: 1,
executionIndex: 3,
executionTime: 1, executionTime: 1,
data: { data: {
main: [ main: [

View File

@@ -849,6 +849,7 @@ function generateMockExecutionEvents() {
data: { data: {
hints: [], hints: [],
startTime: 1727867966633, startTime: 1727867966633,
executionIndex: 0,
executionTime: 1, executionTime: 1,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
@@ -873,6 +874,7 @@ function generateMockExecutionEvents() {
data: { data: {
hints: [], hints: [],
startTime: 1727869043441, startTime: 1727869043441,
executionIndex: 0,
executionTime: 2, executionTime: 2,
source: [ source: [
{ {

View File

@@ -15,6 +15,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
'When clicking Test workflow': [ 'When clicking Test workflow': [
{ {
startTime: 1706027170005, startTime: 1706027170005,
executionIndex: 0,
executionTime: 0, executionTime: 0,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
@@ -24,6 +25,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
DebugHelper: [ DebugHelper: [
{ {
startTime: 1706027170005, startTime: 1706027170005,
executionIndex: 1,
executionTime: 1, executionTime: 1,
source: [{ previousNode: 'When clicking Test workflow' }], source: [{ previousNode: 'When clicking Test workflow' }],
executionStatus: 'success', executionStatus: 'success',
@@ -58,6 +60,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
If: [ If: [
{ {
startTime: 1706027170006, startTime: 1706027170006,
executionIndex: 2,
executionTime: 1, executionTime: 1,
source: [{ previousNode: 'DebugHelper' }], source: [{ previousNode: 'DebugHelper' }],
executionStatus: 'success', executionStatus: 'success',
@@ -94,6 +97,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
'Edit Fields': [ 'Edit Fields': [
{ {
startTime: 1706027170008, startTime: 1706027170008,
executionIndex: 3,
executionTime: 0, executionTime: 0,
source: [{ previousNode: 'If', previousNodeOutput: 1 }], source: [{ previousNode: 'If', previousNodeOutput: 1 }],
executionStatus: 'success', executionStatus: 'success',
@@ -116,6 +120,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
}, },
{ {
startTime: 1706027170009, startTime: 1706027170009,
executionIndex: 3,
executionTime: 0, executionTime: 0,
source: [{ previousNode: 'If' }], source: [{ previousNode: 'If' }],
executionStatus: 'success', executionStatus: 'success',
@@ -140,6 +145,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
'Edit Fields1': [ 'Edit Fields1': [
{ {
startTime: 1706027170008, startTime: 1706027170008,
executionIndex: 4,
executionTime: 0, executionTime: 0,
source: [{ previousNode: 'Edit Fields' }], source: [{ previousNode: 'Edit Fields' }],
executionStatus: 'success', executionStatus: 'success',
@@ -162,6 +168,7 @@ const MOCK_EXECUTION: Partial<IExecutionResponse> = {
}, },
{ {
startTime: 1706027170010, startTime: 1706027170010,
executionIndex: 5,
executionTime: 0, executionTime: 0,
source: [{ previousNode: 'Edit Fields', previousNodeRun: 1 }], source: [{ previousNode: 'Edit Fields', previousNodeRun: 1 }],
executionStatus: 'success', executionStatus: 'success',
@@ -329,6 +336,7 @@ describe('pairedItemUtils', () => {
Start: [ Start: [
{ {
startTime: 1706027170005, startTime: 1706027170005,
executionIndex: 0,
executionTime: 0, executionTime: 0,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
@@ -340,6 +348,7 @@ describe('pairedItemUtils', () => {
DebugHelper: [ DebugHelper: [
{ {
startTime: 1706027170005, startTime: 1706027170005,
executionIndex: 1,
executionTime: 1, executionTime: 1,
source: [{ previousNode: 'Start' }], source: [{ previousNode: 'Start' }],
executionStatus: 'success', executionStatus: 'success',
@@ -409,8 +418,9 @@ describe('pairedItemUtils', () => {
runData: Object.fromEntries( runData: Object.fromEntries(
Array.from({ length: nodeCount }).map<[string, ITaskData[]]>((_, j) => [ Array.from({ length: nodeCount }).map<[string, ITaskData[]]>((_, j) => [
`node_${j}`, `node_${j}`,
Array.from({ length: runCount }).map(() => ({ Array.from({ length: runCount }).map((_, executionIndex) => ({
startTime: 1706027170005, startTime: 1706027170005,
executionIndex,
executionTime: 0, executionTime: 0,
source: j === 0 ? [] : [{ previousNode: `node_${j - 1}` }], source: j === 0 ? [] : [{ previousNode: `node_${j - 1}` }],
executionStatus: 'success', executionStatus: 'success',

View File

@@ -2180,16 +2180,22 @@ export interface ITaskMetadata {
subExecutionsCount?: number; subExecutionsCount?: number;
} }
// The data that gets returned when a node runs /** The data that gets returned when a node execution starts */
export interface ITaskData { export interface ITaskStartedData {
startTime: number; startTime: number;
/** This index tracks the order in which nodes are executed */
executionIndex: number;
source: Array<ISourceData | null>; // 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; executionTime: number;
executionStatus?: ExecutionStatus; executionStatus?: ExecutionStatus;
data?: ITaskDataConnections; data?: ITaskDataConnections;
inputOverride?: ITaskDataConnections; inputOverride?: ITaskDataConnections;
error?: ExecutionError; error?: ExecutionError;
hints?: NodeExecutionHint[];
source: Array<ISourceData | null>; // Is an array as nodes have multiple inputs
metadata?: ITaskMetadata; metadata?: ITaskMetadata;
} }
@@ -2336,6 +2342,7 @@ export interface IWorkflowExecuteAdditionalData {
) => Promise<ExecuteWorkflowData>; ) => Promise<ExecuteWorkflowData>;
executionId?: string; executionId?: string;
restartExecutionId?: string; restartExecutionId?: string;
currentNodeExecutionIndex: number;
httpResponse?: express.Response; httpResponse?: express.Response;
httpRequest?: express.Request; httpRequest?: express.Request;
restApiUrl: string; restApiUrl: string;

View File

@@ -916,7 +916,15 @@ export class WorkflowDataProxy {
); );
if (pinData) { if (pinData) {
taskData = { data: { main: [pinData] }, startTime: 0, executionTime: 0, source: [] }; taskData = {
data: {
main: [pinData],
},
startTime: 0,
executionTime: 0,
executionIndex: 0,
source: [],
};
} }
} }

View File

@@ -1512,6 +1512,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340927, startTime: 1727793340927,
executionTime: 0, executionTime: 0,
executionIndex: 0,
source: [], source: [],
executionStatus: 'success', executionStatus: 'success',
data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] }, data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] },
@@ -1522,6 +1523,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340928, startTime: 1727793340928,
executionTime: 0, executionTime: 0,
executionIndex: 1,
source: [{ previousNode: 'Execute Workflow Trigger' }], source: [{ previousNode: 'Execute Workflow Trigger' }],
executionStatus: 'success', executionStatus: 'success',
data: { data: {
@@ -1555,6 +1557,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340928, startTime: 1727793340928,
executionTime: 1, executionTime: 1,
executionIndex: 2,
source: [{ previousNode: 'DebugHelper' }], source: [{ previousNode: 'DebugHelper' }],
executionStatus: 'success', executionStatus: 'success',
data: { data: {
@@ -1586,6 +1589,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340931, startTime: 1727793340931,
executionTime: 0, executionTime: 0,
executionIndex: 3,
source: [{ previousNode: 'Execute Workflow Trigger' }], source: [{ previousNode: 'Execute Workflow Trigger' }],
executionStatus: 'success', executionStatus: 'success',
data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] }, data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] },
@@ -1596,6 +1600,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340929, startTime: 1727793340929,
executionTime: 1, executionTime: 1,
executionIndex: 4,
source: [{ previousNode: 'Edit Fields' }], source: [{ previousNode: 'Edit Fields' }],
executionStatus: 'success', executionStatus: 'success',
data: { data: {
@@ -1630,6 +1635,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340931, startTime: 1727793340931,
executionTime: 0, executionTime: 0,
executionIndex: 5,
source: [{ previousNode: 'Edit Fields', previousNodeRun: 1 }], source: [{ previousNode: 'Edit Fields', previousNodeRun: 1 }],
executionStatus: 'success', executionStatus: 'success',
data: { main: [[], [], [{ json: {}, pairedItem: { item: 0 } }], []] }, data: { main: [[], [], [{ json: {}, pairedItem: { item: 0 } }], []] },
@@ -1640,6 +1646,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340930, startTime: 1727793340930,
executionTime: 0, executionTime: 0,
executionIndex: 6,
source: [{ previousNode: 'Switch', previousNodeOutput: 2 }], source: [{ previousNode: 'Switch', previousNodeOutput: 2 }],
executionStatus: 'success', executionStatus: 'success',
data: { data: {
@@ -1656,6 +1663,7 @@ function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; r
hints: [], hints: [],
startTime: 1727793340932, startTime: 1727793340932,
executionTime: 1, executionTime: 1,
executionIndex: 7,
source: [{ previousNode: 'Switch', previousNodeOutput: 2, previousNodeRun: 1 }], source: [{ previousNode: 'Switch', previousNodeOutput: 2, previousNodeRun: 1 }],
executionStatus: 'success', executionStatus: 'success',
data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] }, data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] },

View File

@@ -1603,6 +1603,7 @@ describe('Workflow', () => {
], ],
startTime: 1, startTime: 1,
executionTime: 1, executionTime: 1,
executionIndex: 0,
data: { data: {
main: [ main: [
[ [
@@ -1681,6 +1682,7 @@ describe('Workflow', () => {
{ {
startTime: 1, startTime: 1,
executionTime: 1, executionTime: 1,
executionIndex: 0,
data: { data: {
main: [ main: [
[ [