Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3a1b89c
switch to structured destination node
mfsiega Nov 21, 2025
3cbf6b5
test fixes
mfsiega Nov 21, 2025
a08dcf4
another test update
mfsiega Nov 21, 2025
0475b55
more test fixes
mfsiega Nov 21, 2025
d4db7c5
migrate v0 to v1 RunExecutionData at the repository layer
mfsiega Nov 21, 2025
813513f
remove console log
mfsiega Nov 21, 2025
6954d2f
better test name
mfsiega Nov 21, 2025
db58367
small cleanup
mfsiega Nov 21, 2025
7299fe5
test fixes
mfsiega Nov 21, 2025
a066e56
undo debug loggin
mfsiega Nov 21, 2025
b23178f
remove console logs
mfsiega Nov 21, 2025
c97b305
remove test.only
mfsiega Nov 21, 2025
4bee97a
test fix
mfsiega Nov 21, 2025
0c7222d
more fixes
mfsiega Nov 21, 2025
5bd6cc6
remove extra console log
mfsiega Nov 23, 2025
3f1a3e5
fix test data
mfsiega Nov 23, 2025
261048d
Merge branch 'master' into cat-1265-dont-run-destination-node5
mfsiega Nov 23, 2025
c031684
test fix (#22192)
mfsiega Nov 23, 2025
acd3d2a
handle serialized undefined properly
mfsiega Nov 24, 2025
6c2adaf
clean up handling of execution run data
mfsiega Nov 24, 2025
e2492cb
Merge branch 'master' into cat-1265-dont-run-destination-node5
mfsiega Nov 24, 2025
9fa728f
revert stringified representation of undefined
mfsiega Nov 24, 2025
22ce9ce
Merge branch 'master' into cat-1265-dont-run-destination-node5
mfsiega Nov 24, 2025
d679d03
docs: add docstring to IDestinationNode.mode
despairblue Nov 24, 2025
a3eb84f
refactor: simplify migrateRunExecutionData
despairblue Nov 24, 2025
fda9225
refactor: clean up handleExecutionRunData
despairblue Nov 24, 2025
1af3876
refactor: remove unnecessary type annotation in workflow-execution.se…
despairblue Nov 24, 2025
2be7f86
chore: add TODO for destinationNode type migration
despairblue Nov 24, 2025
c3dbad5
test: use createErrorExecutionData factory in telemetry tests
despairblue Nov 24, 2025
6c96dd2
test: add coverage for destination node exclusive/inclusive modes
despairblue Nov 24, 2025
26a992d
fixup! refactor: remove unnecessary type annotation in workflow-execu…
despairblue Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ describe('JsTaskRunner', () => {
});

const helsinkiTimeNow = DateTime.now().setZone('Europe/Helsinki').toSeconds();
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 1) });
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 0) });
});

it('should use the default timezone', async () => {
Expand All @@ -472,7 +472,7 @@ describe('JsTaskRunner', () => {
});

const helsinkiTimeNow = DateTime.now().setZone('Europe/Helsinki').toSeconds();
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 1) });
expect(outcome.result).toEqual({ val: expect.closeTo(helsinkiTimeNow, 0) });
});
});

Expand Down
27 changes: 19 additions & 8 deletions packages/cli/src/__tests__/manual-execution.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
IWaitingForExecution,
IWaitingForExecutionSource,
INodeExecutionData,
IDestinationNode,
} from 'n8n-workflow';
import type PCancelable from 'p-cancelable';

Expand Down Expand Up @@ -190,6 +191,10 @@ describe('ManualExecutionService', () => {
const startNodeName = 'startNode';
const triggerNodeName = 'triggerNode';
const destinationNodeName = 'destinationNode';
const destinationNode: IDestinationNode = {
nodeName: destinationNodeName,
mode: 'inclusive',
};

const data = mock<IWorkflowExecutionDataProcess>({
triggerToStartFrom: {
Expand All @@ -198,7 +203,7 @@ describe('ManualExecutionService', () => {
},
startNodes: [{ name: startNodeName }],
executionMode: 'manual',
destinationNode: destinationNodeName,
destinationNode,
});

const startNode = mock<INode>({ name: startNodeName });
Expand All @@ -225,9 +230,7 @@ describe('ManualExecutionService', () => {
additionalData,
data.executionMode,
expect.objectContaining({
startData: {
destinationNode: destinationNodeName,
},
startData: { destinationNode },
resultData: expect.any(Object),
executionData: expect.any(Object),
}),
Expand Down Expand Up @@ -379,12 +382,16 @@ describe('ManualExecutionService', () => {
const mockRunData = { node1: [{ data: { main: [[{ json: {} }]] } }] };
const dirtyNodeNames = ['node2', 'node3'];
const destinationNodeName = 'destinationNode';
const destinationNode: IDestinationNode = {
nodeName: destinationNodeName,
mode: 'inclusive',
};
const data = mock<IWorkflowExecutionDataProcess>({
executionMode: 'manual',
runData: mockRunData,
startNodes: [{ name: 'node1' }],
dirtyNodeNames,
destinationNode: destinationNodeName,
destinationNode,
});

const workflow = mock<Workflow>({
Expand All @@ -411,7 +418,7 @@ describe('ManualExecutionService', () => {

expect(mockRunPartialWorkflow2).toHaveBeenCalled();
expect(mockRunPartialWorkflow2.mock.calls[0][0]).toBe(workflow);
expect(mockRunPartialWorkflow2.mock.calls[0][4]).toBe(destinationNodeName);
expect(mockRunPartialWorkflow2.mock.calls[0][4]).toEqual(destinationNode);
});

it('should validate nodes exist before execution', async () => {
Expand Down Expand Up @@ -489,11 +496,15 @@ describe('ManualExecutionService', () => {
it('should call runPartialWorkflow2 with runData and empty startNodes', async () => {
const mockRunData = { nodeA: [{ data: { main: [[{ json: { value: 'test' } }]] } }] };
const destinationNodeName = 'nodeB';
const destinationNode: IDestinationNode = {
nodeName: destinationNodeName,
mode: 'inclusive',
};
const data = mock<IWorkflowExecutionDataProcess>({
executionMode: 'manual',
runData: mockRunData,
startNodes: [],
destinationNode: destinationNodeName,
destinationNode,
pinData: {},
dirtyNodeNames: [],
agentRequest: undefined,
Expand Down Expand Up @@ -526,7 +537,7 @@ describe('ManualExecutionService', () => {
mockRunData,
data.pinData,
data.dirtyNodeNames,
destinationNodeName,
destinationNode,
data.agentRequest,
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,10 @@ describe('TestRunnerService', () => {
const runCallArg = workflowRunner.run.mock.calls[0][0];

// Verify it has the correct structure
expect(runCallArg).toHaveProperty('destinationNode', triggerNodeName);
expect(runCallArg).toHaveProperty('destinationNode', {
nodeName: triggerNodeName,
mode: 'inclusive',
});
expect(runCallArg).toHaveProperty('executionMode', 'manual');
expect(runCallArg).toHaveProperty('workflowData.settings.saveManualExecutions', false);
expect(runCallArg).toHaveProperty('workflowData.settings.saveDataErrorExecution', 'none');
Expand Down Expand Up @@ -531,7 +534,10 @@ describe('TestRunnerService', () => {
const runCallArg = workflowRunner.run.mock.calls[0][0];

// Verify it has the correct structure
expect(runCallArg).toHaveProperty('destinationNode', triggerNodeName);
expect(runCallArg).toHaveProperty('destinationNode', {
nodeName: triggerNodeName,
mode: 'inclusive',
});
expect(runCallArg).toHaveProperty('executionMode', 'manual');
expect(runCallArg).toHaveProperty('workflowData.settings.saveManualExecutions', false);
expect(runCallArg).toHaveProperty('workflowData.settings.saveDataErrorExecution', 'none');
Expand All @@ -546,7 +552,10 @@ describe('TestRunnerService', () => {
// But executionData itself should still exist with startData and manualData
expect(runCallArg).toHaveProperty('executionData');
expect(runCallArg.executionData).toBeDefined();
expect(runCallArg).toHaveProperty('executionData.startData.destinationNode', triggerNodeName);
expect(runCallArg).toHaveProperty('executionData.startData.destinationNode', {
nodeName: triggerNodeName,
mode: 'inclusive',
});
expect(runCallArg).toHaveProperty('executionData.manualData.userId', metadata.userId);
expect(runCallArg).toHaveProperty(
'executionData.manualData.triggerToStartFrom.name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ export class TestRunnerService {
};

const data: IWorkflowExecutionDataProcess = {
destinationNode: triggerNode.name,
destinationNode: { nodeName: triggerNode.name, mode: 'inclusive' },
executionMode: 'manual',
runData: {},
workflowData: {
Expand All @@ -334,7 +334,7 @@ export class TestRunnerService {
userId: metadata.userId,
executionData: createRunExecutionData({
startData: {
destinationNode: triggerNode.name,
destinationNode: { nodeName: triggerNode.name, mode: 'inclusive' },
},
manualData: {
userId: metadata.userId,
Expand Down
15 changes: 8 additions & 7 deletions packages/cli/src/events/__tests__/telemetry-event-relay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { mock } from 'jest-mock-extended';
import { type BinaryDataConfig, InstanceSettings } from 'n8n-core';
import {
createRunExecutionData,
type INode,
type INodesGraphResult,
type IRun,
Expand Down Expand Up @@ -1284,9 +1285,9 @@ describe('TelemetryEventRelay', () => {
const runData = {
status: 'error',
mode: 'manual',
data: {
data: createRunExecutionData({
startData: {
destinationNode: 'OpenAI',
destinationNode: { nodeName: 'OpenAI', mode: 'inclusive' },
runNodeFilter: ['OpenAI'],
},
resultData: {
Expand Down Expand Up @@ -1315,7 +1316,7 @@ describe('TelemetryEventRelay', () => {
},
),
},
},
}),
} as IRun;

const nodeGraph: INodesGraphResult = {
Expand Down Expand Up @@ -1386,9 +1387,9 @@ describe('TelemetryEventRelay', () => {
const runData = {
status: 'error',
mode: 'manual',
data: {
data: createRunExecutionData({
startData: {
destinationNode: 'OpenAI',
destinationNode: { nodeName: 'OpenAI', mode: 'inclusive' },
runNodeFilter: ['OpenAI'],
},
resultData: {
Expand Down Expand Up @@ -1417,7 +1418,7 @@ describe('TelemetryEventRelay', () => {
},
),
},
},
}),
} as IRun;

const nodeGraph: INodesGraphResult = {
Expand Down Expand Up @@ -1598,7 +1599,7 @@ describe('TelemetryEventRelay', () => {
nodeExecutionStack: [{ node: { credentials: { openAiApi: { id: 'nhu-l8E4hX' } } } }],
},
startData: {
destinationNode: 'OpenAI',
destinationNode: { nodeName: 'OpenAI', mode: 'inclusive' },
runNodeFilter: ['OpenAI'],
},
resultData: {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/events/relays/telemetry.event-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,14 +787,14 @@ export class TelemetryEventRelay extends EventRelay {
manualExecEventProperties.is_managed = credential.isManaged;
}
}

console.log(runData.data.startData);
const telemetryPayload: ITelemetryTrackProperties = {
...manualExecEventProperties,
node_type: TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.startData?.destinationNode,
runData.data.startData?.destinationNode.nodeName,
)?.type,
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode.nodeName],
};

this.telemetry.track('Manual node exec finished', telemetryPayload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ describe('Execution Lifecycle Hooks', () => {
});
successfulRunWithRewiredDestination.data = createRunExecutionData({
startData: {
destinationNode: 'PartialExecutionToolExecutor',
originalDestinationNode: nodeName,
destinationNode: { nodeName: 'PartialExecutionToolExecutor', mode: 'inclusive' },
originalDestinationNode: { nodeName, mode: 'inclusive' },
},
resultData: {
runData: {},
Expand Down Expand Up @@ -183,7 +183,10 @@ describe('Execution Lifecycle Hooks', () => {
userId: expectedUserId,
});

expect(successfulRunWithRewiredDestination.data.startData?.destinationNode).toBe(nodeName);
expect(successfulRunWithRewiredDestination.data.startData?.destinationNode).toEqual({
nodeName,
mode: 'inclusive',
});
expect(
successfulRunWithRewiredDestination.data.startData?.originalDestinationNode,
).toBeUndefined();
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/executions/execution-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export class ExecutionDataService {

if (node) {
returnData.data.startData = {
destinationNode: node.name,
destinationNode: {
nodeName: node.name,
mode: 'inclusive',
},
runNodeFilter: [node.name],
};
returnData.data.resultData.lastNodeExecuted = node.name;
Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/manual-execution.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class ManualExecutionService {
let waitingExecution: IWaitingForExecution = {};
let waitingExecutionSource: IWaitingForExecutionSource = {};

if (data.destinationNode !== data.triggerToStartFrom.name) {
if (data.destinationNode?.nodeName !== data.triggerToStartFrom.name) {
const recreatedStack = recreateNodeExecutionStack(
filterDisabledNodes(DirectedGraph.fromWorkflow(workflow)),
new Set(startNodes),
Expand Down Expand Up @@ -119,10 +119,10 @@ export class ManualExecutionService {
const startNode = this.getExecutionStartNode(data, workflow);

if (data.destinationNode) {
const destinationNode = workflow.getNode(data.destinationNode);
const destinationNode = workflow.getNode(data.destinationNode.nodeName);
a.ok(
destinationNode,
`Could not find a node named "${data.destinationNode}" in the workflow.`,
`Could not find a node named "${data.destinationNode.nodeName}" in the workflow.`,
);

const destinationNodeType = workflow.nodeTypes.getByNameAndVersion(
Expand All @@ -147,7 +147,8 @@ export class ManualExecutionService {
data.executionData.startData.originalDestinationNode = data.destinationNode;
}
// Set destination to Tool Executor
data.destinationNode = TOOL_EXECUTOR_NODE_NAME;
// TODO(CAT-1265): Verify that this works as expected with inclusive mode.
data.destinationNode = { nodeName: TOOL_EXECUTOR_NODE_NAME, mode: 'inclusive' };
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const taskData: DataRequestResponse = {
node: codeNode,
runExecutionData: createRunExecutionData({
startData: {
destinationNode: codeNode.name,
destinationNode: { nodeName: codeNode.name, mode: 'inclusive' },
runNodeFilter: [triggerNode.name, debugHelperNode.name, codeNode.name],
},
resultData: {
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/webhooks/__tests__/webhook-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,10 +396,13 @@ describe('prepareExecutionData', () => {
webhookResultData,
undefined,
{},
'targetNode',
{ nodeName: 'targetNode', mode: 'inclusive' },
);

expect(runExecutionData.startData?.destinationNode).toBe('targetNode');
expect(runExecutionData.startData?.destinationNode).toEqual({
nodeName: 'targetNode',
mode: 'inclusive',
});
});

test('should update execution data with execution data merge', () => {
Expand Down
15 changes: 12 additions & 3 deletions packages/cli/src/webhooks/test-webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
IHttpRequestMethods,
IRunData,
IWorkflowBase,
IDestinationNode,
} from 'n8n-workflow';

import { authAllowlistedNodes } from './constants';
Expand Down Expand Up @@ -104,7 +105,14 @@ export class TestWebhooks implements IWebhookManager {
});
}

const { destinationNode, pushRef, workflowEntity, webhook: testWebhook } = registration;
const { pushRef, workflowEntity, webhook: testWebhook } = registration;
// TODO(CAT-1265): support destination node mode in test webhook registration.
const destinationNode: IDestinationNode | undefined = registration.destinationNode
? {
nodeName: registration.destinationNode,
mode: 'inclusive',
}
: undefined;

const workflow = this.toWorkflow(workflowEntity);

Expand Down Expand Up @@ -269,7 +277,7 @@ export class TestWebhooks implements IWebhookManager {
additionalData: IWorkflowExecuteAdditionalData;
runData?: IRunData;
pushRef?: string;
destinationNode?: string;
destinationNode?: IDestinationNode;
triggerToStartFrom?: WorkflowRequest.ManualRunPayload['triggerToStartFrom'];
}) {
const {
Expand Down Expand Up @@ -344,10 +352,11 @@ export class TestWebhooks implements IWebhookManager {

cacheableWebhook.userId = userId;

// TODO(CAT-1265): support destination node mode in test webhook registration.
const registration: TestWebhookRegistration = {
pushRef,
workflowEntity,
destinationNode,
destinationNode: destinationNode?.nodeName,
webhook: cacheableWebhook as IWebhookData,
};

Expand Down
Loading
Loading