Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## [2.8.1](https://github.com/n8n-io/n8n/compare/n8n@2.8.0...n8n@2.8.1) (2026-02-11)


### Bug Fixes

* **core:** Revert the fix for execution history when flow includes wait node ([#25610](https://github.com/n8n-io/n8n/issues/25610)) ([61394f7](https://github.com/n8n-io/n8n/commit/61394f7ad15849a111ed2f0227a2ab2c2a470e3c))



# [2.8.0](https://github.com/n8n-io/n8n/compare/n8n@2.7.0...n8n@2.8.0) (2026-02-10)


Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "n8n-monorepo",
"version": "2.8.0",
"version": "2.8.1",
"private": true,
"engines": {
"node": ">=22.16",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const plannerOutputSchema = z.object({
suggestedNodes: z
.array(z.string())
.optional()
.describe('Suggested internal n8n node type names (when known)'),
.describe(
'Copy exact node type names from discovery_context_suggested_nodes. Do not modify, prefix, or invent names.',
),
}),
)
.min(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ export interface DiscoveryPromptOptions {
}

const ROLE = `You are a Discovery Agent for n8n AI Workflow Builder.
Identify relevant n8n nodes and their connection-changing parameters for the user's request.`;
Identify relevant n8n nodes and their connection-changing parameters for the user's request.
When the request is underspecified, ask clarifying questions to ensure the right workflow gets built.`;

const N8N_EXECUTION_MODEL = `n8n executes each node once per input item.

Expand All @@ -57,7 +58,7 @@ const PROCESS = `1. Search for nodes matching the user's request using search_no

const PROCESS_WITH_QUESTIONS = `1. Search for nodes matching the user's request using search_nodes tool
2. Identify connection-changing parameters from input/output expressions (look for $parameter.X)
3. If the search results reveal a genuine ambiguity that would lead to very different workflows, ask clarifying questions using submit_questions (see clarifying_questions section)
3. Assess: do you have enough information to build exactly what the user wants, or would you need to make assumptions about their intent? If assumptions are needed, ask clarifying questions using submit_questions (see clarifying_questions section)
4. Call submit_discovery_results with your nodesFound array`;

const PROCESS_WITH_EXAMPLES = `1. Search for nodes matching the user's request using search_nodes tool
Expand All @@ -70,7 +71,7 @@ const PROCESS_WITH_EXAMPLES_AND_QUESTIONS = `1. Search for nodes matching the us
2. Identify connection-changing parameters from input/output expressions (look for $parameter.X)
3. Use get_documentation to retrieve best practices for relevant workflow techniques—this provides proven patterns that improve workflow quality
4. Use get_workflow_examples to find real community workflows using mentioned services—these examples show how experienced users structure similar integrations
5. If the search results reveal a genuine ambiguity that would lead to very different workflows, ask clarifying questions using submit_questions (see clarifying_questions section)
5. Assess: do you have enough information to build exactly what the user wants, or would you need to make assumptions about their intent? If assumptions are needed, ask clarifying questions using submit_questions (see clarifying_questions section)
6. Call submit_discovery_results with your nodesFound array`;

const AI_NODE_SELECTION = `AI node selection guidance:
Expand Down Expand Up @@ -215,22 +216,26 @@ Chat Trigger: n8n-hosted chat interface for conversational AI.
Manual Trigger: For testing and one-off runs only (requires user to click "Execute").
Use when: explicitly testing or debugging workflows`;

const CLARIFYING_QUESTIONS = `You can ask the user clarifying questions using submit_questions. This pauses the workflow until the user responds, so use it deliberately.
const CLARIFYING_QUESTIONS = `You can ask the user clarifying questions using submit_questions. Asking the right questions produces much better workflows — a quick clarification now prevents building the wrong thing.

Always search for nodes FIRST. Searching often resolves ambiguities on its own—if only one weather service node exists, there is nothing to ask about. Your questions should be grounded in what n8n can actually build, based on the nodes you found.
Always search for nodes FIRST. Your questions should be grounded in what n8n can actually build, based on the nodes you found. But finding relevant nodes doesn't mean you know which ones the user actually wants — assess whether the user's intent is clear enough to pick the right ones.

<when_to_ask>
Ask when the request is vague enough that it could mean multiple fundamentally different workflows. Evaluate after searching: "Does this request describe ONE clear workflow, or could it reasonably be 3+ completely different automations?"
Ask when the request has meaningful gaps — missing services, unclear goals, or unspecified triggers — that would force you to guess in ways the user might disagree with. After searching, decide: do you have enough information to build exactly what the user wants, or are you making assumptions they might not agree with? If you'd need to make more than one significant assumption, ask.

Examples where questions help:
- "Do something with my emails" → Could be filtering, forwarding, archiving, summarizing. Ask about the goal.
- "Set up notifications" → Found Email, Slack, Telegram, SMS nodes. Ask which channel.
- "Another automation for weather" → No specific action stated. Ask what should happen.
- "Automate new employee onboarding" → Which systems? What steps? Ask about the scope.
- "Automatically process invoices and update accounting" → Invoices from where? Which accounting tool? Ask about the services.
- "Use AI to help with my content creation" → What kind of content? Blog, social, email? Ask about the use case.

Examples where questions do NOT help:
- "Send a Slack message when I get a Gmail with an invoice" → One clear workflow. Build it.
- "Check weather every hour and store it" → Specific enough. Build it.
- "Monitor my website for downtime" → Reasonable defaults exist. Build it.
- "Check weather every hour and store it" → Specific enough, reasonable defaults exist. Build it.
- "Monitor my website for downtime" → Clear intent, reasonable defaults exist. Build it.
- "Receive webhook POST data and insert it into PostgreSQL" → All details specified. Build it.
</when_to_ask>

<how_to_ask>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ For additionalSpecs: NEVER mention API keys, credentials, authentication, or acc
Rules:
- Do not generate workflow JSON.
- Do not mention internal n8n node type names in steps — describe what happens in plain language.
- You may include suggestedNodes in the structured output for the builder, but the step description should be human-readable.
- You may include suggestedNodes in the structured output for the builder, but the step description should be human-readable. Copy node names exactly from the discovery_context_suggested_nodes section — do not add prefixes, rename, or invent node names.
- If key information is missing, make reasonable assumptions. Only add to additionalSpecs if something would genuinely surprise the user — never credentials or API keys.

<modification_mode>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export const submitQuestionsTool = tool(
name: 'submit_questions',
description: `Ask clarifying questions before proceeding with node discovery. Use when:
- The request is ambiguous or has multiple interpretations
- Critical information is missing (which service, what trigger, etc.)
- User preferences would significantly affect which nodes to find
- Key information is missing (which service, what goal, what trigger, etc.)
- You would need to make assumptions the user might disagree with

Maximum 5 questions. Provide options where possible.
IMPORTANT: Never include "Other" as an option — the UI automatically adds an "Other" free-text input to every question. Only include specific, meaningful options.`,
Expand Down
19 changes: 19 additions & 0 deletions packages/@n8n/ai-workflow-builder.ee/src/utils/stream-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,19 @@ function processOperationsUpdate(update: unknown): StreamOutput | null {
return { messages: [workflowUpdateChunk] };
}

/** Handle create_workflow_name node update - emits name as workflow update */
function processWorkflowNameUpdate(update: unknown): StreamOutput | null {
const typed = update as { workflowJSON?: { name?: string } } | undefined;
if (!typed?.workflowJSON?.name) return null;

const workflowUpdateChunk: WorkflowUpdateChunk = {
role: 'assistant',
type: 'workflow-updated',
codeSnippet: JSON.stringify(typed.workflowJSON, null, 2),
};
return { messages: [workflowUpdateChunk] };
}

/** Handle agent node message update */
function processAgentNodeUpdate(nodeName: string, update: unknown): StreamOutput | null {
if (!shouldEmitFromNode(nodeName)) return null;
Expand Down Expand Up @@ -283,6 +296,12 @@ function processUpdatesChunk(nodeUpdate: Record<string, unknown>): StreamOutput
return processOperationsUpdate(nodeUpdate.process_operations);
}

// Workflow name update - emit so frontend receives the generated name
// before any potential interrupt (e.g., plan mode approval)
if (nodeUpdate.create_workflow_name) {
return processWorkflowNameUpdate(nodeUpdate.create_workflow_name);
}

// Generic agent node handling
for (const [nodeName, update] of Object.entries(nodeUpdate)) {
if (shouldSkipNode(nodeName)) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,40 @@ describe('stream-processor', () => {
expect(message.codeSnippet).toBe(JSON.stringify(workflowData, null, 2));
});

it('should handle create_workflow_name with generated name', () => {
const workflowData = {
nodes: [] as unknown[],
connections: {},
name: 'Email Automation Workflow',
};
const chunk = {
create_workflow_name: {
workflowJSON: workflowData,
},
};

const result = processStreamChunk('updates', chunk);

expect(result).toBeDefined();
expect(result?.messages).toHaveLength(1);
const message = result?.messages[0] as WorkflowUpdateChunk;
expect(message.role).toBe('assistant');
expect(message.type).toBe('workflow-updated');
expect(message.codeSnippet).toBe(JSON.stringify(workflowData, null, 2));
});

it('should ignore create_workflow_name without a name', () => {
const chunk = {
create_workflow_name: {
workflowJSON: { nodes: [], connections: {} },
},
};

const result = processStreamChunk('updates', chunk);

expect(result).toBeNull();
});

it('should ignore chunks without relevant content', () => {
const chunk = {
responder: {
Expand Down
2 changes: 1 addition & 1 deletion packages/@n8n/backend-test-utils/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@n8n/backend-test-utils",
"version": "1.8.0",
"version": "1.8.1",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
Expand Down
2 changes: 1 addition & 1 deletion packages/@n8n/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@n8n/db",
"version": "1.8.0",
"version": "1.8.1",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "2.8.0",
"version": "2.8.1",
"description": "n8n Workflow Automation Tool",
"main": "dist/index",
"types": "dist/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,45 @@ describe('AskAssistantChat', () => {
expect(secondProps.latestStatusText).toBe('assistantChat.thinking.workflowGenerated');
});

it('should not show "Workflow generated" when workflow-updated precedes tools (naming-only)', () => {
// workflow-updated (naming) arrives before discovery tools — should NOT trigger "Workflow generated"
const messages: ChatUI.AssistantMessage[] = [
{
id: 'user-1',
role: 'user' as const,
type: 'text' as const,
content: 'Build a workflow',
},
{
id: 'wu-name',
role: 'assistant' as const,
type: 'workflow-updated' as const,
codeSnippet: '{"nodes": [], "connections": {}, "name": "My Generated Name"}',
},
createToolMessage({
id: 't1',
status: 'completed',
toolName: 'search_nodes',
displayTitle: 'Searching nodes',
}),
{
id: 'resp-1',
role: 'assistant' as const,
type: 'text' as const,
content: 'Here is the plan',
},
];

renderWithDirectives(messages);

expect(thinkingMessageCallCount).toBe(1);

// Tool group should show "Thinking" (not "Workflow generated") since
// the workflow-updated was a naming-only update before any tools
const props = getThinkingMessageProps(0);
expect(props.latestStatusText).toBe('assistantChat.thinking.thinking');
});

it('should preserve "Workflow generated" on first build group when second build group appears', () => {
// Two sequential builds — both should show "Workflow generated"
const messages: ChatUI.AssistantMessage[] = [
Expand Down Expand Up @@ -966,7 +1005,8 @@ describe('AskAssistantChat', () => {

it('should not add thinking spinner to old completed group when new turn is streaming', () => {
// Old completed tool group + response + user's new message → streaming starts
// The old group should stay frozen, not get a "Thinking" spinner
// The old group should stay frozen, not get a "Thinking" spinner.
// A new placeholder thinking message appears for the current turn.
const messages: ChatUI.AssistantMessage[] = [
createToolMessage({
id: '1',
Expand All @@ -989,15 +1029,20 @@ describe('AskAssistantChat', () => {

renderWithDirectives(messages, { streaming: true, loadingMessage: 'Thinking' });

expect(thinkingMessageCallCount).toBe(1);
// Old tool group + new turn's thinking placeholder
expect(thinkingMessageCallCount).toBe(2);

const props = getThinkingMessageProps(0);
// Should NOT have a "thinking-item" spinner appended
expect(props.items).toHaveLength(1);
expect(props.items[0].displayTitle).toBe('Search Results');
expect(props.items[0].status).toBe('completed');
// Title should be frozen as "Thinking", not the dynamic loading message
expect(props.latestStatusText).toBe('assistantChat.thinking.thinking');
// Old group should NOT have a spinner appended
const oldGroupProps = getThinkingMessageProps(0);
expect(oldGroupProps.items).toHaveLength(1);
expect(oldGroupProps.items[0].displayTitle).toBe('Search Results');
expect(oldGroupProps.items[0].status).toBe('completed');
expect(oldGroupProps.latestStatusText).toBe('assistantChat.thinking.thinking');

// New turn's placeholder should show the loading message
const placeholderProps = getThinkingMessageProps(1);
expect(placeholderProps.latestStatusText).toBe('Thinking');
expect(placeholderProps.isStreaming).toBe(true);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,12 @@ function getToolIdsWithWorkflowUpdate(
if (msg.type === 'tool') {
currentGroupToolIds.push(msg.id);
} else if (msg.type === 'workflow-updated') {
hasWorkflowUpdate = true;
// Only count as a workflow update if tools already exist in this region.
// This prevents naming-only updates (which arrive before tools) from
// causing discovery tool groups to show "Workflow generated".
if (currentGroupToolIds.length > 0) {
hasWorkflowUpdate = true;
}
} else {
// Group boundary — flush
if (hasWorkflowUpdate) {
Expand Down Expand Up @@ -305,14 +310,12 @@ const showSuggestions = computed(() => {
return showPlaceholder.value && props.suggestions && props.suggestions.length > 0;
});

// Check if we have any thinking group (tool messages grouped into thinking blocks)
const hasAnyThinkingGroup = computed(() => {
return normalizedMessages.value.some((msg) => msg.type === 'thinking-group');
});

// Show placeholder when streaming with loading message but no tool messages have arrived yet
// Show placeholder when streaming with loading message but no active tool group for the current turn.
// Check the last message — if it's not a thinking-group, the current turn has no tools yet.
const showThinkingPlaceholder = computed(() => {
return props.streaming && props.loadingMessage && !hasAnyThinkingGroup.value;
if (!props.streaming || !props.loadingMessage) return false;
const lastMsg = normalizedMessages.value[normalizedMessages.value.length - 1];
return !lastMsg || lastMsg.type !== 'thinking-group';
});

const showBottomInput = computed(() => {
Expand Down Expand Up @@ -586,8 +589,8 @@ defineExpose({
@submit="onSendMessage"
@stop="emit('stop')"
>
<template v-if="$slots['before-actions']" #beforeActions>
<slot name="before-actions" />
<template v-if="$slots['extra-actions']" #beforeActions>
<slot name="extra-actions" />
</template>
</N8nPromptInput>
</template>
Expand Down Expand Up @@ -652,8 +655,8 @@ defineExpose({
@submit="onSendMessage"
@stop="emit('stop')"
>
<template v-if="$slots['before-actions']" #beforeActions>
<slot name="before-actions" />
<template v-if="$slots['extra-actions']" #beforeActions>
<slot name="extra-actions" />
</template>
</N8nPromptInput>
</div>
Expand Down
5 changes: 4 additions & 1 deletion packages/frontend/@n8n/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,13 @@
"aiAssistant.builder.planMode.selector.plan.description": "Refine requirements first · free",
"aiAssistant.builder.planMode.questions.title": "A few quick questions",
"aiAssistant.builder.planMode.questions.customPlaceholder": "Add details (optional)",
"aiAssistant.builder.planMode.questions.submitButton": "Submit answers",
"aiAssistant.builder.planMode.questions.submitButton": "Submit",
"aiAssistant.builder.planMode.questions.back": "Back",
"aiAssistant.builder.planMode.questions.next": "Next",
"aiAssistant.builder.planMode.questions.skip": "Skip",
"aiAssistant.builder.planMode.questions.skipAndSubmit": "Skip & submit",
"aiAssistant.builder.planMode.questions.other": "Other",
"aiAssistant.builder.planMode.answers.skipped": "Skipped",
"aiAssistant.builder.planMode.plan.title": "Proposed plan",
"aiAssistant.builder.planMode.plan.triggerLabel": "Trigger",
"aiAssistant.builder.planMode.plan.stepsLabel": "Steps",
Expand Down
Loading
Loading