Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 8 additions & 4 deletions packages/cli/src/workflows/workflow-execution.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,15 @@ export class WorkflowExecutionService {
streamingEnabled?: boolean,
httpResponse?: Response,
) {
const { workflowData, startNodes, dirtyNodeNames, triggerToStartFrom, agentRequest } = payload;
const {
workflowData,
startNodes,
destinationNode,
dirtyNodeNames,
triggerToStartFrom,
agentRequest,
} = payload;
let { runData } = payload;
const destinationNode = payload.destinationNode
? ({ nodeName: payload.destinationNode, mode: 'inclusive' } as const)
: undefined;
const pinData = workflowData.pinData;
let pinnedTrigger = this.selectPinnedActivatorStarter(
workflowData,
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/workflows/workflow.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ITaskData,
IWorkflowBase,
AiAgentRequest,
IDestinationNode,
} from 'n8n-workflow';

import type { ListQuery } from '@/requests';
Expand All @@ -33,7 +34,7 @@ export declare namespace WorkflowRequest {
workflowData: IWorkflowBase;
runData?: IRunData;
startNodes?: StartNodeData[];
destinationNode?: string;
destinationNode?: IDestinationNode;
dirtyNodeNames?: string[];
triggerToStartFrom?: {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ const onExecute = async () => {
telemetry.track('User clicked execute node button in modal', telemetryPayload);

await runWorkflow({
destinationNode: node.value.name,
destinationNode: { nodeName: node.value.name, mode: 'inclusive' },
});

onClose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,10 @@ describe('NodeExecuteButton', () => {

expect(externalHooks.run).toHaveBeenCalledWith('nodeExecuteButton.onClick', expect.any(Object));
expect(runWorkflow.runWorkflow).toHaveBeenCalledWith({
destinationNode: node.name,
destinationNode: {
nodeName: node.name,
mode: 'inclusive',
},
source: 'RunData.ExecuteNodeButton',
});
expect(emitted().execute).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ const props = withDefaults(
tooltip?: string;
tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';
showLoadingSpinner?: boolean;
executionMode?: 'inclusive' | 'exclusive';
}>(),
{
disabled: false,
transparent: false,
square: false,
showLoadingSpinner: true,
executionMode: 'inclusive',
},
);

Expand Down Expand Up @@ -384,7 +386,7 @@ async function onClick() {
await externalHooks.run('nodeExecuteButton.onClick', telemetryPayload);

await runWorkflow({
destinationNode: props.nodeName,
destinationNode: { nodeName: props.nodeName, mode: props.executionMode },
source: 'RunData.ExecuteNodeButton',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(workflowsStore).incomingConnectionsByNodeName.mockReturnValue({});

// ACT
await runWorkflow({ destinationNode: destinationNodeName });
await runWorkflow({ destinationNode: { nodeName: destinationNodeName, mode: 'inclusive' } });

// ASSERT
expect(workflowsStore.runWorkflow).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -593,7 +593,10 @@ describe('useRunWorkflow({ router })', () => {

const { runWorkflow } = composable;

await runWorkflow({ destinationNode: 'Code 1', source: 'Node.executeNode' });
await runWorkflow({
destinationNode: { nodeName: 'Code 1', mode: 'inclusive' },
source: 'Node.executeNode',
});

expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
expect.objectContaining({ dirtyNodeNames: [executeName] }),
Expand Down Expand Up @@ -774,7 +777,9 @@ describe('useRunWorkflow({ router })', () => {
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');

// ACT
const result = await runWorkflow({ destinationNode: 'Test node' });
const result = await runWorkflow({
destinationNode: { nodeName: 'Test node', mode: 'inclusive' },
});

// ASSERT
expect(agentRequestStore.getAgentRequest).toHaveBeenCalledWith('WorkflowId', 'Test id');
Expand All @@ -785,7 +790,7 @@ describe('useRunWorkflow({ router })', () => {
name: 'tool',
},
},
destinationNode: 'Test node',
destinationNode: { nodeName: 'Test node', mode: 'inclusive' },
dirtyNodeNames: undefined,
runData: mockRunData,
startNodes: [
Expand Down Expand Up @@ -824,7 +829,9 @@ describe('useRunWorkflow({ router })', () => {
const setWorkflowExecutionData = vi.spyOn(workflowState, 'setWorkflowExecutionData');

// ACT
const result = await runWorkflow({ destinationNode: 'some node name' });
const result = await runWorkflow({
destinationNode: { nodeName: 'some node name', mode: 'inclusive' },
});

// ASSERT
expect(result).toEqual(mockExecutionResponse);
Expand Down
37 changes: 20 additions & 17 deletions packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import type {

import type {
IRunData,
IRunExecutionData,
ITaskData,
IPinData,
Workflow,
StartNodeData,
INode,
IDataObject,
IWorkflowBase,
DestinationNode,
} from 'n8n-workflow';
import { createRunExecutionData, NodeConnectionTypes, TelemetryHelpers } from 'n8n-workflow';
import { NodeConnectionTypes, TelemetryHelpers } from 'n8n-workflow';
import { retry } from '@n8n/utils/retry';

import { useToast } from '@/app/composables/useToast';
Expand Down Expand Up @@ -129,7 +131,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
}

async function runWorkflow(options: {
destinationNode?: string;
destinationNode?: DestinationNode;
triggerNode?: string;
rerunTriggerNode?: boolean;
nodeData?: ITaskData;
Expand All @@ -146,7 +148,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
let directParentNodes: string[] = [];
if (options.destinationNode !== undefined) {
directParentNodes = workflowObject.value.getParentNodes(
options.destinationNode,
options.destinationNode.nodeName,
NodeConnectionTypes.Main,
-1,
);
Expand All @@ -169,7 +171,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {

const { startNodeNames } = consolidatedData;
const destinationNodeType = options.destinationNode
? workflowsStore.getNodeByName(options.destinationNode)?.type
? workflowsStore.getNodeByName(options.destinationNode.nodeName)?.type
: '';

let executedNode: string | undefined;
Expand All @@ -180,15 +182,15 @@ export function useRunWorkflow(useRunWorkflowOpts: {
'destinationNode' in options &&
options.destinationNode !== undefined
) {
executedNode = options.destinationNode;
startNodeNames.push(options.destinationNode);
executedNode = options.destinationNode.nodeName;
startNodeNames.push(options.destinationNode.nodeName);
} else if (options.triggerNode && options.nodeData && !options.rerunTriggerNode) {
// starts execution from downstream nodes of trigger node
startNodeNames.push(
...workflowObject.value.getChildNodes(options.triggerNode, NodeConnectionTypes.Main, 1),
);
} else if (options.destinationNode) {
executedNode = options.destinationNode;
executedNode = options.destinationNode.nodeName;
}

if (options.triggerNode) {
Expand All @@ -201,11 +203,11 @@ export function useRunWorkflow(useRunWorkflowOpts: {
// If the destination node is specified, check if it is a chat node or has a chat parent
if (
options.destinationNode &&
(workflowsStore.checkIfNodeHasChatParent(options.destinationNode) ||
(workflowsStore.checkIfNodeHasChatParent(options.destinationNode.nodeName) ||
destinationNodeType === CHAT_TRIGGER_NODE_TYPE) &&
options.source !== 'RunData.ManualChatMessage'
) {
const startNode = workflowObject.value.getStartNode(options.destinationNode);
const startNode = workflowObject.value.getStartNode(options.destinationNode.nodeName);
if (startNode && startNode.type === CHAT_TRIGGER_NODE_TYPE) {
// Check if the chat node has input data or pin data
const chatHasInputData =
Expand All @@ -215,7 +217,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
// If the chat node has no input data or pin data, open the chat modal
// and halt the execution
if (!chatHasInputData && !chatHasPinData) {
workflowsStore.chatPartialExecutionDestinationNode = options.destinationNode;
workflowsStore.chatPartialExecutionDestinationNode = options.destinationNode.nodeName;
startChat();
return;
}
Expand Down Expand Up @@ -279,9 +281,9 @@ export function useRunWorkflow(useRunWorkflowOpts: {
.filter((node) => {
if (
options.destinationNode &&
workflowsStore.checkIfNodeHasChatParent(options.destinationNode)
workflowsStore.checkIfNodeHasChatParent(options.destinationNode.nodeName)
) {
return node.name !== options.destinationNode;
return node.name !== options.destinationNode.nodeName;
}
return true;
});
Expand Down Expand Up @@ -322,7 +324,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {

if ('destinationNode' in options) {
startRunData.destinationNode = options.destinationNode;
const nodeId = workflowsStore.getNodeByName(options.destinationNode as string)?.id;
const nodeId = workflowsStore.getNodeByName(options.destinationNode!.nodeName)?.id;
if (workflowObject.value.id && nodeId) {
const agentRequest = agentRequestStore.getAgentRequest(workflowObject.value.id, nodeId);

Expand Down Expand Up @@ -359,12 +361,13 @@ export function useRunWorkflow(useRunWorkflowOpts: {
workflowId: workflowObject.value.id,
executedNode,
triggerNode: triggerToStartFrom?.name,
data: createRunExecutionData({
data: {
resultData: {
runData: startRunData.runData ?? {},
pinData: workflowData.pinData,
workflowData,
},
}),
} as IRunExecutionData,
workflowData: {
id: workflowsStore.workflowId,
name: workflowData.name!,
Expand Down Expand Up @@ -395,7 +398,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
await displayForm({
nodes: workflowData.nodes,
runData: workflowsStore.getWorkflowExecution?.data?.resultData?.runData,
destinationNode: options.destinationNode,
destinationNode: options.destinationNode?.nodeName,
triggerNode: options.triggerNode,
pinData,
directParentNodes,
Expand All @@ -405,7 +408,7 @@ export function useRunWorkflow(useRunWorkflowOpts: {
} catch (error) {}

await externalHooks.run('workflowRun.runWorkflow', {
nodeName: options.destinationNode,
nodeName: options.destinationNode?.nodeName,
source: options.source,
});

Expand Down
10 changes: 8 additions & 2 deletions packages/frontend/editor-ui/src/app/types/externalHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import type {
ExecutionError,
GenericValue,
IConnections,
IDestinationNode,
INodeProperties,
INodeTypeDescription,
ITelemetryTrackProperties,
NodeConnectionType,
NodeParameterValue,
NodeParameterValueType,
DestinationNode,
StartNodeData,
} from 'n8n-workflow';
import type { RouteLocation } from 'vue-router';
import type {
Expand Down Expand Up @@ -65,7 +66,12 @@ interface OutputModeChangedEventData {
}
interface ExecutionFinishedEventData {
runDataExecutedStartData:
| { destinationNode?: IDestinationNode; runNodeFilter?: string[] | undefined }
| {
startNodes?: StartNodeData[];
destinationNode?: DestinationNode | undefined;
originalDestinationNode?: DestinationNode | undefined;
runNodeFilter?: string[] | undefined;
}
| undefined;
nodeName?: string;
errorMessage: string;
Expand Down
5 changes: 4 additions & 1 deletion packages/frontend/editor-ui/src/app/views/NodeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1258,7 +1258,10 @@ async function onRunWorkflowToNode(id: string) {
trackRunWorkflowToNode(node);
agentRequestStore.clearAgentRequests(workflowsStore.workflowId, node.id);

void runWorkflow({ destinationNode: node.name, source: 'Node.executeNode' });
void runWorkflow({
destinationNode: { nodeName: node.name, mode: 'inclusive' },
source: 'Node.executeNode',
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ describe('LogsOverviewPanel', () => {

await fireEvent.click(within(aiAgentRow).getAllByLabelText('Execute step')[0]);
await waitFor(() =>
expect(spyRun).toHaveBeenCalledWith(expect.objectContaining({ destinationNode: 'AI Agent' })),
expect(spyRun).toHaveBeenCalledWith(
expect.objectContaining({ destinationNode: { nodeName: 'AI Agent', mode: 'inclusive' } }),
),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ export function useChatState(isReadOnly: boolean): ChatState {
};

if (workflowsStore.chatPartialExecutionDestinationNode) {
runWorkflowOptions.destinationNode = workflowsStore.chatPartialExecutionDestinationNode;
runWorkflowOptions.destinationNode = {
nodeName: workflowsStore.chatPartialExecutionDestinationNode,
mode: 'inclusive',
};
workflowsStore.chatPartialExecutionDestinationNode = null;
}

Expand Down
Loading
Loading