feat(core): Add chat trigger builder hints (#30730)

This commit is contained in:
Jaakko Husso 2026-05-25 16:24:43 +03:00 committed by GitHub
parent ca949e15c5
commit 2cdc5f51da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 47 additions and 2 deletions

View File

@ -278,7 +278,18 @@ describe('getSystemPrompt', () => {
const prompt = getSystemPrompt({ webhookBaseUrl, formBaseUrl });
expect(prompt).toContain('**Webhook Trigger**: http://localhost:5678/webhook/{path}');
expect(prompt).toContain('**Chat Trigger**: http://localhost:5678/webhook/{webhookId}/chat');
expect(prompt).toContain('http://localhost:5678/webhook/{webhookId}/chat');
});
it('directs the agent to the Open chat button when Chat Trigger is private', () => {
// Regression: agent was sharing the public webhook URL for private chat
// triggers, then offering to flip `public: true` for testing instead of
// pointing the user at the workflow's built-in Open chat button.
const prompt = getSystemPrompt({ webhookBaseUrl, formBaseUrl });
expect(prompt).toContain('**Open chat** button on the workflow canvas');
expect(prompt).toMatch(/public: false[^]*Do NOT share a webhook URL/);
expect(prompt).toMatch(/do NOT suggest flipping `public: true` just to enable testing/);
});
it('explicitly warns that /form and /webhook are distinct prefixes', () => {

View File

@ -41,7 +41,10 @@ Some trigger nodes expose HTTP endpoints. Always share the full production URL w
- **Webhook Trigger**: ${webhookBaseUrl}/{path} (where {path} is the node's webhook path parameter).
- **Form Trigger**: ${formBaseUrl}/{path} (or ${formBaseUrl}/{webhookId} if no custom path is set). The Form Trigger lives under /form/, NOT /webhook/ they are separate URL prefixes. Do NOT use the Webhook base URL for Form Triggers.
- **Chat Trigger**: ${webhookBaseUrl}/{webhookId}/chat (where {webhookId} is the node's unique webhook ID, visible in the workflow JSON). The /chat suffix is unique to Chat Trigger — do NOT append it to Form Trigger or Webhook URLs. The public chat UI is only accessible to end users when the node's "public" parameter is true and the workflow has been published. (This applies only to end-user HTTP access your own testing via \`executions(action="run")\` and \`verify-built-workflow\` works regardless of publish state.) Do NOT guess the webhookId — read the workflow to find it.
- **Chat Trigger**: how the end user reaches this workflow depends on the node's \`public\` parameter — pick the right guidance for the current value, do not default to sharing a URL.
- **\`public: false\` (the default)**: there is NO end-user HTTP URL. Tell the user to open the workflow in the editor and click the **Open chat** button on the workflow canvas — that opens the built-in test chat where they can talk to the workflow. Do NOT share a webhook URL, and do NOT suggest flipping \`public: true\` just to enable testing — the in-editor chat is the intended testing path for private chat workflows.
- **\`public: true\`**: the public chat URL is ${webhookBaseUrl}/{webhookId}/chat — share it after the workflow is published. {webhookId} is the node's unique webhook ID; read it from the workflow JSON, never guess. End users can open this URL in a browser.
The /chat suffix is unique to Chat Trigger do NOT append it to Form Trigger or Webhook URLs. (Your own testing via \`executions(action="run")\` and \`verify-built-workflow\` works regardless of \`public\` or publish state.)
**These URLs are for sharing with the user only.** Do NOT include them in \`build-workflow-with-agent\` task descriptions — the builder cannot reach the n8n instance via HTTP and will fail if it tries to curl/fetch these URLs.`;
}

View File

@ -74,6 +74,9 @@ const respondNodesResponseMode = {
description: 'Send responses to the chat by using one or more Chat nodes',
};
const responseModeBuilderHint =
"'streaming' (preferred for Agent-backed chats): the connected Agent streams its reply to the widget directly — no extra wiring. Place logging or side-effects on a PARALLEL branch off the trigger or Agent, never inline after the Agent. 'lastNode': the last-executed node's output is sent to the widget — that node MUST emit `{ output: '<reply text>' }` (typically the Agent itself, or a Set node re-shaping data). NEVER terminate the chain with a Data Table insert, HTTP Request, or other side-effect node — their output is not a chat reply and the widget will error. 'responseNodes' / 'responseNode': requires explicit response nodes inside the flow (`@n8n/n8n-nodes-langchain.chat` for chat-hub mode, `n8n-nodes-base.respondToWebhook` for webhook mode).";
const commonOptionsFields: INodeProperties[] = [
// CORS parameters are only valid for when chat is used in hosted or webhook mode
{
@ -266,6 +269,25 @@ export class ChatTrigger extends Node {
})() }}`,
outputs: [NodeConnectionTypes.Main],
builderHint: {
searchHint:
"Pair with `@n8n/n8n-nodes-langchain.agent` for chatbot workflows. Reply delivery is controlled by `options.responseMode` — `streaming` (Agent streams directly to widget) is simplest and preferred. For `lastNode` mode, the workflow's last-executed node MUST output `{ output: '<reply>' }` — typically the Agent itself or a Set node re-shaping data; ending the chain with a Data Table insert, HTTP Request, or other side-effect node will fail. Put logging or persistence on a parallel branch, not inline after the Agent.",
relatedNodes: [
{
nodeType: '@n8n/n8n-nodes-langchain.agent',
relationHint:
"Main reply producer; use `responseMode: 'streaming'` so the Agent streams directly to the widget.",
},
{
nodeType: 'n8n-nodes-base.set',
relationHint:
"Append at the end of a `responseMode: 'lastNode'` chain to re-shape the last node's output into `{ output: '<reply text>' }` when the natural last step (e.g. a Data Table insert) doesn't produce chat-shaped data.",
},
{
nodeType: '@n8n/n8n-nodes-langchain.chat',
relationHint:
"Required for `responseMode: 'responseNodes'`. Place inside the flow wherever you want to emit a reply chunk.",
},
],
inputs: {
ai_memory: {
required: true,
@ -582,6 +604,7 @@ export class ChatTrigger extends Node {
options: [lastNodeResponseMode, respondToWebhookResponseMode],
default: 'lastNode',
description: 'When and how to respond to the webhook',
builderHint: { propertyHint: responseModeBuilderHint },
},
autoSaveHighlightedDataProperty,
],
@ -610,6 +633,7 @@ export class ChatTrigger extends Node {
default: 'lastNode',
description: 'When and how to respond to the webhook',
displayOptions: { show: { '/availableInChat': [false] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
{
displayName: 'Response Mode',
@ -619,6 +643,7 @@ export class ChatTrigger extends Node {
default: 'streaming',
description: 'When and how to respond to the webhook',
displayOptions: { show: { '/availableInChat': [true] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
autoSaveHighlightedDataProperty,
],
@ -646,6 +671,7 @@ export class ChatTrigger extends Node {
default: 'lastNode',
description: 'When and how to respond to the chat',
displayOptions: { show: { '/availableInChat': [false] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
{
displayName: 'Response Mode',
@ -655,6 +681,7 @@ export class ChatTrigger extends Node {
default: 'streaming',
description: 'When and how to respond to the chat',
displayOptions: { show: { '/availableInChat': [true] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
autoSaveHighlightedDataProperty,
],
@ -682,6 +709,7 @@ export class ChatTrigger extends Node {
default: 'lastNode',
description: 'When and how to respond to the chat',
displayOptions: { show: { '/mode': ['webhook'], '/availableInChat': [false] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
{
displayName: 'Response Mode',
@ -691,6 +719,7 @@ export class ChatTrigger extends Node {
default: 'streaming',
description: 'When and how to respond to the chat',
displayOptions: { show: { '/mode': ['webhook'], '/availableInChat': [true] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
{
displayName: 'Response Mode',
@ -700,6 +729,7 @@ export class ChatTrigger extends Node {
default: 'lastNode',
description: 'When and how to respond to the chat',
displayOptions: { show: { '/mode': ['hostedChat'], '/availableInChat': [false] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
{
displayName: 'Response Mode',
@ -709,6 +739,7 @@ export class ChatTrigger extends Node {
default: 'streaming',
description: 'When and how to respond to the chat',
displayOptions: { show: { '/mode': ['hostedChat'], '/availableInChat': [true] } },
builderHint: { propertyHint: responseModeBuilderHint },
},
autoSaveHighlightedDataProperty,
],