Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
schemaTypeField,
} from '@utils/descriptions';
import { convertJsonSchemaToZod, generateSchemaFromExample } from '@utils/schemaParsing';
import { getConnectionHintNoticeField } from '@n8n/ai-utilities';
import { getConnectionHintNoticeField, logAiEvent } from '@n8n/ai-utilities';

import type { DynamicZodObject } from '../../../types/zod.types';

Expand Down Expand Up @@ -119,6 +119,7 @@ function getTool(
void ctx.addOutputData(NodeConnectionTypes.AiTool, index, executionError);
} else if (log) {
void ctx.addOutputData(NodeConnectionTypes.AiTool, index, [[{ json: { response } }]]);
logAiEvent(ctx, 'ai-tool-called', { query, response });
}

return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type {
import { NodeConnectionTypes, NodeOperationError, jsonParse } from 'n8n-workflow';
import { z } from 'zod';

import { logAiEvent } from '@n8n/ai-utilities';

import type {
ParameterInputType,
ParametersValues,
Expand Down Expand Up @@ -784,6 +786,7 @@ export const configureToolFunction = (
void ctx.addOutputData(NodeConnectionTypes.AiTool, index, executionError as ExecutionError);
} else {
void ctx.addOutputData(NodeConnectionTypes.AiTool, index, [[{ json: { response } }]]);
logAiEvent(ctx, 'ai-tool-called', { query, response });
}

return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
INode,
} from 'n8n-workflow';

import { logAiEvent } from '@n8n/ai-utilities';

import { WorkflowToolService } from './utils/WorkflowToolService';

// Mock the sleep functions
Expand All @@ -17,6 +19,11 @@ jest.mock('n8n-workflow', () => ({
sleepWithAbort: jest.fn().mockResolvedValue(undefined),
}));

jest.mock('@n8n/ai-utilities', () => ({
...jest.requireActual('@n8n/ai-utilities'),
logAiEvent: jest.fn(),
}));

function createMockClonedContext(
baseContext: ISupplyDataFunctions,
executeWorkflowMock?: jest.MockedFunction<any>,
Expand Down Expand Up @@ -971,4 +978,71 @@ describe('WorkflowTool::WorkflowToolService', () => {
expect(sleepWithAbort).toHaveBeenCalledWith(100, undefined);
});
});

describe('logAiEvent emission', () => {
const mockLogAiEvent = logAiEvent as jest.MockedFunction<typeof logAiEvent>;

beforeEach(() => {
mockLogAiEvent.mockClear();
});

it('should emit ai-tool-called event on successful execution with manualLogging enabled', 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, 'addInputData').mockReturnValue({ index: 0 });
jest.spyOn(context, 'getNodeParameter').mockReturnValue('database');
jest.spyOn(context, 'getWorkflowDataProxy').mockReturnValue({
$execution: { id: 'exec-id' },
$workflow: { id: 'workflow-id' },
} as unknown as IWorkflowDataProxyData);
jest.spyOn(context, 'cloneWith').mockReturnValue(context);

const tool = await service.createTool({
ctx: context,
name: 'TestTool',
description: 'Test Description',
itemIndex: 0,
});

await tool.func('test query');

expect(mockLogAiEvent).toHaveBeenCalledWith(
expect.anything(),
'ai-tool-called',
expect.objectContaining({ query: 'test query' }),
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The positive-path telemetry test should also assert the logged response payload; currently it only checks the query, so it would miss regressions where ai-tool-called is emitted with the wrong or missing response data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/ToolWorkflowV2.test.ts, line 1017:

<comment>The positive-path telemetry test should also assert the logged `response` payload; currently it only checks the query, so it would miss regressions where `ai-tool-called` is emitted with the wrong or missing response data.</comment>

<file context>
@@ -971,4 +978,71 @@ describe('WorkflowTool::WorkflowToolService', () => {
+			expect(mockLogAiEvent).toHaveBeenCalledWith(
+				expect.anything(),
+				'ai-tool-called',
+				expect.objectContaining({ query: 'test query' }),
+			);
+		});
</file context>
Suggested change
expect.objectContaining({ query: 'test query' }),
expect.objectContaining({
query: 'test query',
response: JSON.stringify(TEST_RESPONSE, null, 2),
}),
Fix with Cubic

);
});

it('should not emit ai-tool-called event when manualLogging is disabled', 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: 'TestTool',
description: 'Test Description',
itemIndex: 0,
manualLogging: false,
});

await tool.func('test query');

expect(mockLogAiEvent).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
sleepWithAbort,
} from 'n8n-workflow';

import { createZodSchemaFromArgs, extractFromAIParameters } from '@n8n/ai-utilities';
import { createZodSchemaFromArgs, extractFromAIParameters, logAiEvent } from '@n8n/ai-utilities';

function isNodeExecutionData(data: unknown): data is INodeExecutionData[] {
return isArray(data) && Boolean(data.length) && isObject(data[0]) && 'json' in data[0];
Expand Down Expand Up @@ -168,6 +168,7 @@ export class WorkflowToolService {
metadata,
);

logAiEvent(context, 'ai-tool-called', { query, response: processedResponse });
return processedResponse;
}
// If manualLogging is false we've been called by the engine and need
Expand Down