Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
7f4e955
fix(ai-builder): Only tidy up nodes if a new node is added
mutdmour Dec 1, 2025
b72c000
add tests, rename functions back
mutdmour Dec 1, 2025
a31e75a
remove test
mutdmour Dec 1, 2025
8493b53
Merge branch 'master' of github.com:n8n-io/n8n into ai-1684
mutdmour Dec 1, 2025
f1d9d24
add params to event
mutdmour Dec 3, 2025
8a457c8
Merge branch 'master' of github.com:n8n-io/n8n into ai-1759
mutdmour Dec 3, 2025
2573d22
add user message id
mutdmour Dec 3, 2025
c7f6d9e
add user message id to be event
mutdmour Dec 3, 2025
1bcee47
refactor multiple events into one
mutdmour Dec 3, 2025
61a1144
merge events
mutdmour Dec 3, 2025
d872b52
rename event
mutdmour Dec 3, 2025
6682f7c
add todos
mutdmour Dec 3, 2025
46851c3
add counts
mutdmour Dec 3, 2025
6464c2c
fix bug
mutdmour Dec 4, 2025
18e0075
Merge branch 'master' of github.com:n8n-io/n8n into ai-1759
mutdmour Dec 4, 2025
50e430a
update sdk version
mutdmour Dec 4, 2025
1509eb4
add user message id when calling agent
mutdmour Dec 4, 2025
ce26c6e
fix tests
mutdmour Dec 4, 2025
a52c4f4
remove unnecessary tests
mutdmour Dec 4, 2025
348263c
clean up code
mutdmour Dec 4, 2025
2f83bbe
Merge branch 'master' of github.com:n8n-io/n8n into ai-1759
mutdmour Dec 4, 2025
56b5002
merge in telemetry branch
mutdmour Dec 4, 2025
5d8b15c
add tests
mutdmour Dec 4, 2025
ac71390
add tests
mutdmour Dec 4, 2025
7efec49
add telemetry events
mutdmour Dec 4, 2025
2fa8d3a
fix type issues
mutdmour Dec 4, 2025
e30248b
reset execs in between messages
mutdmour Dec 4, 2025
454e548
refactor a bit
mutdmour Dec 4, 2025
5de8620
update tests
mutdmour Dec 4, 2025
ad19f2b
fix type issues
mutdmour Dec 4, 2025
0e6212a
rename keys
mutdmour Dec 4, 2025
3287b4b
fix tests
mutdmour Dec 4, 2025
b99744c
fix tool calls
mutdmour Dec 4, 2025
3a764a2
fix types
mutdmour Dec 4, 2025
267da61
Refactor a bit
mutdmour Dec 4, 2025
70bccb8
fix tests
mutdmour Dec 4, 2025
b54d0a2
update builder tests
mutdmour Dec 4, 2025
c033dc4
add event
mutdmour Dec 4, 2025
ebfe114
merge
mutdmour Dec 4, 2025
3973773
Merge branch 'master' of github.com:n8n-io/n8n into ai-1759
mutdmour Dec 5, 2025
ecaefdd
add unit tests
mutdmour Dec 5, 2025
8dfa4ef
fix type
mutdmour Dec 5, 2025
509021c
fix type
mutdmour Dec 5, 2025
54e8eba
merge
mutdmour Dec 5, 2025
9f749ca
update tests
mutdmour Dec 5, 2025
da30946
Merge branch 'master' of github.com:n8n-io/n8n into ai-1759
mutdmour Dec 5, 2025
acf2d16
fix connections bug
mutdmour Dec 5, 2025
83308ac
use short id
mutdmour Dec 5, 2025
483982f
add event
mutdmour Dec 5, 2025
de62da0
Merge branch 'ai-1759' of github.com:n8n-io/n8n into AI-1744
mutdmour Dec 5, 2025
cf414e1
fix tests
mutdmour Dec 5, 2025
82d2d6c
update logic
mutdmour Dec 5, 2025
8d9c477
Merge branch 'ai-1759' of github.com:n8n-io/n8n into AI-1744
mutdmour Dec 5, 2025
df76062
handle edge case of tools
mutdmour Dec 5, 2025
b509108
update tests
mutdmour Dec 5, 2025
6661259
add last message user id to journey event
mutdmour Dec 5, 2025
64d151d
address cubic feedback
mutdmour Dec 5, 2025
ee74836
address conflict
mutdmour Dec 5, 2025
88e456a
Merge branch 'ai-1759' of github.com:n8n-io/n8n into AI-1744
mutdmour Dec 5, 2025
d2931ae
address feedback
mutdmour Dec 8, 2025
1d0fe90
merge conflict
mutdmour Dec 8, 2025
8692918
merge conflict
mutdmour Dec 8, 2025
02a3cad
update tests
mutdmour Dec 8, 2025
a7983c2
remove space
mutdmour Dec 8, 2025
4d1e653
merge master
mutdmour Dec 8, 2025
e94d714
fix lint issue
mutdmour Dec 8, 2025
38c67d0
address feedback
mutdmour Dec 8, 2025
cd77b60
Merge branch 'master' of github.com:n8n-io/n8n into AI-1744
mutdmour Dec 8, 2025
cf34305
remove unecessary logic
mutdmour Dec 8, 2025
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 @@ -882,6 +882,10 @@ describe('TelemetryEventRelay', () => {
num_tags: 0,
public_api: false,
sharing_role: undefined,
meta: undefined, // workflow.meta is undefined in mock
workflow_edited_no_pos: false,
credential_edited: false,
ai_builder_assisted: false,
});
});

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/events/maps/relay.event-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export type RelayEventMap = {
user: UserLike;
workflow: IWorkflowDb;
publicApi: boolean;
previousWorkflow?: IWorkflowDb;
aiBuilderAssisted?: boolean;
};

'workflow-activated': {
Expand Down
39 changes: 32 additions & 7 deletions packages/cli/src/events/relays/telemetry.event-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PROJECT_OWNER_ROLE_SLUG } from '@n8n/permissions';
import { snakeCase } from 'change-case';
import { BinaryDataConfig, InstanceSettings } from 'n8n-core';
import type { ExecutionStatus, INodesGraphResult, ITelemetryTrackProperties } from 'n8n-workflow';
import { TelemetryHelpers } from 'n8n-workflow';
import { hasCredentialChanges, hasNonPositionalChanges, TelemetryHelpers } from 'n8n-workflow';
import os from 'node:os';
import { get as pslGet } from 'psl';

Expand Down Expand Up @@ -621,7 +621,13 @@ export class TelemetryEventRelay extends EventRelay {
});
}

private async workflowSaved({ user, workflow, publicApi }: RelayEventMap['workflow-saved']) {
private async workflowSaved({
user,
workflow,
publicApi,
previousWorkflow,
aiBuilderAssisted,
}: RelayEventMap['workflow-saved']) {
const isCloudDeployment = this.globalConfig.deployment.type === 'cloud';

const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes, {
Expand Down Expand Up @@ -654,6 +660,18 @@ export class TelemetryEventRelay extends EventRelay {
(note) => note.overlapping,
).length;

let workflowEditedNoPos = false;
let credentialEdited = false;
if (previousWorkflow) {
workflowEditedNoPos = hasNonPositionalChanges(
previousWorkflow.nodes,
workflow.nodes,
previousWorkflow.connections,
workflow.connections,
);
credentialEdited = hasCredentialChanges(previousWorkflow.nodes, workflow.nodes);
}

this.telemetry.track('User saved workflow', {
user_id: user.id,
workflow_id: workflow.id,
Expand All @@ -665,6 +683,9 @@ export class TelemetryEventRelay extends EventRelay {
public_api: publicApi,
sharing_role: userRole,
meta: JSON.stringify(workflow.meta),
workflow_edited_no_pos: workflowEditedNoPos,
credential_edited: credentialEdited,
ai_builder_assisted: aiBuilderAssisted ?? false,
});
}

Expand Down Expand Up @@ -813,13 +834,17 @@ export class TelemetryEventRelay extends EventRelay {
manualExecEventProperties.is_managed = credential.isManaged;
}
}
const destinationNodeName = runData.data.startData?.destinationNode.nodeName;
const telemetryPayload: ITelemetryTrackProperties = {
...manualExecEventProperties,
node_type: TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.startData?.destinationNode.nodeName,
)?.type,
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode.nodeName],
node_type: TelemetryHelpers.getNodeTypeForName(workflow, destinationNodeName)?.type,
node_id: nodeGraphResult.nameIndices[destinationNodeName],
node_role: TelemetryHelpers.getNodeRole(
destinationNodeName,
workflow.connections,
this.nodeTypes,
workflow.nodes,
),
};

this.telemetry.track('Manual node exec finished', telemetryPayload);
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/workflows/workflow.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export declare namespace WorkflowRequest {
projectId: string;
parentFolderId?: string;
uiContext?: string;
aiBuilderAssisted?: boolean;
}>;

// TODO: Use a discriminator when CAT-1809 lands
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/workflows/workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export class WorkflowService {
forceSave?: boolean;
publicApi?: boolean;
publishIfActive?: boolean;
aiBuilderAssisted?: boolean;
} = {},
): Promise<WorkflowEntity> {
const {
Expand All @@ -234,6 +235,7 @@ export class WorkflowService {
forceSave = false,
publicApi = false,
publishIfActive = false,
aiBuilderAssisted = false,
} = options;
const workflow = await this.workflowFinderService.findWorkflowForUser(
workflowId,
Expand Down Expand Up @@ -414,6 +416,8 @@ export class WorkflowService {
user,
workflow: updatedWorkflow,
publicApi,
previousWorkflow: workflow,
aiBuilderAssisted,
});

// Activate workflow if requested, or
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/workflows/workflows.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ export class WorkflowsController {
const forceSave = req.query.forceSave === 'true';

let updateData = new WorkflowEntity();
const { tags, parentFolderId, ...rest } = req.body;
const { tags, parentFolderId, aiBuilderAssisted, ...rest } = req.body;

// TODO: Add zod validation for entire `rest` object before assigning to `updateData`
if (
Expand All @@ -445,6 +445,7 @@ export class WorkflowsController {
tagIds: tags,
parentFolderId,
forceSave: isSharingEnabled ? forceSave : true,
aiBuilderAssisted,
});

const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface WorkflowDataUpdate {
meta?: WorkflowMetadata;
parentFolderId?: string;
uiContext?: string;
aiBuilderAssisted?: boolean;
}

export interface WorkflowDataCreate extends WorkflowDataUpdate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ vi.mock('@/app/stores/workflows.store', () => {
nodesIssuesExist: false,
executionWaitingForWebhook: false,
workflowObject: { id: '123' } as Workflow,
workflowValidationIssues: [],
workflow: {
nodes: [],
id: '',
name: '',
active: false,
isArchived: false,
createdAt: '',
updatedAt: '',
connections: {},
versionId: '',
activeVersionId: null,
},
getNodeByName: vi
.fn()
.mockImplementation((name) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { useTemplatesStore } from '@/features/workflows/templates/templates.stor
import { useFocusPanelStore } from '@/app/stores/focusPanel.store';
import { injectWorkflowState, type WorkflowState } from '@/app/composables/useWorkflowState';
import { getResourcePermissions } from '@n8n/permissions';
import { useBuilderStore } from '@/features/ai/assistant/builder.store';

export function useWorkflowSaving({
router,
Expand All @@ -47,6 +48,7 @@ export function useWorkflowSaving({
const telemetry = useTelemetry();
const nodeHelpers = useNodeHelpers();
const templatesStore = useTemplatesStore();
const builderStore = useBuilderStore();
const { getWorkflowDataToSave, checkConflictingWebhooks, getWorkflowProjectRole } =
useWorkflowHelpers();

Expand Down Expand Up @@ -220,6 +222,9 @@ export function useWorkflowSaving({

workflowDataRequest.versionId = workflowsStore.workflowVersionId;

// Check if AI Builder made edits since last save
workflowDataRequest.aiBuilderAssisted = builderStore.getAiBuilderMadeEdits();

const deactivateReason = await getWorkflowDeactivationInfo(
currentWorkflow,
workflowDataRequest,
Expand Down Expand Up @@ -255,6 +260,9 @@ export function useWorkflowSaving({
uiStore.removeActiveAction('workflowSaving');
void useExternalHooks().run('workflow.afterUpdate', { workflowData });

// Reset AI Builder edits flag only after successful save
builderStore.resetAiBuilderMadeEdits();

return true;
} catch (error) {
console.error(error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,20 @@ describe('AI Builder store', () => {
builderStore.resetBuilderChat();
expect(builderStore.chatMessages).toEqual([]);
expect(builderStore.builderThinkingMessage).toBeUndefined();

// Verify last_user_message_id is reset (tracked via trackWorkflowBuilderJourney)
track.mockClear();
builderStore.trackWorkflowBuilderJourney('user_clicked_todo');
expect(track).toHaveBeenCalledWith('Workflow builder journey', {
workflow_id: 'test-workflow-id',
session_id: expect.any(String),
event_type: 'user_clicked_todo',
});
// Should NOT have last_user_message_id after reset
expect(track).not.toHaveBeenCalledWith(
'Workflow builder journey',
expect.objectContaining({ last_user_message_id: expect.any(String) }),
);
});

describe('isAIBuilderEnabled computed property', () => {
Expand Down Expand Up @@ -1709,6 +1723,120 @@ describe('AI Builder store', () => {
});
});

describe('trackWorkflowBuilderJourney', () => {
it('tracks event with workflow_id, session_id, and event_type (without last_user_message_id when no message sent)', () => {
const builderStore = useBuilderStore();

builderStore.trackWorkflowBuilderJourney('user_clicked_todo');

expect(track).toHaveBeenCalledWith('Workflow builder journey', {
workflow_id: 'test-workflow-id',
session_id: expect.any(String),
event_type: 'user_clicked_todo',
});
});

it('includes last_user_message_id after user sends a message', async () => {
const builderStore = useBuilderStore();

apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
onMessage({
messages: [{ type: 'message', role: 'assistant', text: 'Hello!' }],
sessionId: 'test-session',
});
onDone();
});

builderStore.sendChatMessage({ text: 'test' });
await vi.waitFor(() => expect(builderStore.streaming).toBe(false));

track.mockClear();
builderStore.trackWorkflowBuilderJourney('user_clicked_todo');

expect(track).toHaveBeenCalledWith('Workflow builder journey', {
workflow_id: 'test-workflow-id',
session_id: expect.any(String),
event_type: 'user_clicked_todo',
last_user_message_id: expect.any(String),
});
});

it('includes event_properties when provided', () => {
const builderStore = useBuilderStore();

builderStore.trackWorkflowBuilderJourney('user_clicked_todo', {
node_type: 'n8n-nodes-base.httpRequest',
type: 'parameters',
});

expect(track).toHaveBeenCalledWith('Workflow builder journey', {
workflow_id: 'test-workflow-id',
session_id: expect.any(String),
event_type: 'user_clicked_todo',
event_properties: {
node_type: 'n8n-nodes-base.httpRequest',
type: 'parameters',
},
});
});

it('omits event_properties when empty object provided', () => {
const builderStore = useBuilderStore();

builderStore.trackWorkflowBuilderJourney('field_focus_placeholder_in_ndv', {});

expect(track).toHaveBeenCalledWith('Workflow builder journey', {
workflow_id: 'test-workflow-id',
session_id: expect.any(String),
event_type: 'field_focus_placeholder_in_ndv',
});
});

it('omits event_properties when not provided', () => {
const builderStore = useBuilderStore();

builderStore.trackWorkflowBuilderJourney('no_placeholder_values_left');

expect(track).toHaveBeenCalledWith('Workflow builder journey', {
workflow_id: 'test-workflow-id',
session_id: expect.any(String),
event_type: 'no_placeholder_values_left',
});
});

it('includes both event_properties and last_user_message_id when both are present', async () => {
const builderStore = useBuilderStore();

apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
onMessage({
messages: [{ type: 'message', role: 'assistant', text: 'Hello!' }],
sessionId: 'test-session',
});
onDone();
});

builderStore.sendChatMessage({ text: 'test' });
await vi.waitFor(() => expect(builderStore.streaming).toBe(false));

track.mockClear();
builderStore.trackWorkflowBuilderJourney('user_clicked_todo', {
node_type: 'n8n-nodes-base.httpRequest',
type: 'parameters',
});

expect(track).toHaveBeenCalledWith('Workflow builder journey', {
workflow_id: 'test-workflow-id',
session_id: expect.any(String),
event_type: 'user_clicked_todo',
event_properties: {
node_type: 'n8n-nodes-base.httpRequest',
type: 'parameters',
},
last_user_message_id: expect.any(String),
});
});
});

describe('abortStreaming telemetry', () => {
it('tracks end of response with aborted flag when aborting', () => {
const builderStore = useBuilderStore();
Expand Down
Loading
Loading