diff --git a/packages/cli/src/__tests__/workflow-runner.test.ts b/packages/cli/src/__tests__/workflow-runner.test.ts index c69240c3b2e..1fa45298fac 100644 --- a/packages/cli/src/__tests__/workflow-runner.test.ts +++ b/packages/cli/src/__tests__/workflow-runner.test.ts @@ -9,7 +9,7 @@ import { } from '@n8n/db'; import { Container, Service } from '@n8n/di'; import type { Response } from 'express'; -import { DirectedGraph, WorkflowExecute } from 'n8n-core'; +import { DirectedGraph, WorkflowExecute, WorkflowHasIssuesError } from 'n8n-core'; import * as core from 'n8n-core'; import { type IExecuteData, @@ -357,6 +357,59 @@ describe('run', () => { await expect(runner.run(data)).resolves.toBe('1'); }); }); + + describe('workflow issues pre-flight failure', () => { + function arrangeFailingRunDeps(error: Error) { + const activeExecutions = Container.get(ActiveExecutions); + vi.spyOn(activeExecutions, 'add').mockResolvedValue('1'); + vi.spyOn(Container.get(CredentialsPermissionChecker), 'check').mockResolvedValueOnce(); + vi.spyOn(WorkflowExecute.prototype, 'processRunExecutionData').mockImplementationOnce(() => { + throw error; + }); + vi.spyOn(WorkflowExecuteAdditionalData, 'getBase').mockResolvedValue( + mock(), + ); + + const data = mock({ + workflowData: { nodes: [], id: 'workflow-id', settings: undefined, staticData: {} }, + executionData: createRunExecutionData({}), + triggerToStartFrom: undefined, + startNodes: undefined, + destinationNode: undefined, + userId: 'mock-user-id', + }); + return { data }; + } + + it('surfaces a WorkflowHasIssuesError as a failed run instead of rejecting', async () => { + const error = new WorkflowHasIssuesError( + { node1: { parameters: { field: ['is missing'] } } }, + {}, + ); + const { data } = arrangeFailingRunDeps(error); + // @ts-expect-error Private method + const failExecution = vi.spyOn(runner, 'failExecution').mockResolvedValueOnce(); + const processError = vi.spyOn(runner, 'processError').mockResolvedValueOnce(); + + await expect(runner.run(data)).resolves.toBe('1'); + + expect(failExecution).toHaveBeenCalledWith(data, '1', error); + expect(processError).not.toHaveBeenCalled(); + }); + + it('still rejects on other startup errors', async () => { + const error = new Error('boom'); + const { data } = arrangeFailingRunDeps(error); + // @ts-expect-error Private method + const failExecution = vi.spyOn(runner, 'failExecution').mockResolvedValueOnce(); + const processError = vi.spyOn(runner, 'processError').mockResolvedValueOnce(); + + await expect(runner.run(data)).rejects.toThrowError(error); + + expect(processError).toHaveBeenCalled(); + expect(failExecution).not.toHaveBeenCalled(); + }); + }); }); describe('enqueueExecution', () => { diff --git a/packages/cli/src/workflow-runner.ts b/packages/cli/src/workflow-runner.ts index cc201dc954c..ab31b54b939 100644 --- a/packages/cli/src/workflow-runner.ts +++ b/packages/cli/src/workflow-runner.ts @@ -12,6 +12,7 @@ import { InstanceSettings, StorageConfig, WorkflowExecute, + WorkflowHasIssuesError, } from 'n8n-core'; import type { ExecutionError, @@ -479,6 +480,11 @@ export class WorkflowRunner { ), ); } catch (error) { + if (error instanceof WorkflowHasIssuesError) { + await this.failExecution(data, executionId, error); + return; + } + await this.processError( error, new Date(),