diff --git a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts index cd94da57547..393494bf7f2 100644 --- a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts +++ b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.adapter.service.test.ts @@ -2670,4 +2670,31 @@ describe('createExecutionAdapter run()', () => { }), ); }); + + it('populates executionData for a trigger run with no input so it survives queue persistence', async () => { + const { adapter, mockWorkflowRunner } = createRunAdapterForTests({ + id: 'wf-1', + nodes: [ + { + id: 'node-1', + name: 'Schedule Trigger', + type: 'n8n-nodes-base.scheduleTrigger', + typeVersion: 1, + parameters: {}, + position: [0, 0], + }, + ], + }); + + await adapter.run('wf-1'); + + const runData = mockWorkflowRunner.run.mock.calls[0][0]; + expect(runData.executionMode).toBe('trigger'); + // In queue mode a trigger execution is offloaded to a worker, which reads + // `execution.data` back from storage. An undefined `executionData` persists + // as an empty payload and deserializes to `undefined`, crashing the worker. + expect(runData.executionData).toBeDefined(); + expect(runData.executionData?.manualData?.userId).toBe('user-1'); + expect(runData.executionData?.manualData?.triggerToStartFrom?.name).toBe('Schedule Trigger'); + }); }); diff --git a/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts b/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts index c18a529a52b..041e6f083c8 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts @@ -994,6 +994,19 @@ export class InstanceAiAdapterService { if (Object.keys(basePinData).length > 0) { runData.pinData = basePinData; } + // In queue mode this execution is offloaded to a worker, which reads + // `execution.data` back from storage. Persist a valid run-data object + // (the worker reconstructs the run and starts from the trigger) so an + // undefined payload doesn't deserialize to `undefined` and crash the worker. + runData.executionData = createRunExecutionData({ + startData: {}, + resultData: { pinData: runData.pinData, runData: null }, + manualData: { + userId: user.id, + triggerToStartFrom: runData.triggerToStartFrom, + }, + executionData: null, + }); } else if (Object.keys(basePinData).length > 0) { runData.pinData = basePinData; } diff --git a/packages/cli/src/scaling/__tests__/job-processor.service.test.ts b/packages/cli/src/scaling/__tests__/job-processor.service.test.ts index 46f09341ae3..0e560e334de 100644 --- a/packages/cli/src/scaling/__tests__/job-processor.service.test.ts +++ b/packages/cli/src/scaling/__tests__/job-processor.service.test.ts @@ -126,6 +126,37 @@ describe('JobProcessor', () => { expect(result).toEqual({ success: false }); }); + it('should throw a descriptive error when the execution has no run data', async () => { + const executionRepository = mock(); + const executionPersistence = mock(); + executionPersistence.findSingleExecution.mockResolvedValue( + mock({ + id: 'execution-id', + mode: 'trigger', + workflowData: { nodes: [] }, + data: undefined, + }), + ); + + const manualExecutionService = createManualExecutionServiceMock(); + const jobProcessor = new JobProcessor( + logger, + executionRepository, + executionPersistence, + mock(), + mock(), + mock(), + manualExecutionService, + executionsConfig, + mock(), + ); + + const job = mock({ data: { executionId: 'execution-id', loadStaticData: false } }); + + await expect(jobProcessor.processJob(job)).rejects.toThrow(/without run data/); + expect(manualExecutionService.runManually).not.toHaveBeenCalled(); + }); + it.each(['manual', 'evaluation'] satisfies WorkflowExecuteMode[])( 'should use manualExecutionService to process a job in %p mode', async (mode) => { diff --git a/packages/cli/src/scaling/job-processor.ts b/packages/cli/src/scaling/job-processor.ts index 2af787b6f1d..8523fa35768 100644 --- a/packages/cli/src/scaling/job-processor.ts +++ b/packages/cli/src/scaling/job-processor.ts @@ -96,6 +96,16 @@ export class JobProcessor { */ if (execution.status === 'crashed') return { success: false }; + // A correctly enqueued execution always carries a run-data payload. A missing + // one means the producer persisted no data, which would otherwise surface as an + // opaque `Cannot read properties of undefined` deref further down. Fail with a + // clear, attributable error instead. + if (!execution.data) { + throw new UnexpectedError( + `Worker received execution ${executionId} without run data (job ${job.id})`, + ); + } + const workflowId = execution.workflowData.id; this.logger.info(`Worker started execution ${executionId} (job ${job.id})`, {