From 44d1835797173efc8e089a9fbd34c14bc21eef8d Mon Sep 17 00:00:00 2001 From: Danny Martini Date: Thu, 16 Oct 2025 16:21:55 +0200 Subject: [PATCH] fix(Call n8n Sub-Workflow Tool Node): Return structured data from Workflow Tool when called by engine (#20869) --- .../ToolWorkflow/v2/ToolWorkflowV2.test.ts | 29 +++++++++++++++++++ .../v2/utils/WorkflowToolService.ts | 12 ++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts index 445e867a44d..01cca5a6db5 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts @@ -137,6 +137,35 @@ describe('WorkflowTool::WorkflowToolService', () => { }); }); + it('returns un-stringified data if manualLogging is false (meaning it was called from the engine)', async () => { + const TEST_RESPONSE = { msg: 'test response' }; + + const mockExecuteWorkflowResponse: ExecuteWorkflowData = { + data: [[{ json: TEST_RESPONSE }]], + executionId: 'test-execution', + }; + + jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockExecuteWorkflowResponse); + jest.spyOn(context, 'getNodeParameter').mockReturnValue('database'); + jest.spyOn(context, 'getWorkflowDataProxy').mockReturnValue({ + $execution: { id: 'exec-id' }, + $workflow: { id: 'workflow-id' }, + } as unknown as IWorkflowDataProxyData); + + const tool = await service.createTool({ + ctx: context, + name: 'Test Tool', + description: 'Test Description', + itemIndex: 0, + manualLogging: false, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const result = await tool.func('test query'); + + expect(result).toEqual([{ json: TEST_RESPONSE }]); + }); + it('should handle errors during tool execution', async () => { const toolParams = { ctx: context, diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts index e5c181599ae..43e7eb17fe0 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/utils/WorkflowToolService.ts @@ -160,6 +160,8 @@ export class WorkflowToolService { }; } + // If manualLogging is enabled we've been called by the AgentExecutor + // and have to return a stringified response. if (manualLogging) { void context.addOutputData( NodeConnectionTypes.AiTool, @@ -167,9 +169,13 @@ export class WorkflowToolService { [responseData], metadata, ); + + return processedResponse; } - return processedResponse; + // If manualLogging is false we've been called by the engine and need + // the structured response. + return responseData; } catch (error) { // Check if error is due to cancellation if (abortSignal?.aborted) { @@ -240,7 +246,7 @@ export class WorkflowToolService { items: INodeExecutionData[], workflowProxy: IWorkflowDataProxyData, runManager?: CallbackManagerForToolRun, - ): Promise<{ response: string | IDataObject | INodeExecutionData[]; subExecutionId: string }> { + ): Promise<{ response: IDataObject | INodeExecutionData[]; subExecutionId: string }> { let receivedData: ExecuteWorkflowData; try { receivedData = await context.executeWorkflow(workflowInfo, items, runManager?.getChild(), { @@ -280,7 +286,7 @@ export class WorkflowToolService { query: string | IDataObject, itemIndex: number, runManager?: CallbackManagerForToolRun, - ): Promise { + ): Promise { const source = context.getNodeParameter('source', itemIndex) as string; const workflowProxy = context.getWorkflowDataProxy(0);