Skip to content

Commit eaaefd7

Browse files
authored
feat: Allow workflow execution even if it has errors (#9037)
1 parent 15fb6cb commit eaaefd7

File tree

5 files changed

+162
-103
lines changed

5 files changed

+162
-103
lines changed

cypress/e2e/19-execution.cy.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,4 +592,31 @@ describe('Execution', () => {
592592
cy.wait(100);
593593
workflowPage.getters.errorToast({ timeout: 1 }).should('not.exist');
594594
});
595+
596+
it('should execute workflow partially up to the node that has issues', () => {
597+
cy.createFixtureWorkflow(
598+
'Test_workflow_partial_execution_with_missing_credentials.json',
599+
'My test workflow',
600+
);
601+
602+
cy.intercept('POST', '/rest/workflows/run').as('workflowRun');
603+
604+
workflowPage.getters.zoomToFitButton().click();
605+
workflowPage.getters.executeWorkflowButton().click();
606+
607+
// Wait for the execution to return.
608+
cy.wait('@workflowRun');
609+
610+
// Check that the previous nodes executed successfully
611+
workflowPage.getters
612+
.canvasNodeByName('DebugHelper')
613+
.within(() => cy.get('.fa-check'))
614+
.should('exist');
615+
workflowPage.getters
616+
.canvasNodeByName('Filter')
617+
.within(() => cy.get('.fa-check'))
618+
.should('exist');
619+
620+
workflowPage.getters.errorToast().should('contain', `Problem in node ‘Telegram‘`);
621+
});
595622
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
{
2+
"meta": {
3+
"templateCredsSetupCompleted": true,
4+
"instanceId": "2be09fdcb9594c0827fd4cee80f7e590c93297d9217685f34c2250fe3144ef0c"
5+
},
6+
"nodes": [
7+
{
8+
"parameters": {},
9+
"id": "09e4325e-ede1-40cf-a1ba-58612bbc7f1b",
10+
"name": "When clicking \"Test workflow\"",
11+
"type": "n8n-nodes-base.manualTrigger",
12+
"typeVersion": 1,
13+
"position": [
14+
820,
15+
400
16+
]
17+
},
18+
{
19+
"parameters": {
20+
"category": "randomData"
21+
},
22+
"id": "4920bf3a-9978-4196-9dcb-8c2892e5641b",
23+
"name": "DebugHelper",
24+
"type": "n8n-nodes-base.debugHelper",
25+
"typeVersion": 1,
26+
"position": [
27+
1040,
28+
400
29+
]
30+
},
31+
{
32+
"parameters": {
33+
"conditions": {
34+
"options": {
35+
"caseSensitive": true,
36+
"leftValue": "",
37+
"typeValidation": "strict"
38+
},
39+
"conditions": [
40+
{
41+
"id": "7508343e-3e99-4d12-96e4-00a35a3d4306",
42+
"leftValue": "={{ $json.email }}",
43+
"rightValue": ".",
44+
"operator": {
45+
"type": "string",
46+
"operation": "contains"
47+
}
48+
}
49+
],
50+
"combinator": "and"
51+
},
52+
"options": {}
53+
},
54+
"id": "4f6a6a4e-19b6-43f5-ba5c-e40b09d7f873",
55+
"name": "Filter",
56+
"type": "n8n-nodes-base.filter",
57+
"typeVersion": 2,
58+
"position": [
59+
1260,
60+
400
61+
]
62+
},
63+
{
64+
"parameters": {
65+
"chatId": "123123",
66+
"text": "1123123",
67+
"additionalFields": {}
68+
},
69+
"id": "1765f352-fc12-4fab-9c24-d666a150266f",
70+
"name": "Telegram",
71+
"type": "n8n-nodes-base.telegram",
72+
"typeVersion": 1.1,
73+
"position": [
74+
1480,
75+
400
76+
]
77+
}
78+
],
79+
"connections": {
80+
"When clicking \"Test workflow\"": {
81+
"main": [
82+
[
83+
{
84+
"node": "DebugHelper",
85+
"type": "main",
86+
"index": 0
87+
}
88+
]
89+
]
90+
},
91+
"DebugHelper": {
92+
"main": [
93+
[
94+
{
95+
"node": "Filter",
96+
"type": "main",
97+
"index": 0
98+
}
99+
]
100+
]
101+
},
102+
"Filter": {
103+
"main": [
104+
[
105+
{
106+
"node": "Telegram",
107+
"type": "main",
108+
"index": 0
109+
}
110+
]
111+
]
112+
}
113+
},
114+
"pinData": {}
115+
}

packages/editor-ui/src/composables/useRunWorkflow.test.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { setActivePinia } from 'pinia';
55
import type { IStartRunData, IWorkflowData } from '@/Interface';
66
import { useWorkflowsStore } from '@/stores/workflows.store';
77
import { useUIStore } from '@/stores/ui.store';
8-
import { useToast } from '@/composables/useToast';
98
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
10-
import { useNodeHelpers } from '@/composables/useNodeHelpers';
119
import { useRouter } from 'vue-router';
1210
import type { IPinData, IRunData, Workflow } from 'n8n-workflow';
1311

@@ -70,7 +68,6 @@ vi.mock('@/composables/useWorkflowHelpers', () => ({
7068

7169
vi.mock('@/composables/useNodeHelpers', () => ({
7270
useNodeHelpers: vi.fn().mockReturnValue({
73-
refreshNodeIssues: vi.fn(),
7471
updateNodesExecutionIssues: vi.fn(),
7572
}),
7673
}));
@@ -94,9 +91,7 @@ describe('useRunWorkflow({ router })', () => {
9491
let uiStore: ReturnType<typeof useUIStore>;
9592
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
9693
let router: ReturnType<typeof useRouter>;
97-
let toast: ReturnType<typeof useToast>;
9894
let workflowHelpers: ReturnType<typeof useWorkflowHelpers>;
99-
let nodeHelpers: ReturnType<typeof useNodeHelpers>;
10095

10196
beforeAll(() => {
10297
const pinia = createTestingPinia();
@@ -108,9 +103,7 @@ describe('useRunWorkflow({ router })', () => {
108103
workflowsStore = useWorkflowsStore();
109104

110105
router = useRouter();
111-
toast = useToast();
112106
workflowHelpers = useWorkflowHelpers({ router });
113-
nodeHelpers = useNodeHelpers();
114107
});
115108

116109
describe('runWorkflowApi()', () => {
@@ -170,22 +163,26 @@ describe('useRunWorkflow({ router })', () => {
170163
expect(result).toBeUndefined();
171164
});
172165

173-
it('should handle workflow issues correctly', async () => {
166+
it('should execute workflow even if it has issues', async () => {
167+
const mockExecutionResponse = { executionId: '123' };
174168
const { runWorkflow } = useRunWorkflow({ router });
175169

176170
vi.mocked(uiStore).isActionActive.mockReturnValue(false);
177171
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
178172
name: 'Test Workflow',
179173
} as unknown as Workflow);
174+
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
180175
vi.mocked(workflowsStore).nodesIssuesExist = true;
181-
vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {});
182-
vi.mocked(workflowHelpers).checkReadyForExecution.mockReturnValue({
183-
someNode: { issues: { input: ['issue'] } },
184-
});
176+
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
177+
id: 'workflowId',
178+
nodes: [],
179+
} as unknown as IWorkflowData);
180+
vi.mocked(workflowsStore).getWorkflowRunData = {
181+
NodeName: [],
182+
};
185183

186184
const result = await runWorkflow({});
187-
expect(result).toBeUndefined();
188-
expect(toast.showMessage).toHaveBeenCalled();
185+
expect(result).toEqual(mockExecutionResponse);
189186
});
190187

191188
it('should execute workflow successfully', async () => {
@@ -198,7 +195,6 @@ describe('useRunWorkflow({ router })', () => {
198195
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
199196
name: 'Test Workflow',
200197
} as Workflow);
201-
vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {});
202198
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
203199
id: 'workflowId',
204200
nodes: [],

packages/editor-ui/src/composables/useRunWorkflow.ts

Lines changed: 8 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,11 @@ import type {
1111
IRunExecutionData,
1212
ITaskData,
1313
IPinData,
14-
IWorkflowBase,
1514
Workflow,
1615
StartNodeData,
1716
IRun,
1817
} from 'n8n-workflow';
19-
import {
20-
NodeHelpers,
21-
NodeConnectionType,
22-
TelemetryHelpers,
23-
FORM_TRIGGER_PATH_IDENTIFIER,
24-
} from 'n8n-workflow';
18+
import { NodeConnectionType, FORM_TRIGGER_PATH_IDENTIFIER } from 'n8n-workflow';
2519

2620
import { useToast } from '@/composables/useToast';
2721
import { useNodeHelpers } from '@/composables/useNodeHelpers';
@@ -42,14 +36,12 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
4236
import type { useRouter } from 'vue-router';
4337
import { isEmpty } from '@/utils/typesUtils';
4438
import { useI18n } from '@/composables/useI18n';
45-
import { useTelemetry } from '@/composables/useTelemetry';
4639
import { get } from 'lodash-es';
4740

48-
export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }) {
41+
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
4942
const nodeHelpers = useNodeHelpers();
50-
const workflowHelpers = useWorkflowHelpers({ router: options.router });
43+
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
5144
const i18n = useI18n();
52-
const telemetry = useTelemetry();
5345
const toast = useToast();
5446
const { titleSet } = useTitleChange();
5547

@@ -106,79 +98,6 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
10698
toast.clearAllStickyNotifications();
10799

108100
try {
109-
// Check first if the workflow has any issues before execute it
110-
nodeHelpers.refreshNodeIssues();
111-
const issuesExist = workflowsStore.nodesIssuesExist;
112-
if (issuesExist) {
113-
// If issues exist get all of the issues of all nodes
114-
const workflowIssues = workflowHelpers.checkReadyForExecution(
115-
workflow,
116-
options.destinationNode,
117-
);
118-
if (workflowIssues !== null) {
119-
const errorMessages = [];
120-
let nodeIssues: string[];
121-
const trackNodeIssues: Array<{
122-
node_type: string;
123-
error: string;
124-
}> = [];
125-
const trackErrorNodeTypes: string[] = [];
126-
for (const nodeName of Object.keys(workflowIssues)) {
127-
nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]);
128-
let issueNodeType = 'UNKNOWN';
129-
const issueNode = workflowsStore.getNodeByName(nodeName);
130-
131-
if (issueNode) {
132-
issueNodeType = issueNode.type;
133-
}
134-
135-
trackErrorNodeTypes.push(issueNodeType);
136-
const trackNodeIssue = {
137-
node_type: issueNodeType,
138-
error: '',
139-
caused_by_credential: !!workflowIssues[nodeName].credentials,
140-
};
141-
142-
for (const nodeIssue of nodeIssues) {
143-
errorMessages.push(
144-
`<a data-action='openNodeDetail' data-action-parameter-node='${nodeName}'>${nodeName}</a>: ${nodeIssue}`,
145-
);
146-
trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue);
147-
}
148-
trackNodeIssues.push(trackNodeIssue);
149-
}
150-
151-
toast.showMessage({
152-
title: i18n.baseText('workflowRun.showMessage.title'),
153-
message: errorMessages.join('<br />'),
154-
type: 'error',
155-
duration: 0,
156-
});
157-
titleSet(workflow.name as string, 'ERROR');
158-
void useExternalHooks().run('workflowRun.runError', {
159-
errorMessages,
160-
nodeName: options.destinationNode,
161-
});
162-
163-
await workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
164-
telemetry.track('Workflow execution preflight failed', {
165-
workflow_id: workflow.id,
166-
workflow_name: workflow.name,
167-
execution_type: options.destinationNode || options.triggerNode ? 'node' : 'workflow',
168-
node_graph_string: JSON.stringify(
169-
TelemetryHelpers.generateNodesGraph(
170-
workflowData as IWorkflowBase,
171-
workflowHelpers.getNodeTypes(),
172-
).nodeGraph,
173-
),
174-
error_node_types: JSON.stringify(trackErrorNodeTypes),
175-
errors: JSON.stringify(trackNodeIssues),
176-
});
177-
});
178-
return;
179-
}
180-
}
181-
182101
// Get the direct parents of the node
183102
let directParentNodes: string[] = [];
184103
if (options.destinationNode !== undefined) {
@@ -319,7 +238,7 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
319238
executedNode,
320239
data: {
321240
resultData: {
322-
runData: newRunData || {},
241+
runData: newRunData ?? {},
323242
pinData: workflowData.pinData,
324243
workflowData,
325244
},
@@ -372,7 +291,9 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
372291
node.parameters.resume === 'form' &&
373292
runWorkflowApiResponse.executionId
374293
) {
375-
const workflowTriggerNodes = workflow.getTriggerNodes().map((node) => node.name);
294+
const workflowTriggerNodes = workflow
295+
.getTriggerNodes()
296+
.map((triggerNode) => triggerNode.name);
376297

377298
const showForm =
378299
options.destinationNode === node.name ||
@@ -383,7 +304,7 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
383304

384305
if (!showForm) continue;
385306

386-
const { webhookSuffix } = (node.parameters.options || {}) as IDataObject;
307+
const { webhookSuffix } = (node.parameters.options ?? {}) as IDataObject;
387308
const suffix = webhookSuffix ? `/${webhookSuffix}` : '';
388309
testUrl = `${rootStore.getFormWaitingUrl}/${runWorkflowApiResponse.executionId}${suffix}`;
389310
}

packages/editor-ui/src/composables/useWorkflowHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
515515
return count;
516516
}
517517

518-
// Checks if everything in the workflow is complete and ready to be executed
518+
/** Checks if everything in the workflow is complete and ready to be executed */
519519
function checkReadyForExecution(workflow: Workflow, lastNodeName?: string) {
520520
let node: INode;
521521
let nodeType: INodeType | undefined;

0 commit comments

Comments
 (0)