diff --git a/packages/@n8n/ai-workflow-builder.ee/src/chains/workflow-name.ts b/packages/@n8n/ai-workflow-builder.ee/src/chains/workflow-name.ts
new file mode 100644
index 0000000000..d03285dd50
--- /dev/null
+++ b/packages/@n8n/ai-workflow-builder.ee/src/chains/workflow-name.ts
@@ -0,0 +1,33 @@
+import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
+import { PromptTemplate } from '@langchain/core/prompts';
+import z from 'zod';
+
+const workflowNamingPromptTemplate = PromptTemplate.fromTemplate(
+ `Based on the initial user prompt, please generate a name for the workflow that captures its essence and purpose.
+
+
+{initialPrompt}
+
+
+This name should be concise, descriptive, and suitable for a workflow that automates tasks related to the given prompt. The name should be in a format that is easy to read and understand.
+`,
+);
+
+export async function workflowNameChain(llm: BaseChatModel, initialPrompt: string) {
+ // Use structured output for the workflow name to ensure it meets the required format and length
+ const nameSchema = z.object({
+ name: z.string().min(10).max(128).describe('Name of the workflow based on the prompt'),
+ });
+
+ const modelWithStructure = llm.withStructuredOutput(nameSchema);
+
+ const prompt = await workflowNamingPromptTemplate.invoke({
+ initialPrompt,
+ });
+
+ const structuredOutput = (await modelWithStructure.invoke(prompt)) as z.infer;
+
+ return {
+ name: structuredOutput.name,
+ };
+}
diff --git a/packages/@n8n/ai-workflow-builder.ee/src/types/workflow.ts b/packages/@n8n/ai-workflow-builder.ee/src/types/workflow.ts
index 7f1fddb4c9..635e92bb81 100644
--- a/packages/@n8n/ai-workflow-builder.ee/src/types/workflow.ts
+++ b/packages/@n8n/ai-workflow-builder.ee/src/types/workflow.ts
@@ -3,7 +3,7 @@ import type { IWorkflowBase, INode, IConnections } from 'n8n-workflow';
/**
* Simplified workflow representation containing only nodes and connections
*/
-export type SimpleWorkflow = Pick;
+export type SimpleWorkflow = Pick;
/**
* Workflow operation types that can be applied to the workflow state
@@ -14,4 +14,5 @@ export type WorkflowOperation =
| { type: 'addNodes'; nodes: INode[] }
| { type: 'updateNode'; nodeId: string; updates: Partial }
| { type: 'setConnections'; connections: IConnections }
- | { type: 'mergeConnections'; connections: IConnections };
+ | { type: 'mergeConnections'; connections: IConnections }
+ | { type: 'setName'; name: string };
diff --git a/packages/@n8n/ai-workflow-builder.ee/src/utils/operations-processor.ts b/packages/@n8n/ai-workflow-builder.ee/src/utils/operations-processor.ts
index 4de54825e2..10470392b3 100644
--- a/packages/@n8n/ai-workflow-builder.ee/src/utils/operations-processor.ts
+++ b/packages/@n8n/ai-workflow-builder.ee/src/utils/operations-processor.ts
@@ -15,13 +15,14 @@ export function applyOperations(
let result: SimpleWorkflow = {
nodes: [...workflow.nodes],
connections: { ...workflow.connections },
+ name: workflow.name || '',
};
// Apply each operation in sequence
for (const operation of operations) {
switch (operation.type) {
case 'clear':
- result = { nodes: [], connections: {} };
+ result = { nodes: [], connections: {}, name: '' };
break;
case 'removeNode': {
diff --git a/packages/@n8n/ai-workflow-builder.ee/src/utils/test/operations-processor.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/utils/test/operations-processor.test.ts
index 6c67395165..4feca4305f 100644
--- a/packages/@n8n/ai-workflow-builder.ee/src/utils/test/operations-processor.test.ts
+++ b/packages/@n8n/ai-workflow-builder.ee/src/utils/test/operations-processor.test.ts
@@ -18,6 +18,7 @@ describe('operations-processor', () => {
node3 = createNode({ id: 'node3', name: 'Node 3', position: [500, 100] });
baseWorkflow = {
+ name: 'Test Workflow',
nodes: [node1, node2, node3],
connections: {
node1: {
diff --git a/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts b/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts
index 2bfa7bca2f..8a5aaa5c98 100644
--- a/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts
+++ b/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts
@@ -12,6 +12,7 @@ import type {
NodeExecutionSchema,
} from 'n8n-workflow';
+import { workflowNameChain } from '@/chains/workflow-name';
import { DEFAULT_AUTO_COMPACT_THRESHOLD_TOKENS, MAX_AI_BUILDER_PROMPT_LENGTH } from '@/constants';
import { conversationCompactChain } from './chains/conversation-compact';
@@ -122,7 +123,7 @@ export class WorkflowBuilderAgent {
};
const shouldModifyState = (state: typeof WorkflowState.State) => {
- const { messages } = state;
+ const { messages, workflowContext } = state;
const lastHumanMessage = messages.findLast((m) => m instanceof HumanMessage)!; // There always should be at least one human message in the array
if (lastHumanMessage.content === '/compact') {
@@ -133,6 +134,12 @@ export class WorkflowBuilderAgent {
return 'delete_messages';
}
+ // If the workflow is empty (no nodes),
+ // we consider it initial generation request and auto-generate a name for the workflow.
+ if (workflowContext?.currentWorkflow?.nodes?.length === 0 && messages.length === 1) {
+ return 'create_workflow_name';
+ }
+
if (shouldAutoCompact(state)) {
return 'auto_compact_messages';
}
@@ -162,6 +169,7 @@ export class WorkflowBuilderAgent {
workflowJSON: {
nodes: [],
connections: {},
+ name: '',
},
};
@@ -180,7 +188,7 @@ export class WorkflowBuilderAgent {
}
const { messages, previousSummary } = state;
- const lastHumanMessage = messages[messages.length - 1] as HumanMessage;
+ const lastHumanMessage = messages[messages.length - 1] satisfies HumanMessage;
const isAutoCompact = lastHumanMessage.content !== '/compact';
this.logger?.debug('Compacting conversation history', {
@@ -209,6 +217,40 @@ export class WorkflowBuilderAgent {
};
};
+ /**
+ * Creates a workflow name based on the initial user message.
+ */
+ const createWorkflowName = async (state: typeof WorkflowState.State) => {
+ if (!this.llmSimpleTask) {
+ throw new LLMServiceError('LLM not setup');
+ }
+
+ const { workflowJSON, messages } = state;
+
+ if (messages.length === 1 && messages[0] instanceof HumanMessage) {
+ const initialMessage = messages[0] satisfies HumanMessage;
+
+ if (typeof initialMessage.content !== 'string') {
+ this.logger?.debug(
+ 'Initial message content is not a string, skipping workflow name generation',
+ );
+ return {};
+ }
+
+ this.logger?.debug('Generating workflow name');
+ const { name } = await workflowNameChain(this.llmSimpleTask, initialMessage.content);
+
+ return {
+ workflowJSON: {
+ ...workflowJSON,
+ name,
+ },
+ };
+ }
+
+ return {};
+ };
+
const workflow = new StateGraph(WorkflowState)
.addNode('agent', callModel)
.addNode('tools', customToolExecutor)
@@ -216,10 +258,12 @@ export class WorkflowBuilderAgent {
.addNode('delete_messages', deleteMessages)
.addNode('compact_messages', compactSession)
.addNode('auto_compact_messages', compactSession)
+ .addNode('create_workflow_name', createWorkflowName)
.addConditionalEdges('__start__', shouldModifyState)
.addEdge('tools', 'process_operations')
.addEdge('process_operations', 'agent')
.addEdge('auto_compact_messages', 'agent')
+ .addEdge('create_workflow_name', 'agent')
.addEdge('delete_messages', END)
.addEdge('compact_messages', END)
.addConditionalEdges('agent', shouldContinue);
diff --git a/packages/@n8n/ai-workflow-builder.ee/src/workflow-state.ts b/packages/@n8n/ai-workflow-builder.ee/src/workflow-state.ts
index 7332c63765..641a6a045c 100644
--- a/packages/@n8n/ai-workflow-builder.ee/src/workflow-state.ts
+++ b/packages/@n8n/ai-workflow-builder.ee/src/workflow-state.ts
@@ -68,7 +68,7 @@ export const WorkflowState = Annotation.Root({
// Now a simple field without custom reducer - all updates go through operations
workflowJSON: Annotation({
reducer: (x, y) => y ?? x,
- default: () => ({ nodes: [], connections: {} }),
+ default: () => ({ nodes: [], connections: {}, name: '' }),
}),
// Operations to apply to the workflow - processed by a separate node
workflowOperations: Annotation({
diff --git a/packages/@n8n/ai-workflow-builder.ee/test/test-utils.ts b/packages/@n8n/ai-workflow-builder.ee/test/test-utils.ts
index 5c0a23eb35..3b43f16a10 100644
--- a/packages/@n8n/ai-workflow-builder.ee/test/test-utils.ts
+++ b/packages/@n8n/ai-workflow-builder.ee/test/test-utils.ts
@@ -44,7 +44,7 @@ export const createNode = (overrides: Partial = {}): INode => ({
// Simple workflow builder
export const createWorkflow = (nodes: INode[] = []): SimpleWorkflow => {
- const workflow: SimpleWorkflow = { nodes, connections: {} };
+ const workflow: SimpleWorkflow = { nodes, connections: {}, name: 'Test workflow' };
return workflow;
};
diff --git a/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.test.ts b/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.test.ts
index b37a493d5c..3d06354ef8 100644
--- a/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.test.ts
+++ b/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.test.ts
@@ -1,4 +1,17 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
+
+// Mock workflow saving first before any other imports
+const saveCurrentWorkflowMock = vi.hoisted(() => vi.fn());
+vi.mock('@/composables/useWorkflowSaving', () => ({
+ useWorkflowSaving: vi.fn().mockReturnValue({
+ saveCurrentWorkflow: saveCurrentWorkflowMock,
+ getWorkflowDataToSave: vi.fn(),
+ setDocumentTitle: vi.fn(),
+ executeData: vi.fn(),
+ getNodeTypes: vi.fn().mockReturnValue([]),
+ }),
+}));
+
import { createComponentRenderer } from '@/__tests__/render';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
@@ -12,6 +25,7 @@ import { useBuilderStore } from '@/stores/builder.store';
import { mockedStore } from '@/__tests__/utils';
import { STORES } from '@n8n/stores';
import { useWorkflowsStore } from '@/stores/workflows.store';
+import type { INodeUi } from '@/Interface';
vi.mock('@/event-bus', () => ({
nodeViewEventBus: {
@@ -53,16 +67,6 @@ vi.mock('vue-router', () => {
};
});
-vi.mock('@/composables/useWorkflowSaving', () => ({
- useWorkflowSaving: vi.fn().mockReturnValue({
- saveCurrentWorkflow: vi.fn(),
- getWorkflowDataToSave: vi.fn(),
- setDocumentTitle: vi.fn(),
- executeData: vi.fn(),
- getNodeTypes: vi.fn().mockReturnValue([]),
- }),
-}));
-
const workflowPrompt = 'Create a workflow';
describe('AskAssistantBuild', () => {
const sessionId = faker.string.uuid();
@@ -140,7 +144,10 @@ describe('AskAssistantBuild', () => {
await flushPromises();
- expect(builderStore.sendChatMessage).toHaveBeenCalledWith({ text: testMessage });
+ expect(builderStore.sendChatMessage).toHaveBeenCalledWith({
+ initialGeneration: true,
+ text: testMessage,
+ });
});
});
@@ -313,4 +320,660 @@ describe('AskAssistantBuild', () => {
});
});
});
+
+ describe('workflow saving after generation', () => {
+ it('should save workflow after initial generation when workflow was empty', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ const { findByTestId } = renderComponent();
+
+ // Send initial message to start generation
+ const testMessage = 'Create a workflow to send emails';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ expect(builderStore.sendChatMessage).toHaveBeenCalledWith({
+ initialGeneration: true,
+ text: testMessage,
+ });
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true, initialGeneration: true });
+
+ await flushPromises();
+
+ // Simulate workflow update with nodes
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Add successful message to chat to indicate successful generation
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: testMessage },
+ { id: '2', role: 'assistant', type: 'text', content: 'Workflow created successfully' },
+ ],
+ });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was saved
+ expect(saveCurrentWorkflowMock).toHaveBeenCalled();
+ });
+
+ it('should NOT save workflow after generation when workflow already had nodes', async () => {
+ // Setup: workflow with existing nodes
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'existing',
+ name: 'Existing',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+ workflowsStore.isNewWorkflow = false;
+
+ const { findByTestId } = renderComponent();
+
+ // Send message to modify existing workflow
+ const testMessage = 'Add an HTTP node';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ expect(builderStore.sendChatMessage).toHaveBeenCalledWith({
+ initialGeneration: false,
+ text: testMessage,
+ });
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ // Simulate workflow update with additional nodes
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'existing',
+ name: 'Existing',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ {
+ id: 'node2',
+ name: 'HTTP',
+ type: 'n8n-nodes-base.httpRequest',
+ position: [100, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was NOT saved
+ expect(saveCurrentWorkflowMock).toHaveBeenCalledTimes(0);
+ });
+
+ it('should NOT save workflow when generation ends with error', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ const { findByTestId } = renderComponent();
+
+ // Send initial message
+ const testMessage = 'Create a workflow';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ // The component should have set initialGeneration to true since workflow was empty
+ await flushPromises();
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ // Simulate workflow update with nodes BEFORE error occurs
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Simulate error message added to chat
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: testMessage },
+ { id: '2', role: 'assistant', type: 'error', content: 'An error occurred' },
+ ],
+ });
+
+ // Simulate streaming ends with error
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was NOT saved despite having nodes because of the error
+ expect(saveCurrentWorkflowMock).not.toHaveBeenCalled();
+ });
+
+ it('should NOT save workflow when generation is cancelled', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ const { findByTestId } = renderComponent();
+
+ // Send initial message
+ const testMessage = 'Create a workflow';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ // Simulate workflow update with nodes BEFORE cancellation
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // User cancels generation - this adds a "[Task aborted]" message
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: testMessage },
+ { id: '2', role: 'assistant', type: 'text', content: '[Task aborted]' },
+ ],
+ });
+
+ // Simulate streaming ends after cancellation
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was NOT saved despite having nodes because generation was cancelled
+ expect(saveCurrentWorkflowMock).not.toHaveBeenCalled();
+ });
+
+ it('should save new workflow before sending first message', async () => {
+ // Setup: new workflow
+ workflowsStore.isNewWorkflow = true;
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+
+ const { findByTestId } = renderComponent();
+
+ // Send initial message
+ const testMessage = 'Create a workflow';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ await flushPromises();
+
+ // Verify workflow was saved to get ID for session
+ expect(saveCurrentWorkflowMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('should work when opening existing AI builder session', async () => {
+ // Setup: existing workflow with AI session
+ workflowsStore.workflowId = 'existing-id';
+ workflowsStore.isNewWorkflow = false;
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+
+ // Simulate existing AI session messages
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: 'Previous message' },
+ { id: '2', role: 'assistant', type: 'text', content: 'Previous response' },
+ ],
+ });
+
+ const { findByTestId } = renderComponent();
+
+ // Send new message in existing session
+ const testMessage = 'Add email nodes';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true, initialGeneration: true });
+ await flushPromises();
+
+ // Add nodes to workflow
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Email',
+ type: 'n8n-nodes-base.emailSend',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Add successful message to chat (building on existing session messages)
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: 'Previous message' },
+ { id: '2', role: 'assistant', type: 'text', content: 'Previous response' },
+ { id: '3', role: 'user', type: 'text', content: testMessage },
+ { id: '4', role: 'assistant', type: 'text', content: 'Added email nodes successfully' },
+ ],
+ });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was saved
+ expect(saveCurrentWorkflowMock).toHaveBeenCalled();
+ });
+
+ it('should save workflow when user deletes all nodes then regenerates', async () => {
+ // Setup: workflow with existing nodes
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'existing',
+ name: 'Existing',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+ workflowsStore.isNewWorkflow = false;
+
+ const { findByTestId } = renderComponent();
+
+ // User manually deletes all nodes (simulated by clearing workflow)
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ await flushPromises();
+
+ // Send message to generate new workflow
+ const testMessage = 'Create a new workflow';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ expect(builderStore.sendChatMessage).toHaveBeenCalledWith({
+ initialGeneration: true,
+ text: testMessage,
+ });
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true, initialGeneration: true });
+ await flushPromises();
+
+ // Add new nodes to workflow
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'new-node',
+ name: 'New Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Add successful message to chat
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: testMessage },
+ {
+ id: '2',
+ role: 'assistant',
+ type: 'text',
+ content: 'New workflow created successfully',
+ },
+ ],
+ });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was saved after regeneration
+ expect(saveCurrentWorkflowMock).toHaveBeenCalled();
+ });
+
+ it('should NOT save if workflow is still empty after generation ends', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ const { findByTestId } = renderComponent();
+
+ // Send message
+ const testMessage = 'Create a workflow';
+ const chatInput = await findByTestId('chat-input');
+ await fireEvent.update(chatInput, testMessage);
+ const sendButton = await findByTestId('send-message-button');
+ await fireEvent.click(sendButton);
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ // Workflow remains empty (maybe AI couldn't generate anything)
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was NOT saved
+ expect(saveCurrentWorkflowMock).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('canvas-initiated generation', () => {
+ it('should save workflow after initial generation from canvas', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ renderComponent();
+
+ // Simulate canvas-initiated generation with initialGeneration flag
+ builderStore.initialGeneration = true;
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ // Simulate workflow update with nodes
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Add successful message to chat
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: 'Create workflow from canvas' },
+ { id: '2', role: 'assistant', type: 'text', content: 'Workflow created successfully' },
+ ],
+ });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was saved
+ expect(saveCurrentWorkflowMock).toHaveBeenCalled();
+ // Verify initialGeneration flag was reset
+ expect(builderStore.initialGeneration).toBe(false);
+ });
+
+ it('should NOT save workflow from canvas when generation fails', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ renderComponent();
+
+ // Simulate canvas-initiated generation with initialGeneration flag
+ builderStore.initialGeneration = true;
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ // Simulate workflow update with nodes
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Add error message to chat
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: 'Create workflow from canvas' },
+ { id: '2', role: 'assistant', type: 'error', content: 'Generation failed' },
+ ],
+ });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was NOT saved
+ expect(saveCurrentWorkflowMock).not.toHaveBeenCalled();
+ // Verify initialGeneration flag was reset
+ expect(builderStore.initialGeneration).toBe(false);
+ });
+
+ it('should NOT save workflow from canvas when generation is cancelled', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ renderComponent();
+
+ // Simulate canvas-initiated generation with initialGeneration flag
+ builderStore.initialGeneration = true;
+
+ // Simulate streaming starts
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ // Simulate workflow update with nodes
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ // Add cancellation message to chat
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: 'Create workflow from canvas' },
+ { id: '2', role: 'assistant', type: 'text', content: '[Task aborted]' },
+ ],
+ });
+
+ // Simulate streaming ends
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify workflow was NOT saved
+ expect(saveCurrentWorkflowMock).not.toHaveBeenCalled();
+ // Verify initialGeneration flag was reset
+ expect(builderStore.initialGeneration).toBe(false);
+ });
+
+ it('should handle multiple canvas generations correctly', async () => {
+ // Setup: empty workflow
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+ workflowsStore.isNewWorkflow = false;
+
+ renderComponent();
+
+ // First canvas generation
+ builderStore.initialGeneration = true;
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [0, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: 'First generation' },
+ { id: '2', role: 'assistant', type: 'text', content: 'Success' },
+ ],
+ });
+
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify first generation saved
+ expect(saveCurrentWorkflowMock).toHaveBeenCalledTimes(1);
+ expect(builderStore.initialGeneration).toBe(false);
+
+ // User clears workflow manually
+ workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
+
+ // Second canvas generation
+ builderStore.initialGeneration = true;
+ builderStore.$patch({ streaming: true });
+ await flushPromises();
+
+ workflowsStore.$patch({
+ workflow: {
+ nodes: [
+ {
+ id: 'node2',
+ name: 'HTTP',
+ type: 'n8n-nodes-base.httpRequest',
+ position: [100, 0],
+ typeVersion: 1,
+ parameters: {},
+ } as INodeUi,
+ ],
+ connections: {},
+ },
+ });
+
+ builderStore.$patch({
+ chatMessages: [
+ { id: '1', role: 'user', type: 'text', content: 'First generation' },
+ { id: '2', role: 'assistant', type: 'text', content: 'Success' },
+ { id: '3', role: 'user', type: 'text', content: 'Second generation' },
+ { id: '4', role: 'assistant', type: 'text', content: 'Success again' },
+ ],
+ });
+
+ builderStore.$patch({ streaming: false });
+ await flushPromises();
+
+ // Verify second generation also saved
+ expect(saveCurrentWorkflowMock).toHaveBeenCalledTimes(2);
+ expect(builderStore.initialGeneration).toBe(false);
+ });
+ });
});
diff --git a/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue b/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue
index e9623e898d..d308b39ce2 100644
--- a/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue
+++ b/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue
@@ -44,7 +44,11 @@ async function onUserMessage(content: string) {
if (isNewWorkflow) {
await workflowSaver.saveCurrentWorkflow();
}
- builderStore.sendChatMessage({ text: content });
+
+ // If the workflow is empty, set the initial generation flag
+ const isInitialGeneration = workflowsStore.workflow.nodes.length === 0;
+
+ builderStore.sendChatMessage({ text: content, initialGeneration: isInitialGeneration });
}
// Watch for workflow updates and apply them
@@ -70,6 +74,7 @@ watch(
nodesIdsToTidyUp: result.newNodeIds,
regenerateIds: false,
});
+
// Track tool usage for telemetry
const newToolMessages = builderStore.toolMessages.filter(
(toolMsg) =>
@@ -94,6 +99,33 @@ watch(
{ deep: true },
);
+// If this is the initial generation, streaming has ended, and there were workflow updates,
+// we want to save the workflow
+watch(
+ () => builderStore.streaming,
+ async () => {
+ if (
+ builderStore.initialGeneration &&
+ !builderStore.streaming &&
+ workflowsStore.workflow.nodes.length > 0
+ ) {
+ // Check if the generation completed successfully (no error or cancellation)
+ const lastMessage = builderStore.chatMessages[builderStore.chatMessages.length - 1];
+ const successful =
+ lastMessage &&
+ lastMessage.type !== 'error' &&
+ !(lastMessage.type === 'text' && lastMessage.content === '[Task aborted]');
+
+ builderStore.initialGeneration = false;
+
+ // Only save if generation completed successfully
+ if (successful) {
+ await workflowSaver.saveCurrentWorkflow();
+ }
+ }
+ },
+);
+
function onNewWorkflow() {
builderStore.resetBuilderChat();
processedWorkflowUpdates.value.clear();
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts
index 88198a343f..eea3c85a7b 100644
--- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts
+++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts
@@ -129,6 +129,7 @@ describe('CanvasNodeAIPrompt', () => {
await waitFor(() => {
expect(openChat).toHaveBeenCalled();
expect(sendChatMessage).toHaveBeenCalledWith({
+ initialGeneration: true,
text: 'Test prompt',
source: 'canvas',
});
@@ -174,6 +175,7 @@ describe('CanvasNodeAIPrompt', () => {
await waitFor(() => {
expect(openChat).toHaveBeenCalled();
expect(sendChatMessage).toHaveBeenCalledWith({
+ initialGeneration: true,
text: 'Test workflow prompt',
source: 'canvas',
});
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue
index a50c182b83..d771015d74 100644
--- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue
@@ -55,7 +55,8 @@ async function onSubmit() {
// Here we need to await for chat to open and session to be loaded
await builderStore.openChat();
isLoading.value = false;
- builderStore.sendChatMessage({ text: prompt.value, source: 'canvas' });
+ // Always pass initialGeneration as true from canvas since the prompt only shows on empty canvas
+ builderStore.sendChatMessage({ text: prompt.value, source: 'canvas', initialGeneration: true });
}
/**
diff --git a/packages/frontend/editor-ui/src/stores/builder.store.test.ts b/packages/frontend/editor-ui/src/stores/builder.store.test.ts
index 5f3eed0405..36acaa9fae 100644
--- a/packages/frontend/editor-ui/src/stores/builder.store.test.ts
+++ b/packages/frontend/editor-ui/src/stores/builder.store.test.ts
@@ -9,7 +9,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { defaultSettings } from '../__tests__/defaults';
import merge from 'lodash/merge';
import { DEFAULT_POSTHOG_SETTINGS } from './posthog.test';
-import { WORKFLOW_BUILDER_EXPERIMENT } from '@/constants';
+import { WORKFLOW_BUILDER_EXPERIMENT, DEFAULT_NEW_WORKFLOW_NAME } from '@/constants';
import { reactive } from 'vue';
import * as chatAPI from '@/api/ai';
import * as telemetryModule from '@/composables/useTelemetry';
@@ -24,6 +24,36 @@ vi.mock('@n8n/i18n', () => ({
}),
}));
+// Mock useToast
+vi.mock('@/composables/useToast', () => ({
+ useToast: () => ({
+ showMessage: vi.fn(),
+ }),
+}));
+
+// Mock the workflows store
+const mockSetWorkflowName = vi.fn();
+const mockRemoveAllConnections = vi.fn();
+const mockRemoveAllNodes = vi.fn();
+const mockWorkflow = {
+ name: DEFAULT_NEW_WORKFLOW_NAME,
+ nodes: [],
+ connections: {},
+};
+
+vi.mock('./workflows.store', () => ({
+ useWorkflowsStore: vi.fn(() => ({
+ workflow: mockWorkflow,
+ workflowId: 'test-workflow-id',
+ allNodes: [],
+ nodesByName: {},
+ workflowExecutionData: null,
+ setWorkflowName: mockSetWorkflowName,
+ removeAllConnections: mockRemoveAllConnections,
+ removeAllNodes: mockRemoveAllNodes,
+ })),
+}));
+
let settingsStore: ReturnType;
let posthogStore: ReturnType;
@@ -76,6 +106,14 @@ describe('AI Builder store', () => {
posthogStore = usePostHog();
posthogStore.init();
track.mockReset();
+ // Reset workflow store mocks
+ mockSetWorkflowName.mockReset();
+ mockRemoveAllConnections.mockReset();
+ mockRemoveAllNodes.mockReset();
+ // Reset workflow to default state
+ mockWorkflow.name = DEFAULT_NEW_WORKFLOW_NAME;
+ mockWorkflow.nodes = [];
+ mockWorkflow.connections = {};
});
afterEach(() => {
@@ -778,4 +816,240 @@ describe('AI Builder store', () => {
expect(userMessage).not.toHaveProperty('ratingStyle');
});
});
+
+ describe('applyWorkflowUpdate with workflow naming', () => {
+ it('should apply generated workflow name during initial generation when workflow has default name', () => {
+ const builderStore = useBuilderStore();
+
+ // Set initial generation flag
+ builderStore.initialGeneration = true;
+
+ // Ensure workflow has default name
+ mockWorkflow.name = DEFAULT_NEW_WORKFLOW_NAME;
+
+ // Create workflow JSON with a generated name
+ const workflowJson = JSON.stringify({
+ name: 'Generated Workflow Name for Email Processing',
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [250, 300],
+ parameters: {},
+ },
+ ],
+ connections: {},
+ });
+
+ // Apply the workflow update
+ const result = builderStore.applyWorkflowUpdate(workflowJson);
+
+ // Verify the update was successful
+ expect(result.success).toBe(true);
+
+ // Verify setWorkflowName was called with the generated name
+ expect(mockSetWorkflowName).toHaveBeenCalledWith({
+ newName: 'Generated Workflow Name for Email Processing',
+ setStateDirty: false,
+ });
+ });
+
+ it('should NOT apply generated workflow name during initial generation when workflow has custom name', () => {
+ const builderStore = useBuilderStore();
+
+ // Set initial generation flag
+ builderStore.initialGeneration = true;
+
+ // Set a custom workflow name (not the default)
+ mockWorkflow.name = 'My Custom Workflow';
+
+ // Create workflow JSON with a generated name
+ const workflowJson = JSON.stringify({
+ name: 'Generated Workflow Name for Email Processing',
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [250, 300],
+ parameters: {},
+ },
+ ],
+ connections: {},
+ });
+
+ // Apply the workflow update
+ const result = builderStore.applyWorkflowUpdate(workflowJson);
+
+ // Verify the update was successful
+ expect(result.success).toBe(true);
+
+ // Verify setWorkflowName was NOT called
+ expect(mockSetWorkflowName).not.toHaveBeenCalled();
+ });
+
+ it('should NOT apply generated workflow name when not initial generation', () => {
+ const builderStore = useBuilderStore();
+
+ // Ensure initial generation flag is false
+ builderStore.initialGeneration = false;
+
+ // Ensure workflow has default name
+ mockWorkflow.name = DEFAULT_NEW_WORKFLOW_NAME;
+
+ // Create workflow JSON with a generated name
+ const workflowJson = JSON.stringify({
+ name: 'Generated Workflow Name for Email Processing',
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [250, 300],
+ parameters: {},
+ },
+ ],
+ connections: {},
+ });
+
+ // Apply the workflow update
+ const result = builderStore.applyWorkflowUpdate(workflowJson);
+
+ // Verify the update was successful
+ expect(result.success).toBe(true);
+
+ // Verify setWorkflowName was NOT called
+ expect(mockSetWorkflowName).not.toHaveBeenCalled();
+ });
+
+ it('should handle workflow updates without name property', () => {
+ const builderStore = useBuilderStore();
+
+ // Set initial generation flag
+ builderStore.initialGeneration = true;
+
+ // Ensure workflow has default name
+ mockWorkflow.name = DEFAULT_NEW_WORKFLOW_NAME;
+
+ // Create workflow JSON without a name property
+ const workflowJson = JSON.stringify({
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [250, 300],
+ parameters: {},
+ },
+ ],
+ connections: {},
+ });
+
+ // Apply the workflow update
+ const result = builderStore.applyWorkflowUpdate(workflowJson);
+
+ // Verify the update was successful
+ expect(result.success).toBe(true);
+
+ // Verify setWorkflowName was NOT called
+ expect(mockSetWorkflowName).not.toHaveBeenCalled();
+ });
+
+ it('should handle workflow names that start with but are not exactly the default name', () => {
+ const builderStore = useBuilderStore();
+
+ // Set initial generation flag
+ builderStore.initialGeneration = true;
+
+ // Set workflow name that starts with default but has more text
+ mockWorkflow.name = `${DEFAULT_NEW_WORKFLOW_NAME} - Copy`;
+
+ // Create workflow JSON with a generated name
+ const workflowJson = JSON.stringify({
+ name: 'Generated Workflow Name for Email Processing',
+ nodes: [
+ {
+ id: 'node1',
+ name: 'Start',
+ type: 'n8n-nodes-base.start',
+ position: [250, 300],
+ parameters: {},
+ },
+ ],
+ connections: {},
+ });
+
+ // Apply the workflow update
+ const result = builderStore.applyWorkflowUpdate(workflowJson);
+
+ // Verify the update was successful
+ expect(result.success).toBe(true);
+
+ // Verify setWorkflowName WAS called because the name starts with default
+ expect(mockSetWorkflowName).toHaveBeenCalledWith({
+ newName: 'Generated Workflow Name for Email Processing',
+ setStateDirty: false,
+ });
+ });
+
+ it('should handle malformed JSON gracefully', () => {
+ const builderStore = useBuilderStore();
+
+ // Set initial generation flag
+ builderStore.initialGeneration = true;
+
+ // Create malformed JSON
+ const workflowJson = '{ invalid json }';
+
+ // Apply the workflow update
+ const result = builderStore.applyWorkflowUpdate(workflowJson);
+
+ // Verify the update failed
+ expect(result.success).toBe(false);
+ expect(result.error).toBeDefined();
+ });
+
+ it('should maintain initial generation flag state across multiple updates', () => {
+ const builderStore = useBuilderStore();
+
+ // Set initial generation flag
+ builderStore.initialGeneration = true;
+
+ // Ensure workflow has default name
+ mockWorkflow.name = DEFAULT_NEW_WORKFLOW_NAME;
+
+ // First update with name
+ const workflowJson1 = JSON.stringify({
+ name: 'First Generated Name',
+ nodes: [],
+ connections: {},
+ });
+
+ builderStore.applyWorkflowUpdate(workflowJson1);
+ expect(mockSetWorkflowName).toHaveBeenCalledTimes(1);
+
+ // The flag should still be true for subsequent updates in the same generation
+ expect(builderStore.initialGeneration).toBe(true);
+
+ // Second update without name (simulating further tool operations)
+ const workflowJson2 = JSON.stringify({
+ nodes: [
+ {
+ id: 'node2',
+ name: 'HTTP',
+ type: 'n8n-nodes-base.httpRequest',
+ position: [450, 300],
+ parameters: {},
+ },
+ ],
+ connections: {},
+ });
+
+ builderStore.applyWorkflowUpdate(workflowJson2);
+
+ // Should not call setWorkflowName again
+ expect(mockSetWorkflowName).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/packages/frontend/editor-ui/src/stores/builder.store.ts b/packages/frontend/editor-ui/src/stores/builder.store.ts
index d3f593dce2..14b82254c2 100644
--- a/packages/frontend/editor-ui/src/stores/builder.store.ts
+++ b/packages/frontend/editor-ui/src/stores/builder.store.ts
@@ -1,5 +1,6 @@
import type { VIEWS } from '@/constants';
import {
+ DEFAULT_NEW_WORKFLOW_NAME,
ASK_AI_SLIDE_OUT_DURATION_MS,
EDITABLE_CANVAS_VIEWS,
WORKFLOW_BUILDER_EXPERIMENT,
@@ -37,6 +38,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
const streaming = ref(false);
const assistantThinkingMessage = ref();
const streamingAbortController = ref(null);
+ const initialGeneration = ref(false);
// Store dependencies
const settings = useSettingsStore();
@@ -101,6 +103,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
function resetBuilderChat() {
chatMessages.value = clearMessages();
assistantThinkingMessage.value = undefined;
+ initialGeneration.value = false;
}
/**
@@ -234,12 +237,18 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
text: string;
source?: 'chat' | 'canvas';
quickReplyType?: string;
+ initialGeneration?: boolean;
}) {
if (streaming.value) {
return;
}
const { text, source = 'chat', quickReplyType } = options;
+
+ // Set initial generation flag if provided
+ if (options.initialGeneration !== undefined) {
+ initialGeneration.value = options.initialGeneration;
+ }
const messageId = generateMessageId();
const currentWorkflowJson = getWorkflowSnapshot();
@@ -370,12 +379,22 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
}
// Capture current state before clearing
- const { nodePositions } = captureCurrentWorkflowState();
+ const { nodePositions, existingNodeIds } = captureCurrentWorkflowState();
// Clear existing workflow
workflowsStore.removeAllConnections({ setStateDirty: false });
workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
+ // For the initial generation, we want to apply auto-generated workflow name
+ // but only if the workflow has default name
+ if (
+ workflowData.name &&
+ initialGeneration.value &&
+ workflowsStore.workflow.name.startsWith(DEFAULT_NEW_WORKFLOW_NAME)
+ ) {
+ workflowsStore.setWorkflowName({ newName: workflowData.name, setStateDirty: false });
+ }
+
// Restore positions for nodes that still exist and identify new nodes
const nodesIdsToTidyUp: string[] = [];
if (workflowData.nodes) {
@@ -391,7 +410,12 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
});
}
- return { success: true, workflowData, newNodeIds: nodesIdsToTidyUp };
+ return {
+ success: true,
+ workflowData,
+ newNodeIds: nodesIdsToTidyUp,
+ oldNodeIds: Array.from(existingNodeIds),
+ };
}
function getWorkflowSnapshot() {
@@ -416,6 +440,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
workflowMessages,
trackingSessionId,
streamingAbortController,
+ initialGeneration,
// Methods
updateWindowWidth,