From 72eca2f3985005b4841271de9c8cdf96c5e55a8c Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Fri, 8 May 2026 15:32:50 +0200 Subject: [PATCH] refactor: Rename node-level builderHint.message to searchHint and propertyHint (#30062) Co-authored-by: Claude Opus 4.7 (1M context) --- .../code-builder-node-search-engine.ts | 12 +- .../code-builder-node-search-engine.test.ts | 4 +- .../tools/code-builder-search.tool.ts | 10 +- .../test/code-builder-search.tool.test.ts | 20 +-- .../src/code-builder/types.ts | 2 +- .../utils/test/discriminator-utils.test.ts | 4 +- .../src/prompts/shared/node-guidance/index.ts | 4 - .../ai-workflow-builder.ee/src/types/index.ts | 1 - .../src/types/node-guidance.ts | 4 - .../test/resource-operation-extractor.test.ts | 8 +- .../src/plugin.ts | 2 + .../src/rules/index.ts | 2 + .../src/rules/no-builder-hint-leakage.test.ts | 84 ++++++++++ .../src/rules/no-builder-hint-leakage.ts | 112 +++++++++++++ .../@n8n/nodes-langchain/eslint.config.mjs | 1 + .../nodes/Guardrails/v2/GuardrailsV2.node.ts | 2 +- .../agents/Agent/agents/ToolsAgent/options.ts | 2 +- .../nodes/chains/ChainLLM/methods/config.ts | 4 +- .../TextClassifier/TextClassifier.node.ts | 2 +- .../LMChatAnthropic/LmChatAnthropic.node.ts | 2 +- .../llms/LMChatOpenAi/LmChatOpenAi.node.ts | 2 +- .../LmChatAlibabaCloud.node.ts | 2 +- .../LmChatAwsBedrock/LmChatAwsBedrock.node.ts | 4 +- .../llms/LmChatCohere/LmChatCohere.node.ts | 2 +- .../LmChatDeepSeek/LmChatDeepSeek.node.ts | 2 +- .../LmChatGoogleGemini.node.ts | 2 +- .../LmChatGoogleVertex.node.ts | 2 +- .../nodes/llms/LmChatGroq/LmChatGroq.node.ts | 2 +- .../llms/LmChatMinimax/LmChatMinimax.node.ts | 2 +- .../LmChatMistralCloud.node.ts | 2 +- .../LmChatMoonshot/LmChatMoonshot.node.ts | 2 +- .../LmChatOpenRouter/LmChatOpenRouter.node.ts | 2 +- .../LmChatVercelAiGateway.node.ts | 2 +- .../llms/LmChatXAiGrok/LmChatXAiGrok.node.ts | 2 +- .../nodes/mcp/McpTrigger/McpTrigger.node.ts | 2 +- .../__tests__/McpTrigger.node.test.ts | 2 +- .../MemoryBufferWindow.node.ts | 2 +- .../nodes/memory/descriptions.ts | 2 +- .../OutputParserStructured.node.ts | 2 +- .../trigger/ChatTrigger/ChatTrigger.node.ts | 4 +- .../__test__/ChatTrigger.node.test.ts | 2 +- .../nodes/vendors/OpenAi/OpenAi.node.ts | 2 +- .../nodes/vendors/OpenAi/v2/OpenAiV2.node.ts | 2 +- .../nodes-langchain/utils/descriptions.ts | 6 +- packages/@n8n/workflow-sdk/eslint.config.mjs | 148 ++++++++++-------- packages/@n8n/workflow-sdk/package.json | 8 +- .../src/generate-types/generate-types.test.ts | 10 +- .../src/generate-types/generate-types.ts | 8 +- .../prompts/best-practices/guides/chatbot.ts | 4 +- .../src/prompts/node-guidance/index.ts | 1 - .../prompts/node-guidance/node-tips/index.ts | 3 - .../node-tips/structured-output-parser.ts | 37 ----- .../prompts/node-guidance/node-tips/types.ts | 10 -- .../node-guidance/node-tips/webhook.ts | 40 ----- .../src/prompts/node-selection/ai-nodes.ts | 20 +-- .../node-selection/connection-parameters.ts | 4 +- .../src/prompts/sdk-reference/expressions.ts | 2 +- .../sdk-reference/workflow-patterns.ts | 8 +- .../instance-ai.adapter.service.ts | 12 +- .../validate-node-description.test.ts | 123 +++++++++++++++ .../core/src/nodes-loader/directory-loader.ts | 2 + .../nodes-loader/validate-node-description.ts | 56 +++++++ packages/nodes-base/eslint.config.mjs | 1 + packages/nodes-base/nodes/Code/Code.node.ts | 2 +- .../nodes/DataTable/common/fields.ts | 2 +- .../Form/test/FormTriggerV2.node.test.ts | 2 +- .../nodes/Form/v2/FormTriggerV2.node.ts | 2 +- .../nodes/Google/Gmail/GmailTrigger.node.ts | 4 +- .../nodes/Google/Sheet/GoogleSheets.node.ts | 2 +- .../Sheet/v2/actions/sheet/Sheet.resource.ts | 4 +- .../nodes/HighLevel/v2/HighLevelV2.node.ts | 3 - .../v2/description/ContactDescription.ts | 3 + packages/nodes-base/nodes/Html/Html.node.ts | 2 +- .../nodes/HttpRequest/HttpRequest.node.ts | 2 +- .../nodes/HttpRequest/V3/Description.ts | 8 +- packages/nodes-base/nodes/If/V2/IfV2.node.ts | 14 +- .../nodes/LinkedIn/LinkedIn.node.ts | 2 +- .../nodes/ManualTrigger/ManualTrigger.node.ts | 2 +- .../nodes/Merge/v3/actions/mode/index.ts | 8 +- .../nodes/Phantombuster/Phantombuster.node.ts | 2 +- .../RespondToWebhook/RespondToWebhook.node.ts | 2 +- .../nodes/Schedule/ScheduleTrigger.node.ts | 2 +- .../Transform/Aggregate/Aggregate.node.ts | 2 +- .../nodes/Transform/SplitOut/SplitOut.node.ts | 2 +- packages/nodes-base/nodes/Wait/Wait.node.ts | 2 +- .../nodes-base/nodes/Webhook/Webhook.node.ts | 2 +- .../nodes-base/nodes/Webhook/description.ts | 8 +- .../nodes/Webhook/test/Webhook.test.ts | 2 +- packages/workflow/src/interfaces.ts | 4 +- pnpm-lock.yaml | 31 ++-- 90 files changed, 634 insertions(+), 322 deletions(-) delete mode 100644 packages/@n8n/ai-workflow-builder.ee/src/prompts/shared/node-guidance/index.ts delete mode 100644 packages/@n8n/ai-workflow-builder.ee/src/types/node-guidance.ts create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.test.ts create mode 100644 packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.ts delete mode 100644 packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/index.ts delete mode 100644 packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/structured-output-parser.ts delete mode 100644 packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/types.ts delete mode 100644 packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/webhook.ts create mode 100644 packages/core/src/nodes-loader/__tests__/validate-node-description.test.ts create mode 100644 packages/core/src/nodes-loader/validate-node-description.ts diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/code-builder-node-search-engine.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/code-builder-node-search-engine.ts index 82ebe411597..bdf91d70d70 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/code-builder-node-search-engine.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/code-builder-node-search-engine.ts @@ -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 }), }; }); diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/test/code-builder-node-search-engine.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/test/code-builder-node-search-engine.test.ts index 27e39bc4e6f..c9cbed37ea6 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/test/code-builder-node-search-engine.test.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/engines/test/code-builder-node-search-engine.test.ts @@ -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]); diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/code-builder-search.tool.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/code-builder-search.tool.ts index 5ea0bb0a522..7b365f32daf 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/code-builder-search.tool.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/code-builder-search.tool.ts @@ -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}`); } } diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/test/code-builder-search.tool.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/test/code-builder-search.tool.test.ts index 96153a9c872..9cabcd72f21 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/test/code-builder-search.tool.test.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/tools/test/code-builder-search.tool.test.ts @@ -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' }], }, }; diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/types.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/types.ts index ffb67e42c37..3e055a5d01c 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/types.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/types.ts @@ -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[]; diff --git a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/utils/test/discriminator-utils.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/utils/test/discriminator-utils.test.ts index b8675a56add..42c40171d08 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/code-builder/utils/test/discriminator-utils.test.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/code-builder/utils/test/discriminator-utils.test.ts @@ -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'); diff --git a/packages/@n8n/ai-workflow-builder.ee/src/prompts/shared/node-guidance/index.ts b/packages/@n8n/ai-workflow-builder.ee/src/prompts/shared/node-guidance/index.ts deleted file mode 100644 index 4562746e281..00000000000 --- a/packages/@n8n/ai-workflow-builder.ee/src/prompts/shared/node-guidance/index.ts +++ /dev/null @@ -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'; diff --git a/packages/@n8n/ai-workflow-builder.ee/src/types/index.ts b/packages/@n8n/ai-workflow-builder.ee/src/types/index.ts index eafb6e6d776..aae5c0d3023 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/types/index.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/types/index.ts @@ -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'; diff --git a/packages/@n8n/ai-workflow-builder.ee/src/types/node-guidance.ts b/packages/@n8n/ai-workflow-builder.ee/src/types/node-guidance.ts deleted file mode 100644 index cfadb417ae0..00000000000 --- a/packages/@n8n/ai-workflow-builder.ee/src/types/node-guidance.ts +++ /dev/null @@ -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'; diff --git a/packages/@n8n/ai-workflow-builder.ee/src/utils/test/resource-operation-extractor.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/utils/test/resource-operation-extractor.test.ts index 485430a3748..16f2a6ae6fb 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/utils/test/resource-operation-extractor.test.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/utils/test/resource-operation-extractor.test.ts @@ -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'); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts index 0f732b193a6..f98e7e2264c 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/plugin.ts @@ -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', diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts index 20e19eb47c4..bd265abbcdc 100644 --- a/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/index.ts @@ -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, diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.test.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.test.ts new file mode 100644 index 00000000000..c3190d21b68 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.test.ts @@ -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' }], + }, + ], +}); diff --git a/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.ts b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.ts new file mode 100644 index 00000000000..b4ba9ebfac7 --- /dev/null +++ b/packages/@n8n/eslint-plugin-community-nodes/src/rules/no-builder-hint-leakage.ts @@ -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({ + 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); + } + }, + }; + }, +}); diff --git a/packages/@n8n/nodes-langchain/eslint.config.mjs b/packages/@n8n/nodes-langchain/eslint.config.mjs index 305f70c7bac..9c311d02ec6 100644 --- a/packages/@n8n/nodes-langchain/eslint.config.mjs +++ b/packages/@n8n/nodes-langchain/eslint.config.mjs @@ -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 }], diff --git a/packages/@n8n/nodes-langchain/nodes/Guardrails/v2/GuardrailsV2.node.ts b/packages/@n8n/nodes-langchain/nodes/Guardrails/v2/GuardrailsV2.node.ts index 614c88fe616..be47d39139e 100644 --- a/packages/@n8n/nodes-langchain/nodes/Guardrails/v2/GuardrailsV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/Guardrails/v2/GuardrailsV2.node.ts @@ -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.', }, }; diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/options.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/options.ts index 1c9117ba45d..b5120262929 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/options.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/agents/ToolsAgent/options.ts @@ -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: { diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/config.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/config.ts index f483f6c610a..b87c029c118 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/config.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/config.ts @@ -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: { diff --git a/packages/@n8n/nodes-langchain/nodes/chains/TextClassifier/TextClassifier.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/TextClassifier/TextClassifier.node.ts index f473eb09d95..bdea5c9c5e5 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/TextClassifier/TextClassifier.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/TextClassifier/TextClassifier.node.ts @@ -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: [ diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts index cc5f8242e99..144c3abc384 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts @@ -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.', }; diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts index 0e58c9bf198..6590e576844 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatOpenAi/LmChatOpenAi.node.ts @@ -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.', }; diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.ts index f707a848d82..6c4df56241b 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts index d9984901f53..337f47bdff8 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatCohere/LmChatCohere.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatCohere/LmChatCohere.node.ts index b6cb61437d6..aa5e20a7a90 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatCohere/LmChatCohere.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatCohere/LmChatCohere.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatDeepSeek/LmChatDeepSeek.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatDeepSeek/LmChatDeepSeek.node.ts index 9a503d03db4..21f2e8af661 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatDeepSeek/LmChatDeepSeek.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatDeepSeek/LmChatDeepSeek.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts index a605e50207c..49221c7b9a7 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleGemini/LmChatGoogleGemini.node.ts @@ -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.', }, }; diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts index 59b9c91e410..b414e33f739 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGoogleVertex/LmChatGoogleVertex.node.ts @@ -92,7 +92,7 @@ export class LmChatGoogleVertex implements INodeType { 'The model which will generate the completion. Learn more.', 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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts index 8db7925bc2b..6c94b229ffd 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatGroq/LmChatGroq.node.ts @@ -102,7 +102,7 @@ export class LmChatGroq implements INodeType { 'The model which will generate the completion. Learn more.', 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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMinimax/LmChatMinimax.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMinimax/LmChatMinimax.node.ts index ae15bbed48e..21fa5e84041 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMinimax/LmChatMinimax.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMinimax/LmChatMinimax.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts index b406ab06630..d91648c3b4e 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMistralCloud/LmChatMistralCloud.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMoonshot/LmChatMoonshot.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMoonshot/LmChatMoonshot.node.ts index 4badf693954..c1fddfffe74 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatMoonshot/LmChatMoonshot.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatMoonshot/LmChatMoonshot.node.ts @@ -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: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts index 39c88e19fad..e12430c6e1f 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatVercelAiGateway/LmChatVercelAiGateway.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatVercelAiGateway/LmChatVercelAiGateway.node.ts index 0ced269fe21..4d10b9dd0bd 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatVercelAiGateway/LmChatVercelAiGateway.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatVercelAiGateway/LmChatVercelAiGateway.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatXAiGrok/LmChatXAiGrok.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatXAiGrok/LmChatXAiGrok.node.ts index e72110645fa..ce16daa7faf 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatXAiGrok/LmChatXAiGrok.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatXAiGrok/LmChatXAiGrok.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpTrigger.node.ts index fc590bb5b1f..1612419d860 100644 --- a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/McpTrigger.node.ts @@ -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.", }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__tests__/McpTrigger.node.test.ts b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__tests__/McpTrigger.node.test.ts index 7556c2fc9d6..7e58f7ff785 100644 --- a/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__tests__/McpTrigger.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/mcp/McpTrigger/__tests__/McpTrigger.node.test.ts @@ -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, }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts index 9fd2977d041..06c4c32e320 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.ts @@ -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.', }, diff --git a/packages/@n8n/nodes-langchain/nodes/memory/descriptions.ts b/packages/@n8n/nodes-langchain/nodes/memory/descriptions.ts index 9d4cd7caf5a..893d012fd6f 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/descriptions.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/descriptions.ts @@ -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).", }, }; diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts index 8c58bf54c7d..4a45624de1f 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts @@ -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: { diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts index d24916d7a7c..09e83dfa80b 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts @@ -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.", }, }, diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/__test__/ChatTrigger.node.test.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/__test__/ChatTrigger.node.test.ts index c50c2ad5f75..3e8bdc67c25 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/__test__/ChatTrigger.node.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/__test__/ChatTrigger.node.test.ts @@ -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, }, }); }); diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts index 3dfcce687dd..e3b60d0ad90 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts @@ -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: [ { diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts index d312244e0dd..f148cf964ef 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts @@ -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.', }, }, diff --git a/packages/@n8n/nodes-langchain/utils/descriptions.ts b/packages/@n8n/nodes-langchain/utils/descriptions.ts index bd58bb17fec..a6bebb86669 100644 --- a/packages/@n8n/nodes-langchain/utils/descriptions.ts +++ b/packages/@n8n/nodes-langchain/utils/descriptions.ts @@ -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.", }, }; diff --git a/packages/@n8n/workflow-sdk/eslint.config.mjs b/packages/@n8n/workflow-sdk/eslint.config.mjs index bdccb6da199..116324533b1 100644 --- a/packages/@n8n/workflow-sdk/eslint.config.mjs +++ b/packages/@n8n/workflow-sdk/eslint.config.mjs @@ -1,65 +1,91 @@ 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, { - rules: { - // Allow PascalCase for object literal property names (n8n node names and AST types) - '@typescript-eslint/naming-convention': [ - 'error', - // Default: require camelCase for most things - { - selector: 'default', - format: ['camelCase'], - leadingUnderscore: 'allow', - }, - // Variables can be camelCase or UPPER_CASE (constants) or PascalCase (classes) - { - selector: 'variable', - format: ['camelCase', 'UPPER_CASE', 'PascalCase'], - leadingUnderscore: 'allow', - }, - // Parameters can be camelCase or PascalCase (for class constructors) - { - selector: 'parameter', - format: ['camelCase', 'PascalCase'], - leadingUnderscore: 'allow', - }, - // Imports can be PascalCase (modules, classes) or camelCase - { - selector: 'import', - format: ['camelCase', 'PascalCase'], - }, - // Types, classes, interfaces, enums should be PascalCase - { - selector: 'typeLike', - format: ['PascalCase'], - }, - // Type properties can be any format (API responses, schema definitions) - { - selector: 'typeProperty', - format: null, - }, - // Object literal properties can be any format (node names, AST types, API responses) - { - selector: 'objectLiteralProperty', - format: null, - }, - // Class properties can be camelCase or UPPER_CASE, with leading underscores for private/internal - { - selector: 'classProperty', - format: ['camelCase', 'UPPER_CASE'], - leadingUnderscore: 'allowSingleOrDouble', - }, - // Enum members should be UPPER_CASE or PascalCase - { - selector: 'enumMember', - format: ['UPPER_CASE', 'PascalCase'], - }, - ], - // Disable this rule - it conflicts with legitimate use of literal ${} in strings - // (e.g., testing code that contains template literals with ${$json.x}) - 'n8n-local-rules/no-interpolation-in-regular-string': 'off', - // These identifiers are used as object keys for type mappings - 'id-denylist': 'off', +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': [ + 'error', + // Default: require camelCase for most things + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + // Variables can be camelCase or UPPER_CASE (constants) or PascalCase (classes) + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow', + }, + // Parameters can be camelCase or PascalCase (for class constructors) + { + selector: 'parameter', + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allow', + }, + // Imports can be PascalCase (modules, classes) or camelCase + { + selector: 'import', + format: ['camelCase', 'PascalCase'], + }, + // Types, classes, interfaces, enums should be PascalCase + { + selector: 'typeLike', + format: ['PascalCase'], + }, + // Type properties can be any format (API responses, schema definitions) + { + selector: 'typeProperty', + format: null, + }, + // Object literal properties can be any format (node names, AST types, API responses) + { + selector: 'objectLiteralProperty', + format: null, + }, + // Class properties can be camelCase or UPPER_CASE, with leading underscores for private/internal + { + selector: 'classProperty', + format: ['camelCase', 'UPPER_CASE'], + leadingUnderscore: 'allowSingleOrDouble', + }, + // Enum members should be UPPER_CASE or PascalCase + { + selector: 'enumMember', + format: ['UPPER_CASE', 'PascalCase'], + }, + ], + // Disable this rule - it conflicts with legitimate use of literal ${} in strings + // (e.g., testing code that contains template literals with ${$json.x}) + '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', + }, + }, +); diff --git a/packages/@n8n/workflow-sdk/package.json b/packages/@n8n/workflow-sdk/package.json index 06804c32813..de137e52b81 100644 --- a/packages/@n8n/workflow-sdk/package.json +++ b/packages/@n8n/workflow-sdk/package.json @@ -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", diff --git a/packages/@n8n/workflow-sdk/src/generate-types/generate-types.test.ts b/packages/@n8n/workflow-sdk/src/generate-types/generate-types.test.ts index 3491e1f3e8b..12f2f054a95 100644 --- a/packages/@n8n/workflow-sdk/src/generate-types/generate-types.test.ts +++ b/packages/@n8n/workflow-sdk/src/generate-types/generate-types.test.ts @@ -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 documentation for examples', + propertyHint: 'See documentation for examples', }, default: '', }; diff --git a/packages/@n8n/workflow-sdk/src/generate-types/generate-types.ts b/packages/@n8n/workflow-sdk/src/generate-types/generate-types.ts index 81a9c34e350..aacf5c4381d 100644 --- a/packages/@n8n/workflow-sdk/src/generate-types/generate-types.ts +++ b/packages/@n8n/workflow-sdk/src/generate-types/generate-types.ts @@ -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, '>'); @@ -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, '>'); @@ -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, '>'); diff --git a/packages/@n8n/workflow-sdk/src/prompts/best-practices/guides/chatbot.ts b/packages/@n8n/workflow-sdk/src/prompts/best-practices/guides/chatbot.ts index 2226f86b5e3..57512757377 100644 --- a/packages/@n8n/workflow-sdk/src/prompts/best-practices/guides/chatbot.ts +++ b/packages/@n8n/workflow-sdk/src/prompts/best-practices/guides/chatbot.ts @@ -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 diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/index.ts b/packages/@n8n/workflow-sdk/src/prompts/node-guidance/index.ts index de4cf2b8e71..357a366a13a 100644 --- a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/index.ts +++ b/packages/@n8n/workflow-sdk/src/prompts/node-guidance/index.ts @@ -1,3 +1,2 @@ export * from './parameter-guides'; -export * from './node-tips'; export * from './node-recommendations'; diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/index.ts b/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/index.ts deleted file mode 100644 index 80e146270e8..00000000000 --- a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type { NodeGuidance } from './types'; -export { webhook } from './webhook'; -export { structuredOutputParser } from './structured-output-parser'; diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/structured-output-parser.ts b/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/structured-output-parser.ts deleted file mode 100644 index c4d341d1d4d..00000000000 --- a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/structured-output-parser.ts +++ /dev/null @@ -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`, -}; diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/types.ts b/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/types.ts deleted file mode 100644 index 0482c4e702a..00000000000 --- a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/types.ts +++ /dev/null @@ -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; -} diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/webhook.ts b/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/webhook.ts deleted file mode 100644 index 8b8021afa80..00000000000 --- a/packages/@n8n/workflow-sdk/src/prompts/node-guidance/node-tips/webhook.ts +++ /dev/null @@ -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.`, -}; diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-selection/ai-nodes.ts b/packages/@n8n/workflow-sdk/src/prompts/node-selection/ai-nodes.ts index e6cb7d78230..a9d2cbb191e 100644 --- a/packages/@n8n/workflow-sdk/src/prompts/node-selection/ai-nodes.ts +++ b/packages/@n8n/workflow-sdk/src/prompts/node-selection/ai-nodes.ts @@ -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.`; diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-selection/connection-parameters.ts b/packages/@n8n/workflow-sdk/src/prompts/node-selection/connection-parameters.ts index a2cacaf2d11..3ad26861953 100644 --- a/packages/@n8n/workflow-sdk/src/prompts/node-selection/connection-parameters.ts +++ b/packages/@n8n/workflow-sdk/src/prompts/node-selection/connection-parameters.ts @@ -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 diff --git a/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/expressions.ts b/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/expressions.ts index eb83eefa69b..ea9ac487d64 100644 --- a/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/expressions.ts +++ b/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/expressions.ts @@ -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 diff --git a/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/workflow-patterns.ts b/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/workflow-patterns.ts index 2bb5625bd94..a55b27f6516 100644 --- a/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/workflow-patterns.ts +++ b/packages/@n8n/workflow-sdk/src/prompts/sdk-reference/workflow-patterns.ts @@ -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' } }, ] } } diff --git a/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts b/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts index cf4cb0a28a0..01afa38898c 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai.adapter.service.ts @@ -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; diff --git a/packages/core/src/nodes-loader/__tests__/validate-node-description.test.ts b/packages/core/src/nodes-loader/__tests__/validate-node-description.test.ts new file mode 100644 index 00000000000..841637a484e --- /dev/null +++ b/packages/core/src/nodes-loader/__tests__/validate-node-description.test.ts @@ -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/); + }); +}); diff --git a/packages/core/src/nodes-loader/directory-loader.ts b/packages/core/src/nodes-loader/directory-loader.ts index ac05e09e2af..166eb2f7f59 100644 --- a/packages/core/src/nodes-loader/directory-loader.ts +++ b/packages/core/src/nodes-loader/directory-loader.ts @@ -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); }); diff --git a/packages/core/src/nodes-loader/validate-node-description.ts b/packages/core/src/nodes-loader/validate-node-description.ts new file mode 100644 index 00000000000..66f7b5ac822 --- /dev/null +++ b/packages/core/src/nodes-loader/validate-node-description.ts @@ -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 | 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); +}; diff --git a/packages/nodes-base/eslint.config.mjs b/packages/nodes-base/eslint.config.mjs index 0dd8473fa9c..55ac1e56373 100644 --- a/packages/nodes-base/eslint.config.mjs +++ b/packages/nodes-base/eslint.config.mjs @@ -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'], diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts index 510d22ad040..fe23a5b6b15 100644 --- a/packages/nodes-base/nodes/Code/Code.node.ts +++ b/packages/nodes-base/nodes/Code/Code.node.ts @@ -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: [ { diff --git a/packages/nodes-base/nodes/DataTable/common/fields.ts b/packages/nodes-base/nodes/DataTable/common/fields.ts index 30ddf2ad4fd..90b4ea4bc9f 100644 --- a/packages/nodes-base/nodes/DataTable/common/fields.ts +++ b/packages/nodes-base/nodes/DataTable/common/fields.ts @@ -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', diff --git a/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts b/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts index bad73da67b2..916d209a899 100644 --- a/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts +++ b/packages/nodes-base/nodes/Form/test/FormTriggerV2.node.test.ts @@ -32,7 +32,7 @@ describe('FormTrigger', () => { expect(authParam).toMatchObject({ default: 'none', builderHint: { - message: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT, + propertyHint: INBOUND_TRIGGER_AUTHENTICATION_BUILDER_HINT, }, }); }); diff --git a/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts b/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts index 8af23700447..47ed6421ddd 100644 --- a/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts +++ b/packages/nodes-base/nodes/Form/v2/FormTriggerV2.node.ts @@ -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.", }, }, diff --git a/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts index 36aad379c32..f347aeca549 100644 --- a/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/GmailTrigger.node.ts @@ -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. More info.', diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts index b9680b7d4ad..690c573ef80 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts @@ -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: [ { diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts index 5fd1acbc4dd..3c7841c27f6 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/Sheet.resource.ts @@ -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'], }, diff --git a/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts b/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts index 8e472818963..b9039b78a65 100644 --- a/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts +++ b/packages/nodes-base/nodes/HighLevel/v2/HighLevelV2.node.ts @@ -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], diff --git a/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts b/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts index 770d2ed26e2..c59b7d16da5 100644 --- a/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts +++ b/packages/nodes-base/nodes/HighLevel/v2/description/ContactDescription.ts @@ -270,6 +270,9 @@ const createProperties: INodeProperties[] = [ operation: ['create'], }, }, + builderHint: { + propertyHint: 'To add notes to contacts, set additionalFields.notes', + }, options: [ { displayName: 'Address', diff --git a/packages/nodes-base/nodes/Html/Html.node.ts b/packages/nodes-base/nodes/Html/Html.node.ts index 3510a2e41e1..91495abd3be 100644 --- a/packages/nodes-base/nodes/Html/Html.node.ts +++ b/packages/nodes-base/nodes/Html/Html.node.ts @@ -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: { diff --git a/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts b/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts index de93d384b49..44515d2a3ac 100644 --- a/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/HttpRequest.node.ts @@ -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.', }, }; diff --git a/packages/nodes-base/nodes/HttpRequest/V3/Description.ts b/packages/nodes-base/nodes/HttpRequest/V3/Description.ts index 06ac8fed1a7..cb3a4ad0b0c 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/Description.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/Description.ts @@ -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: { diff --git a/packages/nodes-base/nodes/If/V2/IfV2.node.ts b/packages/nodes-base/nodes/If/V2/IfV2.node.ts index fc6627627e2..e68366b17f0 100644 --- a/packages/nodes-base/nodes/If/V2/IfV2.node.ts +++ b/packages/nodes-base/nodes/If/V2/IfV2.node.ts @@ -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, diff --git a/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts b/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts index 8d075d13545..ad6ebe3f211 100644 --- a/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts +++ b/packages/nodes-base/nodes/LinkedIn/LinkedIn.node.ts @@ -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', diff --git a/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts b/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts index a2255198cd4..dfd8d038064 100644 --- a/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts +++ b/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts @@ -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: [ { diff --git a/packages/nodes-base/nodes/Merge/v3/actions/mode/index.ts b/packages/nodes-base/nodes/Merge/v3/actions/mode/index.ts index 9b6d917e1ba..78145b801e2 100644 --- a/packages/nodes-base/nodes/Merge/v3/actions/mode/index.ts +++ b/packages/nodes-base/nodes/Merge/v3/actions/mode/index.ts @@ -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', }, }, diff --git a/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts b/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts index 7dded51837e..1403baaf0c0 100644 --- a/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts +++ b/packages/nodes-base/nodes/Phantombuster/Phantombuster.node.ts @@ -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, diff --git a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts index 94d41555022..72c4be7e9d9 100644 --- a/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts +++ b/packages/nodes-base/nodes/RespondToWebhook/RespondToWebhook.node.ts @@ -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: [ { diff --git a/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts b/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts index 334b4f27774..8e906d5f39e 100644 --- a/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts +++ b/packages/nodes-base/nodes/Schedule/ScheduleTrigger.node.ts @@ -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: [ diff --git a/packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts b/packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts index 6ab48e37e55..1e23d58cdb2 100644 --- a/packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts +++ b/packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts @@ -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: [ { diff --git a/packages/nodes-base/nodes/Transform/SplitOut/SplitOut.node.ts b/packages/nodes-base/nodes/Transform/SplitOut/SplitOut.node.ts index a4cbd4a116b..278789bf02b 100644 --- a/packages/nodes-base/nodes/Transform/SplitOut/SplitOut.node.ts +++ b/packages/nodes-base/nodes/Transform/SplitOut/SplitOut.node.ts @@ -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.', }, }, diff --git a/packages/nodes-base/nodes/Wait/Wait.node.ts b/packages/nodes-base/nodes/Wait/Wait.node.ts index f54d3b256ca..34082cbd1cb 100644 --- a/packages/nodes-base/nodes/Wait/Wait.node.ts +++ b/packages/nodes-base/nodes/Wait/Wait.node.ts @@ -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: [ diff --git a/packages/nodes-base/nodes/Webhook/Webhook.node.ts b/packages/nodes-base/nodes/Webhook/Webhook.node.ts index a72b7fca338..5ea20d9f5dc 100644 --- a/packages/nodes-base/nodes/Webhook/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook/Webhook.node.ts @@ -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: diff --git a/packages/nodes-base/nodes/Webhook/description.ts b/packages/nodes-base/nodes/Webhook/description.ts index cb98d1c8a7e..91ccb629927 100644 --- a/packages/nodes-base/nodes/Webhook/description.ts +++ b/packages/nodes-base/nodes/Webhook/description.ts @@ -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: { diff --git a/packages/nodes-base/nodes/Webhook/test/Webhook.test.ts b/packages/nodes-base/nodes/Webhook/test/Webhook.test.ts index 2fc603955c8..2875321225e 100644 --- a/packages/nodes-base/nodes/Webhook/test/Webhook.test.ts +++ b/packages/nodes-base/nodes/Webhook/test/Webhook.test.ts @@ -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, }, }); }); diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index abb7c5095bb..d67eda7c55e 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -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[]; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 540e1e58803..3c488d3d041 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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