refactor: Rename node-level builderHint.message to searchHint and propertyHint (#30062)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mutasem Aldmour 2026-05-08 15:32:50 +02:00 committed by GitHub
parent 7e6bca1f13
commit 72eca2f398
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 634 additions and 322 deletions

View File

@ -165,7 +165,9 @@ export class CodeBuilderNodeSearchEngine {
inputs: item.inputs,
outputs: item.outputs,
score,
...(item.builderHint?.message && { builderHintMessage: item.builderHint.message }),
...(item.builderHint?.searchHint && {
builderHintMessage: item.builderHint.searchHint,
}),
...(subnodeRequirements.length > 0 && { subnodeRequirements }),
};
},
@ -210,8 +212,8 @@ export class CodeBuilderNodeSearchEngine {
inputs: nodeType.inputs,
outputs: nodeType.outputs,
score: connectionScore,
...(nodeType.builderHint?.message && {
builderHintMessage: nodeType.builderHint.message,
...(nodeType.builderHint?.searchHint && {
builderHintMessage: nodeType.builderHint.searchHint,
}),
...(subnodeRequirements.length > 0 && { subnodeRequirements }),
};
@ -240,7 +242,9 @@ export class CodeBuilderNodeSearchEngine {
inputs: item.inputs,
outputs: item.outputs,
score: connectionScore + nameScore,
...(item.builderHint?.message && { builderHintMessage: item.builderHint.message }),
...(item.builderHint?.searchHint && {
builderHintMessage: item.builderHint.searchHint,
}),
...(subnodeRequirements.length > 0 && { subnodeRequirements }),
};
});

View File

@ -627,7 +627,7 @@ describe('CodeBuilderNodeSearchEngine', () => {
displayOptions: { show: { hasOutputParser: [true] } },
},
},
message: 'Use with output parser for structured output',
searchHint: 'Use with output parser for structured output',
},
});
const engine = new CodeBuilderNodeSearchEngine([agentNode]);
@ -708,7 +708,7 @@ describe('CodeBuilderNodeSearchEngine', () => {
displayOptions: { show: { hasOutputParser: [true] } },
},
},
message: 'Test hint message',
searchHint: 'Test hint message',
},
});
const engine = new CodeBuilderNodeSearchEngine([agentNode]);

View File

@ -63,7 +63,7 @@ function formatBuilderHint(
version: number,
): string {
const nodeType = nodeTypeParser.getNodeType(nodeId, version);
const hint = nodeType?.builderHint?.message;
const hint = nodeType?.builderHint?.searchHint;
if (!hint) return '';
return ` @builderHint ${hint}`;
}
@ -224,7 +224,7 @@ function formatModeForDisplay(mode: ModeInfo, showSdkMapping: boolean): string {
// Add builder hint if available
if (mode.builderHint) {
lines.push(` @builderHint ${mode.builderHint.message}`);
lines.push(` @builderHint ${mode.builderHint.propertyHint}`);
}
return lines.join('\n');
@ -306,7 +306,7 @@ function formatResourceOperationLines(
lines.push(` ${resource.description}`);
}
if (resource.builderHint) {
lines.push(` @builderHint ${resource.builderHint.message}`);
lines.push(` @builderHint ${resource.builderHint.propertyHint}`);
}
lines.push(' operations:');
@ -316,7 +316,7 @@ function formatResourceOperationLines(
lines.push(` ${op.description}`);
}
if (op.builderHint) {
lines.push(` @builderHint ${op.builderHint.message}`);
lines.push(` @builderHint ${op.builderHint.propertyHint}`);
}
}
}
@ -339,7 +339,7 @@ function formatOperationLines(operations: DiscriminatorOperationInfo[], nodeId:
lines.push(` ${op.description}`);
}
if (op.builderHint) {
lines.push(` @builderHint ${op.builderHint.message}`);
lines.push(` @builderHint ${op.builderHint.propertyHint}`);
}
}

View File

@ -166,7 +166,7 @@ const mockFormTriggerNode: INodeTypeDescription = {
outputs: ['main'],
properties: [],
builderHint: {
message:
searchHint:
'Use with n8n-nodes-base.form to build a full form experience, with pages and final page',
relatedNodes: [{ nodeType: 'n8n-nodes-base.form', relationHint: 'Build full form experience' }],
},
@ -183,7 +183,7 @@ const mockFormNode: INodeTypeDescription = {
outputs: ['main'],
properties: [],
builderHint: {
message:
searchHint:
'Use with n8n-nodes-base.formTrigger to build a full form experience. Form node creates additional pages/steps after the trigger',
relatedNodes: [
{ nodeType: 'n8n-nodes-base.formTrigger', relationHint: 'Creates additional form pages' },
@ -202,7 +202,7 @@ const mockRespondToWebhookNode: INodeTypeDescription = {
outputs: ['main'],
properties: [],
builderHint: {
message:
searchHint:
'Only works with webhook node (n8n-nodes-base.webhook) with responseMode set to "responseNode"',
relatedNodes: [
{ nodeType: 'n8n-nodes-base.webhook', relationHint: 'Required webhook trigger' },
@ -234,7 +234,7 @@ const mockAgentNode: INodeTypeDescription = {
outputs: ['main'],
properties: [],
builderHint: {
message:
searchHint:
'Use with @n8n/n8n-nodes-langchain.outputParserStructured to get structured JSON output from the agent',
relatedNodes: [
{
@ -257,7 +257,7 @@ const mockAgentNodeWithRelationHints: INodeTypeDescription = {
outputs: ['main'],
properties: [],
builderHint: {
message: 'Always connect memory for conversation context',
searchHint: 'Always connect memory for conversation context',
relatedNodes: [
{
nodeType: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
@ -378,7 +378,7 @@ const mockCodeRunnerNode: INodeTypeDescription = {
outputs: ['main'],
properties: [],
builderHint: {
message: 'Use with n8n-nodes-base.code for executing custom JavaScript',
searchHint: 'Use with n8n-nodes-base.code for executing custom JavaScript',
relatedNodes: [{ nodeType: 'n8n-nodes-base.code', relationHint: 'Execute custom JavaScript' }],
},
};
@ -491,7 +491,7 @@ describe('CodeBuilderSearchTool', () => {
outputs: ['main'],
properties: [],
builderHint: {
message: 'Related to Chain Node B',
searchHint: 'Related to Chain Node B',
relatedNodes: [{ nodeType: 'test.chainB', relationHint: 'Next in chain' }],
},
};
@ -507,7 +507,7 @@ describe('CodeBuilderSearchTool', () => {
outputs: ['main'],
properties: [],
builderHint: {
message: 'Related to Chain Node C',
searchHint: 'Related to Chain Node C',
relatedNodes: [{ nodeType: 'test.chainC', relationHint: 'Next in chain' }],
},
};
@ -551,7 +551,7 @@ describe('CodeBuilderSearchTool', () => {
outputs: ['main'],
properties: [],
builderHint: {
message: 'Related to Node B',
searchHint: 'Related to Node B',
relatedNodes: [{ nodeType: 'test.nodeB', relationHint: 'Related node' }],
},
};
@ -567,7 +567,7 @@ describe('CodeBuilderSearchTool', () => {
outputs: ['main'],
properties: [],
builderHint: {
message: 'Related to Node A',
searchHint: 'Related to Node A',
relatedNodes: [{ nodeType: 'test.nodeA', relationHint: 'Related node' }],
},
};

View File

@ -45,7 +45,7 @@ export interface CodeBuilderNodeSearchResult {
score: number;
inputs: INodeTypeDescription['inputs'];
outputs: INodeTypeDescription['outputs'];
/** General hint message for workflow builders (from builderHint.message) */
/** General hint message for workflow builders (from node-level builderHint.searchHint) */
builderHintMessage?: string;
/** Subnode requirements extracted from builderHint.inputs */
subnodeRequirements?: SubnodeRequirement[];

View File

@ -221,7 +221,7 @@ describe('extractModeDiscriminator', () => {
name: 'Mode A',
value: 'modeA',
description: 'Description of mode A',
builderHint: { message: 'Use mode A when you want to do X' },
builderHint: { propertyHint: 'Use mode A when you want to do X' },
},
{
name: 'Mode B',
@ -241,7 +241,7 @@ describe('extractModeDiscriminator', () => {
const modeA = result!.modes.find((m: ModeInfo) => m.value === 'modeA');
expect(modeA!.description).toBe('Description of mode A');
expect(modeA!.builderHint).toEqual({ message: 'Use mode A when you want to do X' });
expect(modeA!.builderHint).toEqual({ propertyHint: 'Use mode A when you want to do X' });
const modeB = result!.modes.find((m: ModeInfo) => m.value === 'modeB');
expect(modeB!.description).toBe('Description of mode B');

View File

@ -1,4 +0,0 @@
/**
* Re-export node tips from the shared @n8n/workflow-sdk package.
*/
export { structuredOutputParser, webhook } from '@n8n/workflow-sdk/prompts/node-guidance/node-tips';

View File

@ -11,7 +11,6 @@ export type * from './config';
export type * from './utils';
export type * from './categorization';
export type * from './best-practices';
export type * from './node-guidance';
export type * from './session-storage';
export * from './sessions';
export type * from './planning';

View File

@ -1,4 +0,0 @@
/**
* Re-export node guidance types from the shared @n8n/workflow-sdk/prompts package.
*/
export type { NodeGuidance } from '@n8n/workflow-sdk/prompts/node-guidance/node-tips';

View File

@ -647,7 +647,7 @@ describe('resource-operation-extractor', () => {
name: 'Message',
value: 'message',
description: 'Work with email messages',
builderHint: { message: 'Use for reading, sending, or managing emails' },
builderHint: { propertyHint: 'Use for reading, sending, or managing emails' },
},
{
name: 'Draft',
@ -667,7 +667,7 @@ describe('resource-operation-extractor', () => {
name: 'Send',
value: 'send',
description: 'Send an email message',
builderHint: { message: 'Use to send composed emails to recipients' },
builderHint: { propertyHint: 'Use to send composed emails to recipients' },
},
{
name: 'Get All',
@ -690,7 +690,7 @@ describe('resource-operation-extractor', () => {
const messageResource = result?.resources.find((r) => r.value === 'message');
expect(messageResource?.description).toBe('Work with email messages');
expect(messageResource?.builderHint).toEqual({
message: 'Use for reading, sending, or managing emails',
propertyHint: 'Use for reading, sending, or managing emails',
});
const draftResource = result?.resources.find((r) => r.value === 'draft');
@ -701,7 +701,7 @@ describe('resource-operation-extractor', () => {
const sendOp = messageResource?.operations.find((op) => op.value === 'send');
expect(sendOp?.description).toBe('Send an email message');
expect(sendOp?.builderHint).toEqual({
message: 'Use to send composed emails to recipients',
propertyHint: 'Use to send composed emails to recipients',
});
const getAllOp = messageResource?.operations.find((op) => op.value === 'getAll');

View File

@ -44,6 +44,7 @@ const configs = {
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
'@n8n/community-nodes/node-connection-type-literal': 'error',
'@n8n/community-nodes/missing-paired-item': 'error',
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
'@n8n/community-nodes/require-community-node-keyword': 'warn',
'@n8n/community-nodes/require-continue-on-fail': 'error',
@ -82,6 +83,7 @@ const configs = {
'@n8n/community-nodes/cred-class-oauth2-naming': 'error',
'@n8n/community-nodes/node-connection-type-literal': 'error',
'@n8n/community-nodes/missing-paired-item': 'error',
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
'@n8n/community-nodes/node-operation-error-itemindex': 'error',
'@n8n/community-nodes/require-community-node-keyword': 'warn',
'@n8n/community-nodes/require-continue-on-fail': 'error',

View File

@ -10,6 +10,7 @@ import { CredentialTestRequiredRule } from './credential-test-required.js';
import { IconValidationRule } from './icon-validation.js';
import { MissingPairedItemRule } from './missing-paired-item.js';
import { N8nObjectValidationRule } from './n8n-object-validation.js';
import { NoBuilderHintLeakageRule } from './no-builder-hint-leakage.js';
import { NoCredentialReuseRule } from './no-credential-reuse.js';
import { NoDeprecatedWorkflowFunctionsRule } from './no-deprecated-workflow-functions.js';
import { NoForbiddenLifecycleScriptsRule } from './no-forbidden-lifecycle-scripts.js';
@ -60,6 +61,7 @@ export const rules = {
'node-connection-type-literal': NodeConnectionTypeLiteralRule,
'node-operation-error-itemindex': NodeOperationErrorItemIndexRule,
'missing-paired-item': MissingPairedItemRule,
'no-builder-hint-leakage': NoBuilderHintLeakageRule,
'n8n-object-validation': N8nObjectValidationRule,
'require-community-node-keyword': RequireCommunityNodeKeywordRule,
'require-continue-on-fail': RequireContinueOnFailRule,

View File

@ -0,0 +1,84 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { NoBuilderHintLeakageRule } from './no-builder-hint-leakage.js';
const ruleTester = new RuleTester();
ruleTester.run('no-builder-hint-leakage', NoBuilderHintLeakageRule, {
valid: [
{
name: 'builderHint with no forbidden patterns',
code: 'const x = { builderHint: { propertyHint: "use expr() to embed expressions" } };',
},
{
name: 'builderHint using expr() form in example',
code: 'const x = { builderHint: { propertyHint: "e.g. expr(\'{{ $json.field }}\')" } };',
},
{
name: 'connection-type names as keys are allowed (wire-format structural data)',
code: 'const x = { builderHint: { inputs: { ai_languageModel: { required: true } } } };',
},
{
name: 'wire-format ={{ ... }} outside builderHint is fine in default scope',
code: "const inputs = '={{ $parameter.foo }}';",
},
{
name: 'connection-type literal outside builderHint is fine in default scope',
code: "const requires = ['ai_languageModel'];",
},
{
name: 'with scope=all, connection-type as key is still allowed',
code: "const inputs = { ai_languageModel: 'required' };",
options: [{ scope: 'all' }],
},
{
name: 'with scope=all, exact connection-type as value is allowed (structured data)',
code: "const config = { connectionType: 'ai_languageModel' };",
options: [{ scope: 'all' }],
},
{
name: 'with scope=all, connection-type in array of literals is allowed',
code: "const required = ['ai_tool', 'ai_memory'];",
options: [{ scope: 'all' }],
},
],
invalid: [
{
name: 'wire-format ={{ ... }} inside builderHint message',
code: 'const x = { builderHint: { propertyHint: "e.g. ={{ $json.field }}" } };',
errors: [{ messageId: 'wireExpression' }],
},
{
name: 'wire-format ={{ ... }} inside template literal in builderHint',
code: 'const x = { builderHint: { propertyHint: `e.g. ={{ $json.field }}` } };',
errors: [{ messageId: 'wireExpression' }],
},
{
name: 'connection-type literal inside builderHint message',
code: 'const x = { builderHint: { searchHint: "requires ai_languageModel" } };',
errors: [{ messageId: 'connectionTypeLiteral' }],
},
{
name: 'multiple connection-type literals in one string',
code: 'const x = { builderHint: { propertyHint: "needs ai_languageModel and ai_tool" } };',
errors: [{ messageId: 'connectionTypeLiteral' }, { messageId: 'connectionTypeLiteral' }],
},
{
name: 'wire-format inside nested builderHint structure',
code: 'const x = { description: { builderHint: { tip: { message: "={{ $json.x }}" } } } };',
errors: [{ messageId: 'wireExpression' }],
},
{
name: 'with scope=all, wire-format anywhere is flagged',
code: 'const prompt = "use ={{ $json.foo }} pattern";',
options: [{ scope: 'all' }],
errors: [{ messageId: 'wireExpression' }],
},
{
name: 'with scope=all, connection-type literal in any string is flagged',
code: 'const prompt = "Connect via ai_tool to AI Agent";',
options: [{ scope: 'all' }],
errors: [{ messageId: 'connectionTypeLiteral' }],
},
],
});

View File

@ -0,0 +1,112 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import type { TSESTree } from '@typescript-eslint/utils';
import { createRule } from '../utils/index.js';
const WIRE_EXPRESSION_RE = /={{/;
const CONNECTION_NAME_GROUP =
'agent|chain|document|embedding|languageModel|memory|outputParser|retriever|reranker|textSplitter|tool|vectorStore';
const CONNECTION_NAME_RE = new RegExp(`\\bai_(${CONNECTION_NAME_GROUP})\\b`, 'g');
// Strings that are *exactly* a connection name are structured data values,
// not prose, so they're allowed (e.g. `connectionType: 'ai_languageModel'`).
const EXACT_CONNECTION_NAME_RE = new RegExp(`^ai_(${CONNECTION_NAME_GROUP})$`);
type Options = [{ scope?: 'builderHint' | 'all' }];
type MessageIds = 'wireExpression' | 'connectionTypeLiteral';
function isPropertyKey(node: TSESTree.Node): boolean {
const parent = node.parent;
return parent?.type === AST_NODE_TYPES.Property && parent.key === node;
}
function isInsideBuilderHintValue(node: TSESTree.Node): boolean {
let current: TSESTree.Node | undefined = node;
let parent = current.parent;
while (parent) {
if (
parent.type === AST_NODE_TYPES.Property &&
parent.value === current &&
((parent.key.type === AST_NODE_TYPES.Identifier && parent.key.name === 'builderHint') ||
(parent.key.type === AST_NODE_TYPES.Literal && parent.key.value === 'builderHint'))
) {
return true;
}
current = parent;
parent = current.parent;
}
return false;
}
export const NoBuilderHintLeakageRule = createRule<Options, MessageIds>({
name: 'no-builder-hint-leakage',
meta: {
type: 'problem',
docs: {
description:
'Disallow wire-format expression syntax (={{...}}) and NodeConnectionType string literals in builderHint texts and AI-builder prompts. Use expr() and SDK-canonical references instead.',
},
messages: {
wireExpression:
'Wire-format expression syntax leaks into {{ ctx }}. Use the expr() SDK helper instead.',
connectionTypeLiteral:
'NodeConnectionType literal "{{ value }}" leaks wire format into {{ ctx }}. Refer to the SDK helper instead (e.g. languageModel(), tool(), memory()).',
},
schema: [
{
type: 'object',
description:
'Configures where the rule scans for forbidden patterns. Default `builderHint` only checks string values inside builderHint property values; `all` checks every string in the file (used for AI-builder prompts).',
properties: {
scope: {
type: 'string',
enum: ['builderHint', 'all'],
description:
'`builderHint` (default): only flag strings inside builderHint property values. `all`: flag every string literal/template in the file.',
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{}],
create(context, [options]) {
const scope = options.scope ?? 'builderHint';
const ctx = scope === 'all' ? 'AI-builder prompt' : 'builderHint';
function reportFor(node: TSESTree.Node, str: string): void {
if (WIRE_EXPRESSION_RE.test(str)) {
context.report({ node, messageId: 'wireExpression', data: { ctx } });
}
if (EXACT_CONNECTION_NAME_RE.test(str)) return;
CONNECTION_NAME_RE.lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = CONNECTION_NAME_RE.exec(str)) !== null) {
context.report({
node,
messageId: 'connectionTypeLiteral',
data: { value: match[0], ctx },
});
}
}
function shouldCheck(node: TSESTree.Node): boolean {
if (isPropertyKey(node)) return false;
if (scope === 'all') return true;
return isInsideBuilderHintValue(node);
}
return {
Literal(node) {
if (typeof node.value !== 'string') return;
if (!shouldCheck(node)) return;
reportFor(node, node.value);
},
TemplateLiteral(node) {
if (!shouldCheck(node)) return;
for (const quasi of node.quasis) {
reportFor(quasi, quasi.value.cooked ?? quasi.value.raw);
}
},
};
},
});

View File

@ -24,6 +24,7 @@ export default defineConfig(
'import-x/extensions': 'warn',
'n8n-local-rules/no-argument-spread': 'warn', // TODO: mark error
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
'@n8n/community-nodes/credential-documentation-url': ['error', { allowSlugs: true }],

View File

@ -50,7 +50,7 @@ export class GuardrailsV2 implements INodeType {
},
},
},
message:
searchHint:
'Classify operation has two outputs: output 0 (Pass) for items that passed all guardrail checks, output 1 (Fail) for items that failed. Use .output(index).to() to connect from a specific output. @example guardrails.output(0).to(passNode) and guardrails.output(1).to(failNode). Sanitize operation has only one output.',
},
};

View File

@ -10,7 +10,7 @@ export const commonOptions: INodeProperties[] = [
default: SYSTEM_MESSAGE,
description: 'The message that will be sent to the agent before the conversation starts',
builderHint: {
message:
propertyHint:
"Must include: agent's purpose, exact names of connected tools, and response instructions",
},
typeOptions: {

View File

@ -131,8 +131,8 @@ export const nodeProperties: INodeProperties[] = [
rows: 2,
},
builderHint: {
message:
'Use expressions to include dynamic data from previous nodes (e.g., "={{ $json.input }}"). Static text prompts ignore incoming data.',
propertyHint:
"Use expressions to include dynamic data from previous nodes (e.g., expr('{{ $json.input }}')). Static text prompts ignore incoming data.",
},
displayOptions: {
show: {

View File

@ -67,7 +67,7 @@ export class TextClassifier implements INodeType {
inputs: {
ai_languageModel: { required: true },
},
message:
searchHint:
'Each category defined creates a separate output branch. Output 0 corresponds to the first category, output 1 to the second, and so on. Use .output(index).to() to connect from a specific category. @example textClassifier.output(0).to(nodeA) and textClassifier.output(1).to(nodeB)',
},
properties: [

View File

@ -20,7 +20,7 @@ import {
import { searchModels } from './methods/searchModels';
const ANTHROPIC_MODEL_BUILDER_HINT = {
message:
propertyHint:
'Default to claude-sonnet-4-6 (latest Sonnet); use claude-opus-4-7 when the user needs the most capable model. Never use Claude Sonnet 4.5, Claude 3.x, Claude 2, or LEGACY options — those are superseded and are not valid choices. When extended thinking is needed on Opus 4.7+, set Thinking Mode to Adaptive and choose an Effort level. The legacy Manual thinking mode is rejected by Opus 4.7.',
};

View File

@ -35,7 +35,7 @@ const INCLUDE_JSON_WARNING: INodeProperties = {
};
const OPENAI_MODEL_BUILDER_HINT = {
message:
propertyHint:
'Prefer the GPT-5.4 family: the flagship variant (e.g. `gpt-5.4`) for general use, a `-mini` / `-nano` variant when the task explicitly calls for cost-efficiency, or `-pro` only when the user asks for maximum capability. Never use gpt-4o, gpt-4-turbo, gpt-4, gpt-3.5, or earlier — those are superseded by the GPT-5 family and are not valid choices.',
};

View File

@ -120,7 +120,7 @@ export class LmChatAlibabaCloud implements INodeType {
},
default: 'qwen-plus',
builderHint: {
message:
propertyHint:
'Default to the latest Qwen flagship (qwen3.6-max-preview or qwen3.6-plus). Use qwen-plus for cost-efficient builds. Avoid qwen-turbo, Qwen 3.5 and earlier, and older dated snapshots.',
},
},

View File

@ -140,7 +140,7 @@ export class LmChatAwsBedrock implements INodeType {
},
default: '',
builderHint: {
message:
propertyHint:
'Default to the latest Claude Sonnet on Bedrock (anthropic.claude-sonnet-4-6 family). For Claude Sonnet 4+, switch Model Source to Inference Profiles. Avoid claude-sonnet-4-5, claude-3.x, and non-Claude legacy models unless requested.',
},
},
@ -200,7 +200,7 @@ export class LmChatAwsBedrock implements INodeType {
},
default: '',
builderHint: {
message:
propertyHint:
'Default to the latest Claude Sonnet inference profile (anthropic.claude-sonnet-4-6 family). Avoid claude-sonnet-4-5 and claude-3.x profiles unless specifically requested.',
},
},

View File

@ -123,7 +123,7 @@ export class LmChatCohere implements INodeType {
},
default: 'command-a-03-2025',
builderHint: {
message:
propertyHint:
'Default to the latest Cohere Command A model (command-a-03-2025). Avoid command-r and command-light legacy variants.',
},
},

View File

@ -118,7 +118,7 @@ export class LmChatDeepSeek implements INodeType {
},
default: 'deepseek-chat',
builderHint: {
message:
propertyHint:
'Default to the latest DeepSeek (deepseek-chat = V3.2 non-thinking, deepseek-reasoner = V3.2 thinking / R-series reasoning). Avoid older V3 and R1 snapshots.',
},
},

View File

@ -79,7 +79,7 @@ const modelRLC: INodeProperties = {
},
default: 'models/gemini-2.5-flash',
builderHint: {
message:
propertyHint:
'Default to the latest flagship Gemini (models/gemini-3.1-pro-preview). Use models/gemini-3.1-flash-lite for cost-efficient builds. Avoid Gemini 2.x, 1.x, and earlier.',
},
};

View File

@ -92,7 +92,7 @@ export class LmChatGoogleVertex implements INodeType {
'The model which will generate the completion. <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models">Learn more</a>.',
default: 'gemini-2.5-flash',
builderHint: {
message:
propertyHint:
'Default to the latest flagship Gemini on Vertex (gemini-3.1-pro). Use gemini-3.1-flash-lite for cost-efficient builds. Avoid Gemini 2.x, 1.x, and earlier.',
},
},

View File

@ -102,7 +102,7 @@ export class LmChatGroq implements INodeType {
'The model which will generate the completion. <a href="https://console.groq.com/docs/models">Learn more</a>.',
default: 'llama3-8b-8192',
builderHint: {
message:
propertyHint:
'Default to a flagship model on Groq (openai/gpt-oss-120b, llama-3.3-70b-versatile, or moonshotai/kimi-k2-instruct-0905). Avoid the legacy llama3-8b-8192 default and older llama3/llama-2 variants.',
},
},

View File

@ -77,7 +77,7 @@ export class LmChatMinimax implements INodeType {
],
default: 'MiniMax-M2.7',
builderHint: {
message:
propertyHint:
'Default to the latest MiniMax-M2.x flagship (MiniMax-M2.7). Avoid MiniMax-M2 and earlier.',
},
},

View File

@ -113,7 +113,7 @@ export class LmChatMistralCloud implements INodeType {
},
default: 'mistral-small',
builderHint: {
message:
propertyHint:
'Default to the latest flagship Mistral (mistral-large-2512, aka Mistral Large 3). Use mistral-small for cost-efficient builds. Avoid older dated snapshots and Medium/Small 2.x.',
},
},

View File

@ -119,7 +119,7 @@ export class LmChatMoonshot implements INodeType {
},
default: 'kimi-k2.5',
builderHint: {
message:
propertyHint:
'Default to the latest Kimi model (kimi-k2.6). Avoid kimi-k2.5, kimi-k2, kimi-k1, and earlier.',
},
displayOptions: {

View File

@ -183,7 +183,7 @@ export class LmChatOpenRouter implements INodeType {
},
default: 'openai/gpt-4.1-mini',
builderHint: {
message:
propertyHint:
'Default to a current flagship (e.g. openai/gpt-5.4, anthropic/claude-sonnet-4.6, google/gemini-3.1-pro-preview). Avoid openai/gpt-4o, anthropic/claude-3.x, and other pre-2026 models.',
},
},

View File

@ -116,7 +116,7 @@ export class LmChatVercelAiGateway implements INodeType {
},
default: 'openai/gpt-4o',
builderHint: {
message:
propertyHint:
'Default to a current flagship (e.g. openai/gpt-5.4, anthropic/claude-sonnet-4.6, google/gemini-3.1-pro). Avoid the openai/gpt-4o default and other pre-2026 models.',
},
},

View File

@ -118,7 +118,7 @@ export class LmChatXAiGrok implements INodeType {
},
default: 'grok-2-vision-1212',
builderHint: {
message:
propertyHint:
'Default to the latest flagship Grok (grok-4.20-0309-reasoning, or grok-4.20-multi-agent-0309 for agent workloads). Avoid grok-4, grok-2, and grok-1 variants.',
},
},

View File

@ -93,7 +93,7 @@ export class McpTrigger extends Node {
default: 'none',
description: 'The way to authenticate',
builderHint: {
message:
propertyHint:
"Default to 'none'. n8n exposes inbound trigger URLs publicly by design. Only select an authentication method when the user explicitly asks to authenticate inbound traffic.",
},
},

View File

@ -89,7 +89,7 @@ describe('McpTrigger', () => {
expect(authParam?.default).toBe('none');
expect(authParam?.options).toHaveLength(3);
expect(authParam?.builderHint).toEqual({
message: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
propertyHint: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
});
});

View File

@ -99,7 +99,7 @@ export class MemoryBufferWindow implements INodeType {
},
},
builderHint: {
message:
searchHint:
'Reuse with multiple agents in the same workflow by connecting to multiple agent nodes so agents have a shared context.',
},

View File

@ -20,7 +20,7 @@ export const sessionIdOption: INodeProperties = {
],
default: 'fromInput',
builderHint: {
message:
propertyHint:
"Use 'Connected Chat Trigger Node' (fromInput) if there is a Chat Trigger node earlier in the workflow. Otherwise use 'Define below' (customKey).",
},
};

View File

@ -182,7 +182,7 @@ export class OutputParserStructured implements INodeType {
},
],
builderHint: {
message:
searchHint:
'Output data is wrapped in an "output" key, e.g. { "output": { "state": "California", "cities": ["San Francisco"] } }',
inputs: {
ai_languageModel: {

View File

@ -142,7 +142,7 @@ const commonOptionsFields: INodeProperties[] = [
default: 'notSupported',
description: 'If loading messages of a previous session should be enabled',
builderHint: {
message:
propertyHint:
"This ONLY rehydrates the chat widget UI when the user reopens it — it does NOT give the Agent memory. The Agent gets memory from its own memory subnode regardless of this setting. Only set to 'memory' if the user wants the widget to restore visible history on reload; if so, you MUST also attach a memory subnode to this trigger (use the same memory node as the Agent so widget history matches what the Agent remembers). Otherwise leave as 'notSupported'.",
},
},
@ -403,7 +403,7 @@ export class ChatTrigger extends Node {
default: 'none',
description: 'The way to authenticate',
builderHint: {
message:
propertyHint:
"Default to 'none'. n8n exposes inbound trigger URLs publicly by design. Only select an authentication method when the user explicitly asks to authenticate inbound traffic.",
},
},

View File

@ -156,7 +156,7 @@ describe('ChatTrigger Node', () => {
expect(authParam).toMatchObject({
default: 'none',
builderHint: {
message: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
propertyHint: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
},
});
});

View File

@ -43,7 +43,7 @@ export class OpenAi extends VersionedNodeType {
},
},
builderHint: {
message:
searchHint:
'For text generation, reasoning and tools, use AI Agent with OpenAI Chat Model. This OpenAI node is for specialized operations: image generation (DALL-E), audio (Whisper, TTS), and video generation (Sora).',
relatedNodes: [
{

View File

@ -47,7 +47,7 @@ export class OpenAiV2 implements INodeType {
name: 'Text',
value: 'text',
builderHint: {
message:
propertyHint:
'For text generation, reasoning and tools, use AI Agent with OpenAI Chat Model instead of this resource.',
},
},

View File

@ -145,7 +145,7 @@ export const promptTypeOptions: INodeProperties = {
],
default: 'auto',
builderHint: {
message: "Use 'auto' when following a chat trigger, 'define' when custom prompt needed",
propertyHint: "Use 'auto' when following a chat trigger, 'define' when custom prompt needed",
},
};
@ -161,8 +161,8 @@ export const textInput: INodeProperties = {
},
builderHint: {
placeholderSupported: false,
message:
'Use expressions to include dynamic data from previous nodes (e.g., "={{ $json.input }}"). Static text prompts ignore incoming data.',
propertyHint:
"Use expressions to include dynamic data from previous nodes (e.g., expr('{{ $json.input }}')). Static text prompts ignore incoming data.",
},
};

View File

@ -1,7 +1,14 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import { nodeConfig } from '@n8n/eslint-config/node';
import { n8nCommunityNodesPlugin } from '@n8n/eslint-plugin-community-nodes';
export default defineConfig(globalIgnores(['test-fixtures/**', 'scripts/**']), nodeConfig, {
export default defineConfig(
globalIgnores(['test-fixtures/**', 'scripts/**']),
nodeConfig,
{
plugins: {
'@n8n/community-nodes': n8nCommunityNodesPlugin,
},
rules: {
// Allow PascalCase for object literal property names (n8n node names and AST types)
'@typescript-eslint/naming-convention': [
@ -61,5 +68,24 @@ export default defineConfig(globalIgnores(['test-fixtures/**', 'scripts/**']), n
'n8n-local-rules/no-interpolation-in-regular-string': 'off',
// These identifiers are used as object keys for type mappings
'id-denylist': 'off',
// Default scope (`builderHint`) won't fire here because workflow-sdk source has no
// builderHint properties; the prompts override below switches on `scope: 'all'`.
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
},
});
},
{
files: ['src/prompts/**/*.ts'],
rules: {
'@n8n/community-nodes/no-builder-hint-leakage': ['error', { scope: 'all' }],
},
},
{
// Multi-agent parameter guides intentionally document wire-format parameters
// (consumed by the legacy parameter-updater chain in ai-workflow-builder.ee,
// not by the code-builder or instance-ai SDK paths).
files: ['src/prompts/node-guidance/parameter-guides/**/*.ts'],
rules: {
'@n8n/community-nodes/no-builder-hint-leakage': 'off',
},
},
);

View File

@ -26,10 +26,6 @@
"types": "./dist/prompts/node-guidance/parameter-guides/index.d.ts",
"default": "./dist/prompts/node-guidance/parameter-guides/index.js"
},
"./prompts/node-guidance/node-tips": {
"types": "./dist/prompts/node-guidance/node-tips/index.d.ts",
"default": "./dist/prompts/node-guidance/node-tips/index.js"
},
"./prompts/node-guidance/node-recommendations": {
"types": "./dist/prompts/node-guidance/node-recommendations/index.d.ts",
"default": "./dist/prompts/node-guidance/node-recommendations/index.js"
@ -75,9 +71,6 @@
"prompts/node-guidance/parameter-guides": [
"./dist/prompts/node-guidance/parameter-guides/index.d.ts"
],
"prompts/node-guidance/node-tips": [
"./dist/prompts/node-guidance/node-tips/index.d.ts"
],
"prompts/node-guidance/node-recommendations": [
"./dist/prompts/node-guidance/node-recommendations/index.d.ts"
],
@ -93,6 +86,7 @@
"dist/**/*"
],
"devDependencies": {
"@n8n/eslint-plugin-community-nodes": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@types/adm-zip": "^0.5.7",
"@types/estree": "^1.0.8",

View File

@ -19,7 +19,7 @@ import type * as GenerateTypesModule from '../generate-types/generate-types';
// =============================================================================
interface ParameterBuilderHint {
message: string;
propertyHint: string;
placeholderSupported?: boolean;
}
@ -797,7 +797,7 @@ describe('generate-types', () => {
type: 'options',
description: 'Select the interval type',
builderHint: {
message: 'You can add multiple intervals to trigger at different times.',
propertyHint: 'You can add multiple intervals to trigger at different times.',
},
options: [
{ name: 'Seconds', value: 'seconds' },
@ -827,7 +827,7 @@ describe('generate-types', () => {
name: 'interval',
displayName: 'Trigger Interval',
builderHint: {
message: 'You can add multiple intervals to trigger at different times.',
propertyHint: 'You can add multiple intervals to trigger at different times.',
},
values: [
{
@ -1719,7 +1719,7 @@ describe('generate-types', () => {
type: 'fixedCollection',
description: 'Configure when the workflow triggers',
builderHint: {
message:
propertyHint:
'You can add multiple intervals to trigger at different times. Use Custom (Cron) for more specific scheduling patterns.',
},
default: {},
@ -1737,7 +1737,7 @@ describe('generate-types', () => {
type: 'string',
description: 'Custom code to execute',
builderHint: {
message: 'See <a href="https://docs.example.com">documentation</a> for examples',
propertyHint: 'See <a href="https://docs.example.com">documentation</a> for examples',
},
default: '',
};

View File

@ -244,7 +244,7 @@ const AI_TYPE_TO_SUBNODE_FIELD: Record<
// =============================================================================
export interface ParameterBuilderHint {
message: string;
propertyHint: string;
placeholderSupported?: boolean;
}
@ -875,7 +875,7 @@ function generateNestedPropertyJSDoc(
// Builder hint - guidance for AI/workflow builders
if (prop.builderHint) {
const safeBuilderHint = prop.builderHint.message
const safeBuilderHint = prop.builderHint.propertyHint
.replace(/\*\//g, '*\\/')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
@ -1041,7 +1041,7 @@ function generateFixedCollectionType(
groupJsDocLines.push(`${INDENT.repeat(2)}/** ${desc}`);
}
if (group.builderHint) {
const safeBuilderHint = group.builderHint.message
const safeBuilderHint = group.builderHint.propertyHint
.replace(/\*\//g, '*\\/')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
@ -1901,7 +1901,7 @@ export function generatePropertyJSDoc(
// Builder hint - guidance for AI/workflow builders
if (prop.builderHint) {
const safeBuilderHint = prop.builderHint.message
const safeBuilderHint = prop.builderHint.propertyHint
.replace(/\*\//g, '*\\/')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');

View File

@ -16,8 +16,8 @@ Most chatbots run through external platforms like Slack, Telegram, or WhatsApp r
CRITICAL: The user may ask to be able to chat to a workflow as well as trigger it via some other method, for example scheduling information gathering but also being able to chat with the agent - in scenarios like this the two separate workflows MUST be connected through shared memory, vector stores, data storage, or direct connections.
Example pattern:
- Schedule Trigger News Gathering Agent [memory node via ai_memory]
- Chat Trigger Chatbot Agent [SAME memory node via ai_memory]
- Schedule Trigger News Gathering Agent [memory node via memory()]
- Chat Trigger Chatbot Agent [SAME memory node via memory()]
- Result: Both agents share conversation/context history, enabling the chatbot to discuss gathered news
For the chatbot always use the same chat node type as used for response. If Telegram has been requested trigger the chatbot via telegram AND

View File

@ -1,3 +1,2 @@
export * from './parameter-guides';
export * from './node-tips';
export * from './node-recommendations';

View File

@ -1,3 +0,0 @@
export type { NodeGuidance } from './types';
export { webhook } from './webhook';
export { structuredOutputParser } from './structured-output-parser';

View File

@ -1,37 +0,0 @@
import type { NodeGuidance } from './types';
export const structuredOutputParser: NodeGuidance = {
nodeType: '@n8n/n8n-nodes-langchain.outputParserStructured',
usage: `Search for "Structured Output Parser" (@n8n/n8n-nodes-langchain.outputParserStructured) when:
- AI output will be used programmatically (conditions, formatting, database storage, API calls)
- AI needs to extract specific fields (e.g., score, category, priority, action items)
- AI needs to classify/categorize data into defined categories
- Downstream nodes need to access specific fields from AI response (e.g., $json.score, $json.category)
- Output will be displayed in a formatted way (e.g., HTML email with specific sections)
- Data needs validation against a schema before processing
- Always use search_nodes to find the exact node names and versions - NEVER guess versions`,
connections: `When Discovery results include AI Agent or Structured Output Parser:
1. Create the Structured Output Parser node
2. Set AI Agent's hasOutputParser: true in initialParameters
3. Connect: Structured Output Parser AI Agent (ai_outputParser connection)`,
configuration: `WHEN TO SET hasOutputParser: true on AI Agent:
- Discovery found Structured Output Parser node MUST set hasOutputParser: true
- AI output will be used in conditions (IF/Switch nodes checking $json.field)
- AI output will be formatted/displayed (HTML emails, reports with specific sections)
- AI output will be stored in database/data tables with specific fields
- AI is classifying, scoring, or extracting specific data fields`,
recommendation: `For AI-generated structured data, prefer Structured Output Parser nodes over Code nodes.
Why: Purpose-built parsers are more reliable and handle edge cases better than custom code.
For binary file data, use Extract From File node to extract content from files before processing.
Use Code nodes only for:
- Simple string manipulations
- Already structured data (JSON, CSV)
- Custom business logic beyond parsing`,
};

View File

@ -1,10 +0,0 @@
/**
* Structured guidance for node usage across different agents.
*/
export interface NodeGuidance {
nodeType: string;
usage: string;
connections: string;
configuration: string;
recommendation?: string;
}

View File

@ -1,40 +0,0 @@
import type { NodeGuidance } from './types';
export const webhook: NodeGuidance = {
nodeType: 'n8n-nodes-base.webhook',
usage: `Search for "Webhook" (n8n-nodes-base.webhook) when:
- Workflow needs to receive HTTP requests from external services
- API callbacks, webhooks, or HTTP endpoints are needed
- Integration requires real-time event handling from external systems`,
connections: `WEBHOOK RESPONSE MODE RULES - CRITICAL:
Response modes and their requirements:
- "onReceived" (Immediately): Responds instantly when webhook is called. No RespondToWebhook node needed.
- "lastNode" (When Last Node Finishes): Responds with data from last node. No RespondToWebhook node needed.
- "responseNode" (Using Respond to Webhook Node): REQUIRES a RespondToWebhook node connected downstream.
RULE 1: If responseMode='responseNode', you MUST add a RespondToWebhook node
RULE 2: If RespondToWebhook node exists, responseMode MUST be 'responseNode'
Pattern for custom response control:
Webhook (responseMode: responseNode) [Processing] RespondToWebhook`,
configuration: `WEBHOOK RESPONSE MODE CONFIGURATION:
Choose responseMode based on use case:
- "onReceived": Quick acknowledgment, processing happens async
- "lastNode": Return processed data, simple request-response flows
- "responseNode": Full control over response timing, headers, status codes
CRITICAL: When user needs to control response content/timing/headers, set responseMode='responseNode' AND add RespondToWebhook node.
When NOT to use responseNode:
- Simple acknowledgments (use 'onReceived')
- Return last node output directly (use 'lastNode')`,
recommendation: `For webhook workflows requiring custom response handling (status codes, headers, delayed response), use responseMode='responseNode' with a RespondToWebhook node.
For simple webhook acknowledgments or direct data return, use 'onReceived' or 'lastNode' modes respectively.`,
};

View File

@ -3,30 +3,30 @@ export const AI_NODE_SELECTION = `AI node selection guidance:
AI Agent: Use for text analysis, summarization, classification, or any AI reasoning tasks.
OpenAI node: Use only for DALL-E, Whisper, Sora, or embeddings (these are specialized APIs that AI Agent cannot access).
Default chat model: OpenAI Chat Model provides the lowest setup friction for new users.
Tool nodes (ending in "Tool"): Connect to AI Agent via ai_tool for agent-controlled actions.
Tool nodes (ending in "Tool"): Connect to AI Agent via tool() for agent-controlled actions.
Text Classifier vs AI Agent: Text Classifier for simple categorization with fixed categories; AI Agent for complex multi-step classification requiring reasoning.
Memory nodes: Include with chatbot AI Agents to maintain conversation context across messages.
Structured Output Parser: Prefer this over manually extracting/parsing AI output with Set or Code nodes. Define the desired schema and the LLM handles parsing automatically. Use for classification, data extraction, or any workflow where AI output feeds into database storage, API calls, or Switch routing.
Multi-agent systems:
AI Agent Tool (@n8n/n8n-nodes-langchain.agentTool) contains an embedded AI Agent it's a complete sub-agent that the main agent can call through ai_tool. Each AgentTool needs its own Chat Model. Node selection: 1 AI Agent + N AgentTools + (N+1) Chat Models.`;
AI Agent Tool (@n8n/n8n-nodes-langchain.agentTool) contains an embedded AI Agent it's a complete sub-agent that the main agent can call through tool(). Each AgentTool needs its own Chat Model. Node selection: 1 AI Agent + N AgentTools + (N+1) Chat Models.`;
export const AI_TOOL_PATTERNS = `AI Agent tool connection patterns:
When AI Agent needs external capabilities, use TOOL nodes (not regular nodes):
- Research: SerpAPI Tool, Perplexity Tool -> AI Agent [ai_tool]
- Calendar: Google Calendar Tool -> AI Agent [ai_tool]
- Messaging: Slack Tool, Gmail Tool -> AI Agent [ai_tool]
- HTTP calls: HTTP Request Tool -> AI Agent [ai_tool]
- Calculations: Calculator Tool -> AI Agent [ai_tool]
- Sub-agents: AI Agent Tool -> AI Agent [ai_tool] (for multi-agent systems)
- Research: SerpAPI Tool, Perplexity Tool -> AI Agent [tool()]
- Calendar: Google Calendar Tool -> AI Agent [tool()]
- Messaging: Slack Tool, Gmail Tool -> AI Agent [tool()]
- HTTP calls: HTTP Request Tool -> AI Agent [tool()]
- Calculations: Calculator Tool -> AI Agent [tool()]
- Sub-agents: AI Agent Tool -> AI Agent [tool()] (for multi-agent systems)
Tool nodes: AI Agent decides when/if to use them based on reasoning.
Regular nodes: Execute at that workflow step regardless of context.
Vector Store patterns:
- Insert documents: Document Loader -> Vector Store (mode='insert') [ai_document]
- RAG with AI Agent: Vector Store (mode='retrieve-as-tool') -> AI Agent [ai_tool]
- Insert documents: Document Loader -> Vector Store (mode='insert') [documentLoader()]
- RAG with AI Agent: Vector Store (mode='retrieve-as-tool') -> AI Agent [tool()]
The retrieve-as-tool mode makes the Vector Store act as a tool the Agent can call.
Structured Output Parser: Connect to AI Agent when structured JSON output is required.`;

View File

@ -1,8 +1,8 @@
export const CONNECTION_CHANGING_PARAMETERS = `Connection-changing parameters (affect node inputs/outputs):
Common connection-changing parameters:
- Vector Store: mode (insert/retrieve/retrieve-as-tool) changes output type between main, ai_vectorStore, and ai_tool
- AI Agent: hasOutputParser (true/false) enables ai_outputParser input
- Vector Store: mode (insert/retrieve/retrieve-as-tool) changes output type between main, vectorStore(), and tool()
- AI Agent: hasOutputParser (true/false) enables outputParser() input
- Merge: numberInputs (default 2) requires mode="append" OR mode="combine" + combineBy="combineByPosition"
- Switch: mode (expression/rules) affects routing behavior

View File

@ -10,7 +10,7 @@ export const EXPRESSION_REFERENCE = `Available variables inside \`expr('{{ ... }
- \`$json\` — current item's JSON data from the immediate predecessor node
- \`$('NodeName').item.json\` — access any node's output by name
- \`nodeJson(node, 'field.path')\` — SDK helper that creates \`={{ $('NodeName').item.json.field.path }}\`
- \`nodeJson(node, 'field.path')\` — SDK helper equivalent to \`expr('{{ $("NodeName").item.json.field.path }}')\`
- \`$input.first()\` — first item from immediate predecessor
- \`$input.all()\` — all items from immediate predecessor
- \`$input.item\` — current item being processed

View File

@ -144,7 +144,7 @@ const hasResults = ifElse({
parameters: {
conditions: {
options: { caseSensitive: true, typeValidation: 'loose' },
conditions: [{ leftValue: '={{ $json.results }}', operator: { type: 'array', operation: 'notEmpty' } }],
conditions: [{ leftValue: expr('{{ $json.results }}'), operator: { type: 'array', operation: 'notEmpty' } }],
combinator: 'and'
}
}
@ -180,7 +180,7 @@ const checkValid = ifElse({
parameters: {
conditions: {
options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' },
conditions: [{ leftValue: '={{ $json.status }}', operator: { type: 'string', operation: 'equals' }, rightValue: 'active' }],
conditions: [{ leftValue: expr('{{ $json.status }}'), operator: { type: 'string', operation: 'equals' }, rightValue: 'active' }],
combinator: 'and'
}
}
@ -207,8 +207,8 @@ const routeByPriority = switchCase({
parameters: {
rules: {
values: [
{ outputKey: 'urgent', conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' }, conditions: [{ leftValue: '={{ $json.priority }}', operator: { type: 'string', operation: 'equals' }, rightValue: 'urgent' }], combinator: 'and' } },
{ outputKey: 'normal', conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' }, conditions: [{ leftValue: '={{ $json.priority }}', operator: { type: 'string', operation: 'equals' }, rightValue: 'normal' }], combinator: 'and' } },
{ outputKey: 'urgent', conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' }, conditions: [{ leftValue: expr('{{ $json.priority }}'), operator: { type: 'string', operation: 'equals' }, rightValue: 'urgent' }], combinator: 'and' } },
{ outputKey: 'normal', conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'strict' }, conditions: [{ leftValue: expr('{{ $json.priority }}'), operator: { type: 'string', operation: 'equals' }, rightValue: 'normal' }], combinator: 'and' } },
]
}
}

View File

@ -1705,8 +1705,8 @@ export class InstanceAiAdapterService {
}
if (n.builderHint) {
result.builderHint = {};
if (n.builderHint.message) {
result.builderHint.message = n.builderHint.message;
if (n.builderHint.searchHint) {
result.builderHint.message = n.builderHint.searchHint;
}
if (n.builderHint.inputs) {
const inputs: Record<
@ -1815,7 +1815,7 @@ export class InstanceAiAdapterService {
nodeType,
normalizeNodeVersion(result.version ?? options?.version),
);
const builderHint = nodeDesc?.builderHint?.message;
const builderHint = nodeDesc?.builderHint?.searchHint;
return {
content: result.content,
@ -2381,7 +2381,7 @@ export async function resolveDataTableByIdOrName(
}
/**
* Find the `builderHint.message` of the property that references a given
* Find the `builderHint.propertyHint` of the property that references a given
* method name via `@searchListMethod` (RLC list modes) or `@loadOptionsMethod`.
* Returns undefined if no matching property is found.
*
@ -2424,8 +2424,8 @@ function findBuilderHintForMethod(
continue;
}
if (!isProperty(item)) continue; // plain enum value — skip
if (referencesMethod(item) && item.builderHint?.message) {
return item.builderHint.message;
if (referencesMethod(item) && item.builderHint?.propertyHint) {
return item.builderHint.propertyHint;
}
const nested = searchProps(item.options);
if (nested) return nested;

View File

@ -0,0 +1,123 @@
import type { INodeTypeDescription } from 'n8n-workflow';
import { validateNodeDescription } from '../validate-node-description';
const baseDescription = (properties: INodeTypeDescription['properties']): INodeTypeDescription =>
({
displayName: 'Test',
name: 'test',
group: ['transform'],
version: 1,
description: '',
defaults: { name: 'Test' },
inputs: [],
outputs: [],
properties,
}) as unknown as INodeTypeDescription;
describe('validateNodeDescription', () => {
it.each(['operation', 'mode', 'resource'] as const)(
'throws when %s discriminator property has builderHint',
(name) => {
const description = baseDescription([
{
displayName: name,
name,
type: 'options',
default: '',
options: [{ name: 'A', value: 'a' }],
builderHint: { propertyHint: 'unrendered' },
},
]);
expect(() => validateNodeDescription(description)).toThrow(/discriminator.*not rendered/i);
},
);
it('does not throw when discriminator option has builderHint', () => {
const description = baseDescription([
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: 'classify',
options: [
{
name: 'Classify',
value: 'classify',
builderHint: { propertyHint: 'rendered on the option' },
},
],
},
]);
expect(() => validateNodeDescription(description)).not.toThrow();
});
it('does not throw when non-discriminator property has builderHint', () => {
const description = baseDescription([
{
displayName: 'Conditions',
name: 'conditions',
type: 'filter',
default: {},
builderHint: { propertyHint: 'this renders' },
},
]);
expect(() => validateNodeDescription(description)).not.toThrow();
});
it('does not throw when a string property happens to be named operation', () => {
const description = baseDescription([
{
displayName: 'Operation',
name: 'operation',
type: 'string',
default: '',
builderHint: { propertyHint: 'free-form text, not a discriminator' },
},
]);
expect(() => validateNodeDescription(description)).not.toThrow();
});
it('throws when builderHint is on a discriminator nested inside a collection', () => {
const description = baseDescription([
{
displayName: 'Wrapper',
name: 'wrapper',
type: 'collection',
default: {},
options: [
{
displayName: 'Mode',
name: 'mode',
type: 'options',
default: '',
options: [{ name: 'A', value: 'a' }],
builderHint: { propertyHint: 'still unrendered' },
},
],
},
]);
expect(() => validateNodeDescription(description)).toThrow(/discriminator.*not rendered/i);
});
it('reports the offending node name in the error message', () => {
const description = baseDescription([
{
displayName: 'Operation',
name: 'operation',
type: 'options',
default: '',
options: [{ name: 'A', value: 'a' }],
builderHint: { propertyHint: 'oops' },
},
]);
description.name = 'myNode';
expect(() => validateNodeDescription(description)).toThrow(/myNode/);
});
});

View File

@ -32,6 +32,7 @@ import {
CUSTOM_NODES_PACKAGE_NAME,
} from './constants';
import { loadClassInIsolation } from './load-class-in-isolation';
import { validateNodeDescription } from './validate-node-description';
function toJSON(this: ICredentialType) {
return {
@ -227,6 +228,7 @@ export abstract class DirectoryLoader implements NodeLoader {
});
this.getVersionedNodeTypeAll(tempNode).forEach(({ description }) => {
validateNodeDescription(description);
this.types.nodes.push(description);
});

View File

@ -0,0 +1,56 @@
import type {
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
INodeTypeDescription,
} from 'n8n-workflow';
import { UnexpectedError } from 'n8n-workflow';
const DISCRIMINATOR_NAMES = new Set(['operation', 'mode', 'resource']);
const isCollection = (
item: INodePropertyOptions | INodeProperties | INodePropertyCollection,
): item is INodePropertyCollection => 'values' in item;
const isProperty = (
item: INodePropertyOptions | INodeProperties | INodePropertyCollection,
): item is INodeProperties => 'type' in item;
const visit = (
items: Array<INodePropertyOptions | INodeProperties | INodePropertyCollection> | undefined,
nodeName: string,
): void => {
for (const item of items ?? []) {
if (isCollection(item)) {
visit(item.values, nodeName);
continue;
}
if (!isProperty(item)) continue;
if (
DISCRIMINATOR_NAMES.has(item.name) &&
item.type === 'options' &&
item.builderHint !== undefined
) {
throw new UnexpectedError(
`Node "${nodeName}" has a builderHint on the "${item.name}" discriminator property. ` +
'builderHint on discriminator properties (operation/mode/resource) is not rendered to the AI workflow builder. ' +
'Move the hint to the relevant option, or to the node-level builderHint.searchHint.',
);
}
visit(item.options, nodeName);
}
};
/**
* Validate a node description for known structural mistakes that would
* silently break AI workflow builder rendering.
*
* Currently checks: builderHint on `operation` / `mode` / `resource`
* discriminator properties those hints are dropped because the
* extractors only walk discriminator *options*, not the property itself.
*/
export const validateNodeDescription = (description: INodeTypeDescription): void => {
visit(description.properties, description.name);
};

View File

@ -39,6 +39,7 @@ export default defineConfig(
'import-x/no-extraneous-dependencies': 'warn',
'n8n-local-rules/no-argument-spread': 'warn', // TODO: mark error
'@n8n/community-nodes/no-builder-hint-leakage': 'error',
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': true }],
'@typescript-eslint/naming-convention': ['warn'],

View File

@ -44,7 +44,7 @@ export class Code implements INodeType {
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
builderHint: {
message:
searchHint:
'Use Code node as a LAST RESORT — it runs in a sandboxed environment and is slower than native nodes. Code node is ONLY appropriate for complex multi-step algorithms that cannot be expressed in single expressions, or operations requiring complex data structures.',
relatedNodes: [
{

View File

@ -18,7 +18,7 @@ export const DATA_TABLE_RESOURCE_LOCATOR_BASE = {
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
builderHint: { message: "Default to mode: 'list' which is easier for users to set up" },
builderHint: { propertyHint: "Default to mode: 'list' which is easier for users to set up" },
modes: [
{
displayName: 'From List',

View File

@ -32,7 +32,7 @@ describe('FormTrigger', () => {
expect(authParam).toMatchObject({
default: 'none',
builderHint: {
message: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
propertyHint: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
},
});
});

View File

@ -110,7 +110,7 @@ const descriptionV2: INodeTypeDescription = {
],
default: 'none',
builderHint: {
message:
propertyHint:
"Default to 'none'. n8n exposes inbound trigger URLs publicly by design. Only select an authentication method when the user explicitly asks to authenticate inbound traffic.",
},
},

View File

@ -112,7 +112,7 @@ export class GmailTrigger implements INodeType {
description:
'Whether to return a simplified version of the response instead of the raw data',
builderHint: {
message:
propertyHint:
'Set to false when the email body is needed for AI analysis, summarization, or content processing. When true, only returns snippet (preview text). When false, returns full email with {id, threadId, labelIds, headers, html, text, textAsHtml, subject, date, to, from, messageId, replyTo}.',
},
},
@ -172,7 +172,7 @@ export class GmailTrigger implements INodeType {
default: '',
placeholder: 'has:attachment',
builderHint: {
message:
propertyHint:
'Always set a search query to filter emails. Uses Gmail search syntax, e.g. "from:example@gmail.com", "subject:invoice", "has:attachment", "label:important", "newer_than:1d". Combine with spaces for AND: "from:shop@example.com subject:delivery". Without this filter, ALL incoming emails will trigger the workflow.',
},
hint: 'Use the same format as in the Gmail search box. <a href="https://support.google.com/mail/answer/7190?hl=en">More info</a>.',

View File

@ -15,7 +15,7 @@ export class GoogleSheets extends VersionedNodeType {
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Read, update and write data to Google Sheets',
builderHint: {
message:
searchHint:
'For workflow data storage, DataTable with upsert avoids duplicates. Use Google Sheets when spreadsheet collaboration is specifically needed.',
relatedNodes: [
{

View File

@ -81,7 +81,7 @@ export const descriptions: INodeProperties[] = [
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
builderHint: { message: "Default to mode: 'list' which is easier for users to set up" },
builderHint: { propertyHint: "Default to mode: 'list' which is easier for users to set up" },
modes: [
{
displayName: 'From List',
@ -139,7 +139,7 @@ export const descriptions: INodeProperties[] = [
default: { mode: 'list', value: '' },
// default: '', //empty string set to progresivly reveal fields
required: true,
builderHint: { message: "Default to mode: 'list' which is easier for users to set up" },
builderHint: { propertyHint: "Default to mode: 'list' which is easier for users to set up" },
typeOptions: {
loadOptionsDependsOn: ['documentId.value'],
},

View File

@ -62,9 +62,6 @@ const versionDescription: INodeTypeDescription = {
defaults: {
name: 'HighLevel',
},
builderHint: {
message: 'To add notes to contacts, set additionalFields.notes',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],

View File

@ -270,6 +270,9 @@ const createProperties: INodeProperties[] = [
operation: ['create'],
},
},
builderHint: {
propertyHint: 'To add notes to contacts, set additionalFields.notes',
},
options: [
{
displayName: 'Address',

View File

@ -175,7 +175,7 @@ export class Html implements INodeType {
noDataExpression: true,
description: 'HTML template to render',
builderHint: {
message:
propertyHint:
'Use expressions to generate loops, reference data, etc. Does not support handlebars.',
},
displayOptions: {

View File

@ -17,7 +17,7 @@ export class HttpRequest extends VersionedNodeType {
description: 'Makes an HTTP request and returns the response data',
defaultVersion: 4.4,
builderHint: {
message:
searchHint:
'Prefer dedicated integration nodes over HTTP Request — n8n has 400+ dedicated nodes (e.g. Gmail, Slack, Google Sheets, Notion, OpenAI, HubSpot, Jira, etc.) with built-in authentication, pre-configured parameters, better error handling, and easier maintenance. Only use HTTP Request when no dedicated node exists for the service, the user explicitly requests it, accessing a custom/internal API, or the dedicated node does not support the specific operation needed.',
},
};

View File

@ -207,7 +207,7 @@ export const mainProperties: INodeProperties[] = [
name: 'parameters',
displayName: 'Query Parameter',
builderHint: {
message: `NEVER put static authentication values (API keys, tokens, PATs) in queryParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "httpQueryAuth", and add credentials: { httpQueryAuth:
propertyHint: `NEVER put static authentication values (API keys, tokens, PATs) in queryParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "httpQueryAuth", and add credentials: { httpQueryAuth:
newCredential("Name") }. Only use queryParameters for non-auth values. Dynamic values from previous nodes via expr() are acceptable.`,
},
values: [
@ -298,7 +298,7 @@ export const mainProperties: INodeProperties[] = [
name: 'parameters',
displayName: 'Header',
builderHint: {
message: `NEVER put static authentication values (API keys, tokens, PATs) in headerParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "httpHeaderAuth", and add credentials: { httpHeaderAuth:
propertyHint: `NEVER put static authentication values (API keys, tokens, PATs) in headerParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "httpHeaderAuth", and add credentials: { httpHeaderAuth:
newCredential("Name") }. Only use headerParameters for non-auth headers like Content-Type or Accept. Dynamic values from previous nodes via expr() are acceptable.`,
},
values: [
@ -403,7 +403,7 @@ export const mainProperties: INodeProperties[] = [
name: 'bodyParameters',
type: 'fixedCollection',
builderHint: {
message: `NEVER put static authentication values (API keys, tokens, PATs) in bodyParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "customAuth", and add credentials: { customAuth:
propertyHint: `NEVER put static authentication values (API keys, tokens, PATs) in bodyParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "customAuth", and add credentials: { customAuth:
newCredential("Name") }. Only use bodyParameters for non-auth values. Dynamic values from previous nodes via expr() are acceptable.`,
},
displayOptions: {
@ -457,7 +457,7 @@ export const mainProperties: INodeProperties[] = [
name: 'jsonBody',
type: 'json',
builderHint: {
message: `NEVER put static authentication values (API keys, tokens, PATs) in bodyParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "customAuth", and add credentials: { customAuth:
propertyHint: `NEVER put static authentication values (API keys, tokens, PATs) in bodyParameters. It's insecure to store credentials directly in parameters. Instead set authentication to "genericCredentialType", genericAuthType to "customAuth", and add credentials: { customAuth:
newCredential("Name") }. Only use bodyParameters for non-auth values. Dynamic values from previous nodes via expr() are acceptable.`,
},
displayOptions: {

View File

@ -29,13 +29,6 @@ export class IfV2 implements INodeType {
outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
outputNames: ['true', 'false'],
parameterPane: 'wide',
builderHint: {
message: `parameters.conditions must always contain these three sibling keys:
- "combinator": "and" or "or", default to "and"
- "conditions": [ {a list of condition objects } ]
- "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 1 }
e.g.: { "conditions": { "combinator": "and", "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [{ "leftValue": "={{ $json.field }}", "rightValue": "value", "operator": { "type": "string", "operation": "equals" } }] } }`,
},
properties: [
{
displayName: 'Conditions',
@ -50,6 +43,13 @@ e.g.: { "conditions": { "combinator": "and", "options": { "caseSensitive": true,
version: '={{ $nodeVersion >= 2.3 ? 3 : $nodeVersion >= 2.2 ? 2 : 1 }}',
},
},
builderHint: {
propertyHint: `Must always contain these three sibling keys:
- combinator: 'and' or 'or', default to 'and'
- conditions: [ a list of condition objects ]
- options: { caseSensitive: true, leftValue: '', typeValidation: 'strict', version: 1 }
e.g.: { combinator: 'and', options: { caseSensitive: true, leftValue: '', typeValidation: 'strict', version: 2 }, conditions: [{ leftValue: expr('{{ $json.field }}'), rightValue: 'value', operator: { type: 'string', operation: 'equals' } }] }`,
},
},
{
...looseTypeValidationProperty,

View File

@ -25,7 +25,7 @@ export class LinkedIn implements INodeType {
name: 'LinkedIn',
},
builderHint: {
message: 'LinkedIn API does not support scraping profiles or leads.',
searchHint: 'LinkedIn API does not support scraping profiles or leads.',
relatedNodes: [
{
nodeType: 'n8n-nodes-base.phantombuster',

View File

@ -24,7 +24,7 @@ export class ManualTrigger implements INodeType {
inputs: [],
outputs: [NodeConnectionTypes.Main],
builderHint: {
message: 'There can only be one manual trigger node per workflow',
searchHint: 'There can only be one manual trigger node per workflow',
},
properties: [
{

View File

@ -21,7 +21,7 @@ export const description: INodeProperties[] = [
value: 'append',
description: 'Output items of each input, one after the other',
builderHint: {
message:
propertyHint:
'Append items from multiple branches into a single list sequentially. Waits for all running branches. Supports any number of inputs. @example input1: [{ x }] [{ y }] input2: [{ z }]. Output: [{ x }, { y }, { z }]. Next node will execute 3 times with each item. Set executeOnce on next node to execute once.',
},
},
@ -30,7 +30,7 @@ export const description: INodeProperties[] = [
value: 'combine',
description: 'Merge matching items together',
builderHint: {
message:
propertyHint:
'Combines items from 2 branches. Waits for both to have input data. @example **combine by position** input1: [{ x }, { y }] input2: [{ z }] output: [{ x, y }, { x: undefined, y: undefined, z }] @example **combine by key** input1: [{ id: 1, x }, { id: 2, y }] input2: [{ id: 1, z }] output: [{ id: 1, x, z }, { id: 2, y }]',
},
},
@ -39,7 +39,7 @@ export const description: INodeProperties[] = [
value: 'combineBySql',
description: 'Write a query to do the merge',
builderHint: {
message:
propertyHint:
'Need to combine more than 2 branches? Use SQL Query for advanced operations. Waits for all inputs. @example Results depend on query - can filter, join, aggregate',
},
},
@ -48,7 +48,7 @@ export const description: INodeProperties[] = [
value: 'chooseBranch',
description: 'Output data from a specific branch, without modifying it',
builderHint: {
message:
propertyHint:
'Do you need to select data from only ONE specific input and discard the others? Use Choose Branch after conditional nodes to pick which path to continue. Waits for all inputs. @example 3 items from Input A + 2 items from Input B, choose Input A → 3 items',
},
},

View File

@ -30,7 +30,7 @@ export class Phantombuster implements INodeType {
name: 'Phantombuster',
},
builderHint: {
message:
searchHint:
'Recommended for scraping LinkedIn profiles and social media for leads, and company data. Use with AI Agent for lead generation workflows.',
},
usableAsTool: true,

View File

@ -90,7 +90,7 @@ export class RespondToWebhook implements INodeType {
name: 'Respond to Webhook',
},
builderHint: {
message:
searchHint:
'Only works with webhook node (n8n-nodes-base.webhook) with responseMode set to "responseNode"',
relatedNodes: [
{

View File

@ -65,7 +65,7 @@ export class ScheduleTrigger implements INodeType {
name: 'interval',
displayName: 'Trigger Interval',
builderHint: {
message:
propertyHint:
'You can add multiple intervals to trigger at different times. Use "Custom (Cron)" for more specific scheduling patterns.',
},
values: [

View File

@ -32,7 +32,7 @@ export class Aggregate implements INodeType {
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
builderHint: {
message:
searchHint:
'Need to combine items from multiple branches? Use merge node. This nodes combines all items from one branch into one item.',
relatedNodes: [
{

View File

@ -49,7 +49,7 @@ export class SplitOut implements INodeType {
requiresDataPath: 'multiple',
hint: 'Use $binary to split out the input item by binary data',
builderHint: {
message:
propertyHint:
'Must be a field name (or comma-separated list of field names) as it appears inside $json. Examples: "issues" when $json is { issues: [...] }; "user.addresses" for nested arrays (dot notation supported — disable via Options > Disable Dot Notation if keys contain literal dots); "fieldA,fieldB" to split multiple arrays. Write the key/path directly — do NOT prefix with "$json." or pass "$json" (that is the whole item, not a field name). Use "$binary" only when splitting binary data. If the upstream item is an array at $json root (no wrapping key), restructure it first (Set/Code) so the array lives under a named key.',
},
},

View File

@ -315,7 +315,7 @@ export class Wait extends Webhook {
name: 'resume',
type: 'options',
builderHint: {
message:
propertyHint:
'For user approval workflows, consider using nodes with operation: "sendAndWait" (e.g., email, Slack) instead of Wait node. If using "webhook", the URL will be generated at runtime and can be referenced with {{ $execution.resumeUrl }}.',
},
options: [

View File

@ -136,7 +136,7 @@ export class Webhook extends Node {
default: '',
placeholder: 'webhook',
builderHint: {
message: 'The webhook path that triggers this workflow',
propertyHint: 'The webhook path that triggers this workflow',
placeholderSupported: false,
},
description:

View File

@ -49,7 +49,7 @@ export const credentialsProperty = (
];
export const inboundTriggerAuthenticationBuilderHint = {
message:
propertyHint:
"Default to 'none'. n8n exposes inbound trigger URLs publicly by design. Only select an authentication method when the user explicitly asks to authenticate inbound traffic.",
};
@ -156,7 +156,8 @@ export const responseModeProperty: INodeProperties = {
default: 'onReceived',
description: 'When and how to respond to the webhook',
builderHint: {
message: "Use 'responseNode' to respond via a 'Respond to Webhook' node later in the workflow",
propertyHint:
"Use 'responseNode' to respond via a 'Respond to Webhook' node later in the workflow",
},
displayOptions: {
show: {
@ -180,7 +181,8 @@ export const responseModePropertyStreaming: INodeProperties = {
default: 'onReceived',
description: 'When and how to respond to the webhook',
builderHint: {
message: "Use 'responseNode' to respond via a 'Respond to Webhook' node later in the workflow",
propertyHint:
"Use 'responseNode' to respond via a 'Respond to Webhook' node later in the workflow",
},
displayOptions: {
hide: {

View File

@ -25,7 +25,7 @@ describe('Test Webhook Node', () => {
expect(authParam).toMatchObject({
default: 'none',
builderHint: {
message: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
propertyHint: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT,
},
});
});

View File

@ -1909,7 +1909,7 @@ export interface INodePropertyCollection {
}
export interface IParameterBuilderHint {
message: string;
propertyHint: string;
placeholderSupported?: boolean;
}
@ -2552,7 +2552,7 @@ export interface IBuilderHint {
/** Declarative output availability — which outputs the node exposes per parameter values */
outputs?: BuilderHintOutputs;
/** General hint message for LLM workflow builders */
message?: string;
searchHint?: string;
/** Related nodes that work together with this node */
relatedNodes?: IRelatedNode[];
}

View File

@ -882,7 +882,7 @@ importers:
version: link:../vitest-config
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
vitest:
specifier: 'catalog:'
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
@ -1056,7 +1056,7 @@ importers:
version: link:../vitest-config
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
vitest:
specifier: 'catalog:'
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
@ -1793,7 +1793,7 @@ importers:
version: 7.0.15
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
vitest:
specifier: 'catalog:'
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
@ -2281,7 +2281,7 @@ importers:
version: 0.9.4
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
fast-glob:
specifier: 'catalog:'
version: 3.2.12
@ -2309,7 +2309,7 @@ importers:
version: link:../vitest-config
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
vitest:
specifier: 'catalog:'
version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(@vitest/browser-playwright@4.0.16)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
@ -2374,7 +2374,7 @@ importers:
version: link:../vitest-config
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
rimraf:
specifier: 'catalog:'
version: 6.0.1
@ -2467,7 +2467,7 @@ importers:
version: 20.19.21
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
nodemon:
specifier: ^2.0.20
version: 2.0.22
@ -2563,6 +2563,9 @@ importers:
specifier: 3.25.67
version: 3.25.67
devDependencies:
'@n8n/eslint-plugin-community-nodes':
specifier: workspace:*
version: link:../eslint-plugin-community-nodes
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
@ -3304,7 +3307,7 @@ importers:
version: 5.2.4(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2))
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
unplugin-icons:
specifier: ^0.19.0
version: 0.19.0(@vue/compiler-sfc@3.5.26)
@ -3744,7 +3747,7 @@ importers:
version: 4.0.16(bufferutil@4.0.9)(playwright@1.58.0)(utf-8-validate@5.0.10)(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vitest@4.1.1)
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
'@vue/tsconfig':
specifier: catalog:frontend
version: 0.7.0(typescript@6.0.2)(vue@3.5.26(typescript@6.0.2))
@ -4111,7 +4114,7 @@ importers:
version: 5.2.4(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))(vue@3.5.26(typescript@6.0.2))
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
browserslist-to-esbuild:
specifier: ^2.1.1
version: 2.1.1(browserslist@4.28.1)
@ -4536,7 +4539,7 @@ importers:
version: link:../../@n8n/vitest-config
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
fast-glob:
specifier: 'catalog:'
version: 3.2.12
@ -4600,7 +4603,7 @@ importers:
version: 20.19.21
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
ts-morph:
specifier: 'catalog:'
version: 27.0.2
@ -4732,7 +4735,7 @@ importers:
version: link:../../@n8n/vitest-config
'@vitest/coverage-v8':
specifier: 'catalog:'
version: 4.1.1(vitest@4.1.1)
version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))
tsx:
specifier: 'catalog:'
version: 4.19.3
@ -32789,7 +32792,7 @@ snapshots:
tinyrainbow: 3.0.3
vitest: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.89.2)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))
'@vitest/coverage-v8@4.1.1(vitest@4.1.1)':
'@vitest/coverage-v8@4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.21)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.1.1