diff --git a/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap b/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap
index c105c6114da..5ed3e763e51 100644
--- a/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap
+++ b/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/__snapshots__/createVectorStoreNode.test.ts.snap
@@ -3,6 +3,154 @@
exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] = `
{
"builderHint": {
+ "extraTypeDefContent": [
+ {
+ "content": "Sits on the main flow — pipe the documents you want to embed into this node. Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\` and \`documentLoader\`. If the goal is letting an LLM query the store, use \`mode: 'retrieve-as-tool'\` instead.
+
+
+// Substitute the type literal and provider-specific parameters (e.g. pineconeIndex,
+// qdrantCollection, supabaseTableName) — see the rest of this file for the exact shape.
+const store = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: {
+ mode: 'insert',
+ // ...provider-specific parameters
+ },
+ subnodes: { embedding: embeddingsOpenAi, documentLoader: defaultDataLoader }
+ }
+});
+
+",
+ "displayOptions": {
+ "show": {
+ "mode": [
+ "insert",
+ ],
+ },
+ },
+ },
+ {
+ "content": "Canonical RAG mode — declare with the \`tool({...})\` factory (NOT \`vectorStore\`) and plug into an AI Agent's \`subnodes.tools\`. Required subnodes: \`embedding\`. Set \`toolDescription\` so the agent knows when to call it.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file
+// for the exact shape (e.g. pineconeIndex, qdrantCollection, supabaseTableName).
+const knowledgeBase = tool({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: {
+ mode: 'retrieve-as-tool',
+ toolDescription: 'Search the product knowledge base',
+ // ...provider-specific parameters
+ },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+const agent = node({
+ type: '@n8n/n8n-nodes-langchain.agent',
+ config: {
+ name: 'Support Agent',
+ parameters: { promptType: 'define', text: expr('{{ $json.question }}') },
+ subnodes: { model: openAiModel, tools: [knowledgeBase] }
+ }
+});
+
+",
+ "displayOptions": {
+ "show": {
+ "mode": [
+ "retrieve-as-tool",
+ ],
+ },
+ },
+ },
+ {
+ "content": "One-shot similarity search on the main flow using the \`prompt\` parameter. Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\`. For LLM-driven querying (RAG), use \`mode: 'retrieve-as-tool'\` instead.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file.
+const lookup = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: {
+ mode: 'load',
+ prompt: expr('{{ $json.query }}'),
+ // ...provider-specific parameters
+ },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+",
+ "displayOptions": {
+ "show": {
+ "mode": [
+ "load",
+ ],
+ },
+ },
+ },
+ {
+ "content": "Exposes the store as an \`ai_vectorStore\` subnode for another node (e.g. \`toolVectorStore\`). Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\`. For RAG with an AI Agent directly, prefer \`mode: 'retrieve-as-tool'\`.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file.
+const store = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: { mode: 'retrieve' /* + provider-specific parameters */ },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+const retrieverTool = tool({
+ type: '@n8n/n8n-nodes-langchain.toolVectorStore',
+ config: {
+ name: 'KB Retriever',
+ parameters: { description: 'Search the product knowledge base' },
+ subnodes: { vectorStore: store, model: openAiModel }
+ }
+});
+
+",
+ "displayOptions": {
+ "show": {
+ "mode": [
+ "retrieve",
+ ],
+ },
+ },
+ },
+ {
+ "content": "Updates a single document by \`id\`. Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\`. Only available on stores whose \`operationModes\` enables it — most providers omit this mode.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file.
+const store = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: { mode: 'update', id: expr('{{ $json.docId }}') },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+",
+ "displayOptions": {
+ "show": {
+ "mode": [
+ "update",
+ ],
+ },
+ },
+ },
+ ],
"inputs": {
"ai_document": {
"displayOptions": {
@@ -66,6 +214,7 @@ exports[`createVectorStoreNode retrieve mode supplies vector store as data 1`] =
},
},
},
+ "searchHint": "Pick mode by where data flows: \`insert\` upserts documents into the store on the main flow; \`load\` runs a one-shot similarity search on the main flow; \`retrieve-as-tool\` is the canonical RAG mode — plug into an AI Agent's \`subnodes.tools\`; \`retrieve\` exposes the store as a subnode for another node's \`subnodes.vectorStore\`; \`update\` updates a single document by ID.",
},
"codex": {
"categories": [
diff --git a/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/constants.ts b/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/constants.ts
index 30aceb2bc0d..e02d5297075 100644
--- a/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/constants.ts
+++ b/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/constants.ts
@@ -10,6 +10,10 @@ export const DEFAULT_OPERATION_MODES: NodeOperationMode[] = [
'retrieve-as-tool',
];
+// `mode` is a discriminator field, so per-option `builderHint`s here would never
+// surface in the generated `.d.ts` (discriminator props are dropped from narrowed
+// types). Per-mode guidance lives as node-level `extraTypeDefContent` variations
+// in `createVectorStoreNode.ts`, which the codegen routes per-combo.
export const OPERATION_MODE_DESCRIPTIONS: INodePropertyOptions[] = [
{
name: 'Get Many',
diff --git a/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/createVectorStoreNode.ts b/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/createVectorStoreNode.ts
index 5449dea2497..ad5ca91c95f 100644
--- a/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/createVectorStoreNode.ts
+++ b/packages/@n8n/ai-utilities/src/utils/vector-store/createVectorStoreNode/createVectorStoreNode.ts
@@ -77,7 +77,127 @@ export const createVectorStoreNode = (
},
},
builderHint: {
+ searchHint:
+ "Pick mode by where data flows: `insert` upserts documents into the store on the main flow; `load` runs a one-shot similarity search on the main flow; `retrieve-as-tool` is the canonical RAG mode — plug into an AI Agent's `subnodes.tools`; `retrieve` exposes the store as a subnode for another node's `subnodes.vectorStore`; `update` updates a single document by ID.",
...args.meta.builderHint,
+ extraTypeDefContent: [
+ {
+ displayOptions: { show: { mode: ['insert'] } },
+ content: `Sits on the main flow — pipe the documents you want to embed into this node. Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\` and \`documentLoader\`. If the goal is letting an LLM query the store, use \`mode: 'retrieve-as-tool'\` instead.
+
+
+// Substitute the type literal and provider-specific parameters (e.g. pineconeIndex,
+// qdrantCollection, supabaseTableName) — see the rest of this file for the exact shape.
+const store = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: {
+ mode: 'insert',
+ // ...provider-specific parameters
+ },
+ subnodes: { embedding: embeddingsOpenAi, documentLoader: defaultDataLoader }
+ }
+});
+
+`,
+ },
+ {
+ displayOptions: { show: { mode: ['retrieve-as-tool'] } },
+ content: `Canonical RAG mode — declare with the \`tool({...})\` factory (NOT \`vectorStore\`) and plug into an AI Agent's \`subnodes.tools\`. Required subnodes: \`embedding\`. Set \`toolDescription\` so the agent knows when to call it.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file
+// for the exact shape (e.g. pineconeIndex, qdrantCollection, supabaseTableName).
+const knowledgeBase = tool({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: {
+ mode: 'retrieve-as-tool',
+ toolDescription: 'Search the product knowledge base',
+ // ...provider-specific parameters
+ },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+const agent = node({
+ type: '@n8n/n8n-nodes-langchain.agent',
+ config: {
+ name: 'Support Agent',
+ parameters: { promptType: 'define', text: expr('{{ $json.question }}') },
+ subnodes: { model: openAiModel, tools: [knowledgeBase] }
+ }
+});
+
+`,
+ },
+ {
+ displayOptions: { show: { mode: ['load'] } },
+ content: `One-shot similarity search on the main flow using the \`prompt\` parameter. Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\`. For LLM-driven querying (RAG), use \`mode: 'retrieve-as-tool'\` instead.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file.
+const lookup = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: {
+ mode: 'load',
+ prompt: expr('{{ $json.query }}'),
+ // ...provider-specific parameters
+ },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+`,
+ },
+ {
+ displayOptions: { show: { mode: ['retrieve'] } },
+ content: `Exposes the store as an \`ai_vectorStore\` subnode for another node (e.g. \`toolVectorStore\`). Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\`. For RAG with an AI Agent directly, prefer \`mode: 'retrieve-as-tool'\`.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file.
+const store = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: { mode: 'retrieve' /* + provider-specific parameters */ },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+const retrieverTool = tool({
+ type: '@n8n/n8n-nodes-langchain.toolVectorStore',
+ config: {
+ name: 'KB Retriever',
+ parameters: { description: 'Search the product knowledge base' },
+ subnodes: { vectorStore: store, model: openAiModel }
+ }
+});
+
+`,
+ },
+ {
+ displayOptions: { show: { mode: ['update'] } },
+ content: `Updates a single document by \`id\`. Declare with \`vectorStore({...})\`. Required subnodes: \`embedding\`. Only available on stores whose \`operationModes\` enables it — most providers omit this mode.
+
+
+// Substitute the type literal and provider-specific parameters — see the rest of this file.
+const store = vectorStore({
+ type: '@n8n/n8n-nodes-langchain.vectorStoreXxx',
+ config: {
+ name: 'Knowledge Base',
+ parameters: { mode: 'update', id: expr('{{ $json.docId }}') },
+ subnodes: { embedding: embeddingsOpenAi }
+ }
+});
+
+`,
+ },
+ ],
inputs: {
ai_embedding: { required: true },
ai_document: {
diff --git a/packages/@n8n/instance-ai/scripts/print-prompts.ts b/packages/@n8n/instance-ai/scripts/print-prompts.ts
index 60748627e72..b3e30196929 100644
--- a/packages/@n8n/instance-ai/scripts/print-prompts.ts
+++ b/packages/@n8n/instance-ai/scripts/print-prompts.ts
@@ -75,7 +75,7 @@ function collectAgents(): AgentEntry[] {
researchMode: true,
webhookBaseUrl: 'https://your-instance.example.com',
filesystemAccess: true,
- localGateway: { status: 'connected' },
+ localGateway: { status: 'connected', capabilities: ['filesystem', 'browser'] },
toolSearchEnabled: true,
licenseHints: [''],
timeZone: 'UTC',
@@ -101,10 +101,7 @@ function collectAgents(): AgentEntry[] {
"localGateway disconnected with filesystem + browser capabilities — renders the 'install Computer Use' pitch and 'Browser Automation (Unavailable)' note",
body: getSystemPrompt({
webhookBaseUrl: 'https://your-instance.example.com',
- localGateway: {
- status: 'disconnected',
- capabilities: ['filesystem', 'browser'],
- },
+ localGateway: { status: 'disconnected' },
browserAvailable: false,
}),
},
@@ -115,7 +112,7 @@ function collectAgents(): AgentEntry[] {
body: getSystemPrompt({
webhookBaseUrl: 'https://your-instance.example.com',
filesystemAccess: true,
- localGateway: { status: 'connected' },
+ localGateway: { status: 'connected', capabilities: ['filesystem'] },
browserAvailable: false,
}),
},
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts
index d3fe5f42203..3e62b509002 100644
--- a/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts
+++ b/packages/@n8n/instance-ai/src/tools/nodes/__tests__/node-search-engine.test.ts
@@ -50,6 +50,12 @@ const agentNode = makeNode({
ai_memory: { required: false },
ai_tool: { required: false, displayOptions: { show: { hasTools: [true] } } },
},
+ extraTypeDefContent: [
+ {
+ content:
+ '\n\nconst agent = node({ ... })\n\n',
+ },
+ ],
},
});
@@ -171,6 +177,21 @@ describe('NodeSearchEngine', () => {
expect(agentResult?.builderHintMessage).toBe('Use an AI Agent for autonomous task execution');
});
+ it('should NOT surface builderHint.extraTypeDefContent in search results', () => {
+ const results = engine.searchByName('AI Agent');
+ const agentResult = results.find((r) => r.name === '@n8n/n8n-nodes-langchain.agent');
+ expect(agentResult).toBeDefined();
+ // Result type has no extraTypeDefContent field; assert it never leaks in
+ // via untyped assignment either.
+ expect(agentResult).not.toHaveProperty('extraTypeDefContent');
+ expect(JSON.stringify(agentResult)).not.toContain('');
+ expect(JSON.stringify(agentResult)).not.toContain('basic');
+ // The formatted XML the LLM actually sees must not contain the example.
+ const xml = engine.formatResult(agentResult!);
+ expect(xml).not.toContain('');
+ expect(xml).not.toContain('const agent = node');
+ });
+
it('should include subnode requirements when present', () => {
const results = engine.searchByName('AI Agent');
const agentResult = results.find((r) => r.name === '@n8n/n8n-nodes-langchain.agent');
diff --git a/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.types.ts b/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.types.ts
index 02fec4eccdf..f2b4ef4929e 100644
--- a/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.types.ts
+++ b/packages/@n8n/instance-ai/src/tools/nodes/node-search-engine.types.ts
@@ -67,6 +67,17 @@ export interface SearchableNodeType {
builderHint?: {
message?: string;
inputs?: BuilderHintInputs;
+ /**
+ * Multi-line content variations emitted into generated `.d.ts` only;
+ * intentionally ignored by the search engine to keep results lightweight.
+ */
+ extraTypeDefContent?: Array<{
+ content: string;
+ displayOptions?: {
+ show?: Record;
+ hide?: Record;
+ };
+ }>;
};
}
diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.prompt.ts b/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.prompt.ts
index cc50b52d741..4f987b0c18d 100644
--- a/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.prompt.ts
+++ b/packages/@n8n/instance-ai/src/tools/orchestration/build-workflow-agent.prompt.ts
@@ -6,11 +6,6 @@
* - createSandboxBuilderAgentPrompt(): Sandbox-based builder with real files + tsc
*/
-import {
- AI_TOOL_PATTERNS,
- CONNECTION_CHANGING_PARAMETERS,
- BASELINE_FLOW_CONTROL,
-} from '@n8n/workflow-sdk/prompts/node-selection';
import {
EXPRESSION_REFERENCE,
ADDITIONAL_FUNCTIONS,
@@ -60,219 +55,12 @@ const NODE_CONFIGURATION_SAFETY_RULES = `## Node Configuration Safety Rules
- Use live \`nodes(action="explore-resources")\` for resource locator, list, and model fields when credentials are available.
- If a configuration is unclear after reading the definition, ask for clarification or use placeholders — do not guess.`;
-// The AI Agent subnode example uses `newCredential()` in both modes. In sandbox
-// mode the submit runner preserves unresolved credential slots for
-// `submit-workflow`, so the same outlet works there too.
-function buildBuilderSpecificPatterns(): string {
- const openAiCredExample = "newCredential('OpenAI')";
- return `## Critical Patterns (Common Mistakes)
+// Node-specific configuration examples used to live here. They have moved
+// onto the nodes themselves as `@builderHint` annotations and `...`
+// blocks in the generated `.d.ts` — fetch them on-demand via `nodes(action="type-definition")`.
+const BUILDER_SPECIFIC_PATTERNS = `## Critical Patterns (Common Mistakes)
-**Pay attention to @builderHint annotations in search results and type definitions** — these provide critical guidance on how to correctly configure node parameters. Write them out as notes when reviewing — they prevent common configuration mistakes.
-
-### Self-check: conditional nodes and routing
-
-After writing any workflow with IF, Switch, or Filter nodes, verify:
-1. **Every \`conditions\` object has \`options\`, \`conditions\` array, and \`combinator\`** — missing any of these crashes the node at runtime.
-2. **Switch uses \`rules.values\`** (not \`rules.rules\`) — the wrong key crashes during workflow loading.
-3. **Each branch reaches the correct destination** — trace the data flow from the condition through \`.onTrue()\`/\`.onFalse()\`/\`.onCase()\` to the target node. Verify the routing matches the user's requirements.
-4. **Condition expressions reference the right fields** — check that \`leftValue\` expressions use fields that actually exist in the upstream node's output.
-5. **Merge nodes use the correct mode** — \`append\` to concatenate items from branches, \`combineBySql\` or \`combineByPosition\` only when matching items across inputs. Wrong mode silently drops or duplicates data.
-
-### AI Agent with Subnodes — use factory functions in subnodes config
-\`\`\`javascript
-const chatTrigger = trigger({
- type: '@n8n/n8n-nodes-langchain.chatTrigger',
- version: 1.3,
- config: {
- name: 'Chat Trigger',
- parameters: { public: false },
- output: [{ sessionId: 'chat-session-id', chatInput: 'Hello' }]
- }
-});
-
-const model = languageModel({
- type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
- version: 1.3,
- config: {
- name: 'OpenAI Chat Model',
- parameters: { model: { __rl: true, mode: 'list', value: 'gpt-5.4' } },
- credentials: { openAiApi: ${openAiCredExample} }
- }
-});
-
-const parser = outputParser({
- type: '@n8n/n8n-nodes-langchain.outputParserStructured',
- version: 1.3,
- config: {
- name: 'Output Parser',
- parameters: {
- schemaType: 'fromJson',
- jsonSchemaExample: '{ "score": 75, "tier": "hot" }'
- }
- }
-});
-
-const memoryNode = memory({
- type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
- version: 1.3,
- config: {
- name: 'Conversation Memory',
- parameters: {
- sessionIdType: 'customKey',
- sessionKey: nodeJson(chatTrigger, 'sessionId'),
- contextWindowLength: 10
- }
- }
-});
-
-const agent = node({
- type: '@n8n/n8n-nodes-langchain.agent',
- version: 3.1,
- config: {
- name: 'AI Agent',
- parameters: {
- promptType: 'define',
- text: '={{ $json.prompt }}',
- hasOutputParser: true,
- options: { systemMessage: 'You are an expert...' }
- },
- subnodes: { model: model, memory: memoryNode, outputParser: parser }
- }
-});
-\`\`\`
-WRONG: \`.to(agent, { connectionType: 'ai_languageModel' })\` — subnodes MUST be in the config object.
-For values inside AI subnodes, use explicit references such as \`nodeJson(triggerNode, 'sessionId')\` instead of \`$json.sessionId\`. For Chat Trigger memory specifically, \`sessionIdType: 'fromInput'\` is also valid.
-
-### Code Node
-\`\`\`javascript
-const codeNode = node({
- type: 'n8n-nodes-base.code',
- version: 2,
- config: {
- name: 'Process Data',
- parameters: {
- mode: 'runOnceForAllItems',
- jsCode: \\\`
-const items = $input.all();
-return items.map(item => ({
- json: { ...item.json, processed: true }
-}));
-\\\`.trim()
- }
- }
-});
-\`\`\`
-
-### Data Table (built-in n8n storage)
-\`\`\`javascript
-const storeData = node({
- type: 'n8n-nodes-base.dataTable',
- version: 1.1,
- config: {
- name: 'Store Data',
- parameters: {
- resource: 'row',
- operation: 'insert',
- dataTableId: { __rl: true, mode: 'name', value: 'my-table' },
- columns: {
- mappingMode: 'defineBelow',
- value: {
- name: '={{ $json.name }}',
- email: '={{ $json.email }}'
- },
- schema: [
- { id: 'name', displayName: 'name', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true },
- { id: 'email', displayName: 'email', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true }
- ]
- }
- }
- }
-});
-\`\`\`
-
-**Data Table rules**
-- Row IDs are auto-generated by Data Tables. Do NOT create a custom \`id\` column and do NOT seed an \`id\` value on insert.
-- To fetch many rows, use \`operation: 'get'\` with \`returnAll: true\`. Do NOT invent \`getAll\`.
-- When filtering rows for update/delete, it is valid to match on the built-in row \`id\`, but that is not part of the user-defined table schema.
-
-### Google Sheets — Column Mapping
-The \`columns\` parameter requires a schema object, never a string:
-\`\`\`javascript
-// autoMapInputData — maps $json fields to sheet columns automatically
-columns: {
- mappingMode: 'autoMapInputData',
- value: {},
- schema: [
- { id: 'Name', displayName: 'Name', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true },
- { id: 'Email', displayName: 'Email', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: false },
- ]
-}
-
-// defineBelow — explicit expression mapping
-columns: {
- mappingMode: 'defineBelow',
- value: { name: '={{ $json.name }}', email: '={{ $json.email }}' },
- schema: [
- { id: 'name', displayName: 'name', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true },
- { id: 'email', displayName: 'email', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true }
- ]
-}
-\`\`\`
-WRONG: \`columns: 'autoMapInputData'\` — this is a string, not a schema object. Will fail validation.
-
-### Parallel Branches + Merge
-When multiple paths must converge, include the full downstream chain in EACH branch.
-There is NO fan-in primitive — shared nodes must be duplicated or use sub-workflows.
-
-### Batch Processing — splitInBatches with loop
-\`\`\`javascript
-const batch = node({
- type: 'n8n-nodes-base.splitInBatches',
- version: 3,
- config: { name: 'Batch', parameters: { batchSize: 50 } }
-});
-// Connect: trigger -> batch -> processNode -> batch (loop back)
-// The batch node automatically outputs to "done" when all items are processed.
-\`\`\`
-
-### Multiple Triggers
-Independent entry points can feed into shared downstream nodes. Each trigger starts its own branch:
-\`\`\`javascript
-export default workflow('id', 'name')
- .add(webhookTrigger).to(processNode).to(storeNode)
- .add(scheduleTrigger).to(processNode);
-\`\`\`
-
-### Google Sheets — documentId and sheetName (RLC fields)
-
-These are Resource Locator fields that require the \`__rl\` object format:
-\`\`\`typescript
-// CORRECT — RLC object with discovered ID
-documentId: { __rl: true, mode: 'id', value: '1abc123...' },
-sheetName: { __rl: true, mode: 'name', value: 'Sheet1' },
-
-// CORRECT — RLC with name-based lookup
-documentId: { __rl: true, mode: 'name', value: 'Sales Pipeline' },
-
-// WRONG — plain string
-documentId: 'YOUR_SPREADSHEET_ID', // Not an RLC object
-
-// WRONG — expr() wrapper
-documentId: expr('{{ "spreadsheetId" }}'), // RLC fields don't use expressions
-\`\`\`
-Always use the IDs from \`nodes(action="explore-resources")\` results inside the RLC \`value\` field.
-
-### AI Tool Connection Patterns
-${AI_TOOL_PATTERNS}
-
-### Connection-Changing Parameters
-${CONNECTION_CHANGING_PARAMETERS}
-
-### Baseline Flow Control Nodes
-${BASELINE_FLOW_CONTROL}`;
-}
-
-const BUILDER_SPECIFIC_PATTERNS = buildBuilderSpecificPatterns();
+**Pay attention to @builderHint annotations in search results and type definitions** — they contain node-specific configuration rules and code examples. Read them carefully when configuring any node — they prevent common mistakes.`;
// ── Composed SDK rules from shared + local sources ───────────────────────────
@@ -340,11 +128,12 @@ ${PLACEHOLDERS_RULE}
## Mandatory Process
1. **Research**: If the workflow fits a known category (notification, chatbot, scheduling, data_transformation, etc.), call \`nodes(action="suggested")\` first for curated recommendations. Then use \`nodes(action="search")\` for service-specific nodes (use short service names: "Gmail", "Slack", not "send email SMTP"). The results include \`discriminators\` (available resources and operations) for nodes that need them. Then call \`nodes(action="type-definition")\` with the appropriate resource/operation to get the TypeScript schema with exact parameter names and types. **Pay attention to @builderHint annotations** in search results and type definitions — they prevent common configuration mistakes.
2. **Build**: Write TypeScript SDK code and call \`build-workflow\`. Follow the SDK patterns below exactly.
-3. **Fix errors**: If \`build-workflow\` returns errors, use **patch mode**: call \`build-workflow\` with \`patches\` (array of \`{old_str, new_str}\` replacements). Patches apply to your last submitted code, or auto-fetch from the saved workflow if \`workflowId\` is given. Much faster than resending full code.
-4. **Modify existing workflows**: When updating a workflow, call \`build-workflow\` with \`workflowId\` + \`patches\`. The tool fetches the current code and applies your patches. Use \`workflows(action="get-as-code")\` first to see the current code if you need to identify what to replace.
-5. **Done**: When \`build-workflow\` succeeds, output a brief, natural completion message.
+3. **Trace wiring before declaring done**: For workflows containing IF, Switch, or Merge nodes, trace each branch from its source to its target — confirm IF outputs are wired with \`.onTrue()\`/\`.onFalse()\`, every Switch \`outputKey\` has a matching \`.onCase('')\`, and the Merge mode matches the data shape. Read each node's \`@builderHint\` for selection criteria.
+4. **Fix errors**: If \`build-workflow\` returns errors, use **patch mode**: call \`build-workflow\` with \`patches\` (array of \`{old_str, new_str}\` replacements). Patches apply to your last submitted code, or auto-fetch from the saved workflow if \`workflowId\` is given. Much faster than resending full code.
+5. **Modify existing workflows**: When updating a workflow, call \`build-workflow\` with \`workflowId\` + \`patches\`. The tool fetches the current code and applies your patches. Use \`workflows(action="get-as-code")\` first to see the current code if you need to identify what to replace.
+6. **Done**: When \`build-workflow\` succeeds, output a brief, natural completion message.
-Do NOT produce visible output until step 5. All reasoning happens internally.
+Do NOT produce visible output until step 6. All reasoning happens internally.
## Credential Rules (tool mode)
- Use \`newCredential('Credential Name', 'credential-id')\` only when the user selected a specific existing credential or the workflow already has one.
@@ -448,8 +237,8 @@ const fetchWeather = node({
name: 'Fetch Weather',
parameters: {
locationSelection: 'cityName',
- cityName: '={{ $json.city }}',
- format: '={{ $json.units }}'
+ cityName: expr('{{ $json.city }}'),
+ format: expr('{{ $json.units }}')
},
credentials: { openWeatherMapApi: { id: 'credId', name: 'OpenWeatherMap account' } }
}
@@ -617,18 +406,20 @@ n8n normalizes column names to snake_case (e.g., \`dayName\` → \`day_name\`).
5. **Write workflow code** to \`${workspaceRoot}/src/workflow.ts\`.
-6. **Validate with tsc**: Run the TypeScript compiler for real type checking:
+6. **Trace wiring before declaring done**: For workflows containing IF, Switch, or Merge nodes, trace each branch from its source to its target — confirm IF outputs are wired with \`.onTrue()\`/\`.onFalse()\`, every Switch \`outputKey\` has a matching \`.onCase('')\`, and the Merge mode matches the data shape. Read each node's \`@builderHint\` for selection criteria.
+
+7. **Validate with tsc**: Run the TypeScript compiler for real type checking:
\`\`\`
execute_command: cd ~/workspace && npx tsc --noEmit 2>&1
\`\`\`
Fix any errors using \`edit_file\` (with absolute path) to update the code, then re-run tsc. Iterate until clean.
**Important**: If tsc reports errors you cannot resolve after 2 attempts, skip tsc and proceed to submit-workflow. The submit tool has its own validation.
-7. **Submit**: When tsc passes cleanly, call \`submit-workflow\` to validate the workflow graph and save it to n8n.
+8. **Submit**: When tsc passes cleanly, call \`submit-workflow\` to validate the workflow graph and save it to n8n.
-8. **Fix submission errors**: If \`submit-workflow\` returns errors, edit the file and submit again immediately. Skip tsc for validation-only errors. **Never end your turn on a file edit — always re-submit first.** The system compares file hashes: if the file changed since the last submit, all your work is discarded. End only on a successful re-submit or after you explicitly report the blocking error.
+9. **Fix submission errors**: If \`submit-workflow\` returns errors, edit the file and submit again immediately. Skip tsc for validation-only errors. **Never end your turn on a file edit — always re-submit first.** The system compares file hashes: if the file changed since the last submit, all your work is discarded. End only on a successful re-submit or after you explicitly report the blocking error.
-9. **Done**: Output ONE sentence summarizing what was built, including the workflow ID and any known issues.
+10. **Done**: Output ONE sentence summarizing what was built, including the workflow ID and any known issues.
### For complex workflows (5+ nodes, multiple integrations):
@@ -644,8 +435,9 @@ Follow the **Compositional Workflow Pattern** above. The process becomes:
c. Submit the chunk: \`submit-workflow\` with \`filePath\` pointing to the chunk file. Test via \`executions(action="run")\`.
d. Fix if needed (max 2 submission fix attempts per chunk).
6. **Write the main workflow** in \`${workspaceRoot}/src/workflow.ts\` that composes chunks via \`executeWorkflow\` nodes, referencing each chunk's workflow ID.
-7. **Submit** the main workflow.
-8. **Done**: Output ONE sentence summarizing what was built, including the workflow ID and any known issues.
+7. **Trace wiring before declaring done**: For workflows containing IF, Switch, or Merge nodes, trace each branch from its source to its target — confirm IF outputs are wired with \`.onTrue()\`/\`.onFalse()\`, every Switch \`outputKey\` has a matching \`.onCase('')\`, and the Merge mode matches the data shape. Read each node's \`@builderHint\` for selection criteria.
+8. **Submit** the main workflow.
+9. **Done**: Output ONE sentence summarizing what was built, including the workflow ID and any known issues.
Do NOT produce visible output until the final step. All reasoning happens internally.
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 be47d39139e..da507ca74fd 100644
--- a/packages/@n8n/nodes-langchain/nodes/Guardrails/v2/GuardrailsV2.node.ts
+++ b/packages/@n8n/nodes-langchain/nodes/Guardrails/v2/GuardrailsV2.node.ts
@@ -52,6 +52,58 @@ export class GuardrailsV2 implements INodeType {
},
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.',
+ extraTypeDefContent: [
+ {
+ displayOptions: {
+ show: {
+ operation: ['classify'],
+ },
+ },
+ content: `
+
+const model = languageModel({
+ type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
+ version: 1.3,
+ config: {
+ name: 'OpenAI Chat Model',
+ parameters: { model: { __rl: true, mode: 'list', value: 'gpt-5.4' } },
+ credentials: { openAiApi: { id: 'credId', name: 'OpenAI account' } }
+ }
+});
+
+const guardrailsCheck = node({
+ type: '@n8n/n8n-nodes-langchain.guardrails',
+ version: 2,
+ config: {
+ name: 'Guardrails',
+ parameters: {
+ operation: 'classify',
+ text: expr('{{ $json.input }}'),
+ guardrails: { jailbreak: { value: { threshold: 0.7 } } }
+ },
+ subnodes: { model }
+ }
+});
+
+const passHandler = node({
+ type: 'n8n-nodes-base.set',
+ version: 3.4,
+ config: { name: 'Handle Pass', parameters: {} }
+});
+
+const failHandler = node({
+ type: 'n8n-nodes-base.set',
+ version: 3.4,
+ config: { name: 'Handle Fail', parameters: {} }
+});
+
+// output 0 = Pass, output 1 = Fail
+guardrailsCheck.output(0).to(passHandler);
+guardrailsCheck.output(1).to(failHandler);
+
+`,
+ },
+ ],
},
};
}
diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts
index c6f6f97adce..085fe62fd09 100644
--- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts
+++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts
@@ -30,6 +30,8 @@ export class Agent extends VersionedNodeType {
},
defaultVersion: 3.1,
builderHint: {
+ searchHint:
+ "Wire model/memory/tools/outputParser via the SDK `subnodes` config object using factory functions (`languageModel()`, `memory()`, `tool()`, `outputParser()`). Inside subnodes, reference upstream data with `nodeJson(triggerNode, 'path')`, not `$json` — subnodes do not share the main predecessor's item context.",
relatedNodes: [
{
nodeType: 'n8n-nodes-base.aggregate',
@@ -50,6 +52,73 @@ export class Agent extends VersionedNodeType {
'Required for conversational workflows - connect memory to every agent that needs to recall previous messages in the conversation',
},
],
+ extraTypeDefContent: [
+ {
+ content: `
+
+const chatTrigger = trigger({
+ type: '@n8n/n8n-nodes-langchain.chatTrigger',
+ version: 1.3,
+ config: {
+ name: 'Chat Trigger',
+ parameters: { public: false },
+ output: [{ sessionId: 'chat-session-id', chatInput: 'Hello' }]
+ }
+});
+
+const model = languageModel({
+ type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
+ version: 1.3,
+ config: {
+ name: 'OpenAI Chat Model',
+ parameters: { model: { __rl: true, mode: 'list', value: 'gpt-5.4' } },
+ credentials: { openAiApi: { id: 'credId', name: 'OpenAI account' } }
+ }
+});
+
+const parser = outputParser({
+ type: '@n8n/n8n-nodes-langchain.outputParserStructured',
+ version: 1.3,
+ config: {
+ name: 'Output Parser',
+ parameters: {
+ schemaType: 'fromJson',
+ jsonSchemaExample: '{ "score": 75, "tier": "hot" }'
+ }
+ }
+});
+
+const memoryNode = memory({
+ type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
+ version: 1.3,
+ config: {
+ name: 'Conversation Memory',
+ parameters: {
+ sessionIdType: 'customKey',
+ sessionKey: nodeJson(chatTrigger, 'sessionId'),
+ contextWindowLength: 10
+ }
+ }
+});
+
+const agent = node({
+ type: '@n8n/n8n-nodes-langchain.agent',
+ version: 3.1,
+ config: {
+ name: 'AI Agent',
+ parameters: {
+ promptType: 'define',
+ text: expr('{{ $json.prompt }}'),
+ hasOutputParser: true,
+ options: { systemMessage: 'You are an expert...' }
+ },
+ subnodes: { model, memory: memoryNode, outputParser: parser }
+ }
+});
+
+`,
+ },
+ ],
},
};
diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentToolV3.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentToolV3.node.ts
index 8ebf445fd51..46d28ef8e12 100644
--- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentToolV3.node.ts
+++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentToolV3.node.ts
@@ -57,6 +57,10 @@ export class AgentToolV3 implements INodeType {
type: 'boolean',
default: false,
noDataExpression: true,
+ builderHint: {
+ propertyHint:
+ 'Set to `true` when you need structured JSON output. The agent then requires an `outputParser` entry in its `subnodes` config (typically an `outputParserStructured` node defined via the `outputParser({...})` SDK factory). With `hasOutputParser: false` the agent returns a plain string in `$json.output`.',
+ },
},
{
displayName: `Connect an output parser on the canvas to specify the output format you require`,
diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentV3.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentV3.node.ts
index c0c3edc119e..33e853158b5 100644
--- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentV3.node.ts
+++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/V3/AgentV3.node.ts
@@ -98,6 +98,10 @@ export class AgentV3 implements INodeType {
type: 'boolean',
default: false,
noDataExpression: true,
+ builderHint: {
+ propertyHint:
+ 'Set to `true` when you need structured JSON output. The agent then requires an `outputParser` entry in its `subnodes` config (typically an `outputParserStructured` node defined via the `outputParser({...})` SDK factory). With `hasOutputParser: false` the agent returns a plain string in `$json.output`.',
+ },
},
{
displayName: `Connect an output parser on the canvas to specify the output format you require`,
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 3efdd111b14..6938614aa31 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
@@ -18,9 +18,18 @@ import type * as GenerateTypesModule from '../generate-types/generate-types';
// Type Definitions (Expected interfaces from the implementation)
// =============================================================================
+interface BuilderHintVariation {
+ content: string;
+ displayOptions?: {
+ show?: Record;
+ hide?: Record;
+ };
+}
+
interface ParameterBuilderHint {
propertyHint: string;
placeholderSupported?: boolean;
+ extraTypeDefContent?: BuilderHintVariation[];
}
interface NestedOption {
@@ -76,6 +85,7 @@ interface NodeTypeDescription {
hidden?: boolean;
schemaPath?: string;
builderHint?: {
+ message?: string;
inputs?: Record<
string,
{
@@ -86,6 +96,7 @@ interface NodeTypeDescription {
};
}
>;
+ extraTypeDefContent?: BuilderHintVariation[];
};
}
@@ -1393,6 +1404,156 @@ describe('generate-types', () => {
expect(result).not.toContain('GmailV21Params');
});
+ it('should route node-level extraTypeDefContent variations into matching narrowed types only', () => {
+ const vectorStoreLikeNode: NodeTypeDescription & { builderHint?: unknown } = {
+ name: 'n8n-nodes-test.vectorStoreLike',
+ displayName: 'Vector Store Like',
+ group: ['transform'],
+ version: 1,
+ inputs: ['main'],
+ outputs: ['main'],
+ builderHint: {
+ extraTypeDefContent: [
+ {
+ displayOptions: { show: { mode: ['insert'] } },
+ content: '\ninsert example\n',
+ },
+ {
+ displayOptions: { show: { mode: ['retrieve-as-tool'] } },
+ content: '\nRAG example\n',
+ },
+ ],
+ },
+ properties: [
+ {
+ displayName: 'Operation Mode',
+ name: 'mode',
+ type: 'options',
+ options: [
+ { name: 'Insert', value: 'insert' },
+ { name: 'Retrieve as Tool', value: 'retrieve-as-tool' },
+ ],
+ default: 'insert',
+ },
+ {
+ displayName: 'Insert Field',
+ name: 'insertField',
+ type: 'string',
+ default: '',
+ displayOptions: { show: { mode: ['insert'] } },
+ },
+ {
+ displayName: 'Tool Description',
+ name: 'toolDescription',
+ type: 'string',
+ default: '',
+ displayOptions: { show: { mode: ['retrieve-as-tool'] } },
+ },
+ ],
+ };
+
+ const result = generateTypes.generateDiscriminatedUnion(vectorStoreLikeNode);
+
+ // Each variation must land in its own narrowed type body — no cross-bleed.
+ const sections = result.split(/export type /);
+ const insertSection = sections.find((s) => s.startsWith('VectorStoreLikeInsertParams'));
+ const retrieveSection = sections.find((s) =>
+ s.startsWith('VectorStoreLikeRetrieveAsToolParams'),
+ );
+ expect(insertSection).toBeDefined();
+ expect(retrieveSection).toBeDefined();
+
+ expect(insertSection!).toContain('insert example');
+ expect(insertSection!).not.toContain('RAG example');
+
+ expect(retrieveSection!).toContain('RAG example');
+ expect(retrieveSection!).not.toContain('insert example');
+ });
+
+ it('should not duplicate an unconditional node-level variation across narrowed types (file header only)', () => {
+ const node: NodeTypeDescription & { builderHint?: unknown } = {
+ name: 'n8n-nodes-test.unconditional',
+ displayName: 'Unconditional',
+ group: ['transform'],
+ version: 1,
+ inputs: ['main'],
+ outputs: ['main'],
+ builderHint: {
+ extraTypeDefContent: [{ content: 'unconditional only' }],
+ },
+ properties: [
+ {
+ displayName: 'Operation Mode',
+ name: 'mode',
+ type: 'options',
+ options: [
+ { name: 'Insert', value: 'insert' },
+ { name: 'Retrieve', value: 'retrieve' },
+ ],
+ default: 'insert',
+ },
+ {
+ displayName: 'Insert Field',
+ name: 'insertField',
+ type: 'string',
+ default: '',
+ displayOptions: { show: { mode: ['insert'] } },
+ },
+ ],
+ };
+
+ // Unconditional content does NOT appear inside narrowed type bodies —
+ // it's reserved for the file-level node header.
+ const result = generateTypes.generateDiscriminatedUnion(node);
+ expect(result).not.toContain('unconditional only');
+
+ // File header emits it exactly once.
+ const header = generateTypes.generateNodeJSDoc(node);
+ expect(header).toContain('unconditional only');
+ expect(header.match(/unconditional only/g)?.length).toBe(1);
+ });
+
+ it('should skip variations whose displayOptions do not match the combo', () => {
+ const node: NodeTypeDescription & { builderHint?: unknown } = {
+ name: 'n8n-nodes-test.partialMatch',
+ displayName: 'Partial Match',
+ group: ['transform'],
+ version: 1,
+ inputs: ['main'],
+ outputs: ['main'],
+ builderHint: {
+ extraTypeDefContent: [
+ {
+ displayOptions: { show: { mode: ['unknownMode'] } },
+ content: 'should NOT appear',
+ },
+ ],
+ },
+ properties: [
+ {
+ displayName: 'Operation Mode',
+ name: 'mode',
+ type: 'options',
+ options: [
+ { name: 'Insert', value: 'insert' },
+ { name: 'Retrieve', value: 'retrieve' },
+ ],
+ default: 'insert',
+ },
+ {
+ displayName: 'Insert Field',
+ name: 'insertField',
+ type: 'string',
+ default: '',
+ displayOptions: { show: { mode: ['insert'] } },
+ },
+ ],
+ };
+
+ const result = generateTypes.generateDiscriminatedUnion(node);
+ expect(result).not.toContain('should NOT appear');
+ });
+
it('should generate simple interface for HTTP Request (no discriminators)', () => {
const result = generateTypes.generateDiscriminatedUnion(mockHttpRequestNode);
@@ -1776,6 +1937,77 @@ describe('generate-types', () => {
expect(result).toContain('@builderHint');
expect(result).toContain('<a href=');
});
+
+ it('should include unconditional extraTypeDefContent variation below @builderHint, preserving line breaks and tags verbatim', () => {
+ const prop: NodeProperty = {
+ name: 'columns',
+ displayName: 'Columns',
+ type: 'resourceMapper',
+ description: 'Column mapping',
+ builderHint: {
+ propertyHint: 'Pass the full resourceMapper object',
+ extraTypeDefContent: [
+ {
+ content:
+ '\n\ncolumns: { mappingMode: \'autoMapInputData\' }\n\n',
+ },
+ ],
+ },
+ default: {},
+ };
+ const result = generateTypes.generatePropertyJSDoc(prop);
+ expect(result).toContain('@builderHint Pass the full resourceMapper object');
+ expect(result).toContain(' * ');
+ expect(result).toContain(' * ');
+ expect(result).toContain(" * columns: { mappingMode: 'autoMapInputData' }");
+ expect(result).toContain(' * ');
+ expect(result).toContain(' * ');
+ // Angle brackets in variation content must NOT be HTML-escaped — the LLM
+ // must see the tags verbatim so it can use them as structural cues.
+ expect(result).not.toContain('<patterns>');
+ expect(result).not.toContain('<pattern title=');
+ });
+
+ it('should escape closing JSDoc sequences inside variation content', () => {
+ const prop: NodeProperty = {
+ name: 'foo',
+ displayName: 'Foo',
+ type: 'string',
+ description: 'Foo',
+ builderHint: {
+ propertyHint: 'msg',
+ extraTypeDefContent: [{ content: 'block end */ inside example' }],
+ },
+ default: '',
+ };
+ const result = generateTypes.generatePropertyJSDoc(prop);
+ // The literal "*/" would terminate the JSDoc block early; it must be escaped.
+ expect(result).not.toContain('block end */ inside');
+ expect(result).toContain('block end *\\/ inside');
+ });
+
+ it('should skip param-level variations whose displayOptions cannot be evaluated at file/property scope', () => {
+ // Param-level emission (generatePropertyJSDoc, generateNestedPropertyJSDoc) has
+ // no discriminator combo, so any gated variation is dropped — those belong on the
+ // node-level builderHint where the codegen can route them per narrowed type.
+ const prop: NodeProperty = {
+ name: 'foo',
+ displayName: 'Foo',
+ type: 'string',
+ description: 'Foo',
+ builderHint: {
+ propertyHint: 'msg',
+ extraTypeDefContent: [
+ { displayOptions: { show: { mode: ['insert'] } }, content: 'gated content' },
+ { content: 'always shown' },
+ ],
+ },
+ default: '',
+ };
+ const result = generateTypes.generatePropertyJSDoc(prop);
+ expect(result).toContain('always shown');
+ expect(result).not.toContain('gated content');
+ });
});
describe('generateNodeJSDoc', () => {
@@ -1789,6 +2021,37 @@ describe('generate-types', () => {
const result = generateTypes.generateNodeJSDoc(mockGmailNode);
expect(result).toContain('Node Types');
});
+
+ it('should emit node-level @builderHint searchHint at the file header', () => {
+ const node = {
+ ...mockGmailNode,
+ builderHint: {
+ searchHint: 'AI Agent — wire subnodes via the config object',
+ },
+ };
+ const result = generateTypes.generateNodeJSDoc(node);
+ expect(result).toContain('@builderHint AI Agent — wire subnodes via the config object');
+ });
+
+ it('should emit unconditional extraTypeDefContent variations at the file header but skip gated ones', () => {
+ const node = {
+ ...mockGmailNode,
+ builderHint: {
+ extraTypeDefContent: [
+ { content: '\nalways\n' },
+ {
+ displayOptions: { show: { mode: ['insert'] } },
+ content: '\ninsert-only\n',
+ },
+ ],
+ },
+ };
+ const result = generateTypes.generateNodeJSDoc(node);
+ // Unconditional variation lands at file header.
+ expect(result).toContain(' * always');
+ // Gated variation does NOT — it's emitted per-combo via emitNodeHintForCombo.
+ expect(result).not.toContain('insert-only');
+ });
});
// =========================================================================
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 0285529f8b8..06e9f1f485e 100644
--- a/packages/@n8n/workflow-sdk/src/generate-types/generate-types.ts
+++ b/packages/@n8n/workflow-sdk/src/generate-types/generate-types.ts
@@ -250,6 +250,20 @@ const AI_TYPE_TO_SUBNODE_FIELD: Record<
// Type Definitions
// =============================================================================
+/**
+ * One variation of `extraTypeDefContent`, optionally gated by `displayOptions`.
+ * Variations with `displayOptions` are emitted only in narrowed discriminator
+ * types (e.g. per-mode or per-resource/operation files) whose combo matches.
+ * Variations without `displayOptions` are emitted unconditionally.
+ */
+export interface BuilderHintVariation {
+ content: string;
+ displayOptions?: {
+ show?: Record;
+ hide?: Record;
+ };
+}
+
export interface ParameterBuilderHint {
propertyHint: string;
placeholderSupported?: boolean;
@@ -350,6 +364,117 @@ export interface JsonSchema {
$ref?: string;
}
+// =============================================================================
+// JSDoc emission helpers
+// =============================================================================
+
+/**
+ * Emit `@builderHint` JSDoc plus optional multi-line `extraTypeDefContent` lines.
+ *
+ * `message` is HTML-escaped (`<` / `>` → entities) because it round-trips through
+ * the search engine's `...` XML envelope, where bare
+ * angle brackets would corrupt the wrapping tag.
+ *
+ * `extraTypeDefContent` does NOT escape angle brackets — author-written tags such
+ * as `` must round-trip into the `.d.ts` verbatim so the LLM sees them
+ * as structural cues. Only `*\/` is escaped to keep the JSDoc block well-formed.
+ */
+/**
+ * Determines whether a variation should be emitted in the current scope.
+ *
+ * The two scopes are mutually exclusive so unconditional variations are
+ * NOT duplicated across narrowed types:
+ *
+ * - File-level header (no `combo`): emit ONLY unconditional variations
+ * (those with no `displayOptions`). They appear once at the top of the
+ * generated `.d.ts` and cover every narrowed type.
+ *
+ * - Narrowed config block (per `combo`): emit ONLY gated variations
+ * whose `displayOptions` match the combo. Unconditional variations are
+ * skipped here — they were already emitted at the file header.
+ */
+function variationApplies(
+ variation: BuilderHintVariation,
+ combo: DiscriminatorCombination | undefined,
+): boolean {
+ const opts = variation.displayOptions;
+
+ // File-level scope: only unconditional variations.
+ if (!combo) return !opts;
+
+ // Narrowed scope: only gated variations whose displayOptions match.
+ if (!opts) return false;
+
+ if (opts.show) {
+ for (const [key, conditions] of Object.entries(opts.show)) {
+ const value = combo[key];
+ if (value === undefined) return false;
+ if (!checkConditions(conditions, [value])) return false;
+ }
+ }
+ if (opts.hide) {
+ for (const [key, conditions] of Object.entries(opts.hide)) {
+ const value = combo[key];
+ if (value === undefined) continue;
+ if (checkConditions(conditions, [value])) return false;
+ }
+ }
+ return true;
+}
+
+function emitBuilderHint(
+ lines: string[],
+ indent: string,
+ hint: { propertyHint?: string; extraTypeDefContent?: BuilderHintVariation[] },
+ combo?: DiscriminatorCombination,
+): void {
+ if (hint.propertyHint) {
+ const safePropertyHint = hint.propertyHint
+ .replace(/\*\//g, '*\\/')
+ .replace(//g, '>');
+ lines.push(`${indent} * @builderHint ${safePropertyHint}`);
+ }
+
+ if (!hint.extraTypeDefContent) return;
+
+ for (const variation of hint.extraTypeDefContent) {
+ if (!variationApplies(variation, combo)) continue;
+ const safe = variation.content.replace(/\*\//g, '*\\/');
+ for (const line of safe.split('\n')) {
+ lines.push(`${indent} * ${line}`);
+ }
+ }
+}
+
+/**
+ * `builderHint` is an extended n8n property not part of the upstream
+ * `NodeTypeDescription`. Centralized cast keeps the rest of the file clean.
+ */
+function getNodeBuilderHint(node: NodeTypeDescription): NodeBuilderHint | undefined {
+ return (node as NodeTypeDescription & { builderHint?: NodeBuilderHint }).builderHint;
+}
+
+/**
+ * Emit a JSDoc block for the node-level builderHint scoped to a single
+ * discriminator combination — only variations whose `displayOptions` match the
+ * combo are rendered. The `propertyHint` is intentionally not re-emitted per combo
+ * (it already lands in the file-level node header).
+ */
+function emitNodeHintForCombo(
+ lines: string[],
+ node: NodeTypeDescription,
+ combo: DiscriminatorCombination,
+): void {
+ const hint = getNodeBuilderHint(node);
+ if (!hint?.extraTypeDefContent?.some((v) => variationApplies(v, combo))) return;
+
+ const hintLines: string[] = [`${INDENT}/**`];
+ emitBuilderHint(hintLines, INDENT, { extraTypeDefContent: hint.extraTypeDefContent }, combo);
+ hintLines.push(`${INDENT} */`);
+ lines.push(...hintLines);
+}
+
// =============================================================================
// Schema Discovery & JSON Schema to TypeScript Conversion
// =============================================================================
@@ -883,11 +1008,7 @@ function generateNestedPropertyJSDoc(
// Builder hint - guidance for AI/workflow builders
if (prop.builderHint) {
- const safeBuilderHint = prop.builderHint.propertyHint
- .replace(/\*\//g, '*\\/')
- .replace(//g, '>');
- lines.push(`${indent} * @builderHint ${safeBuilderHint}`);
+ emitBuilderHint(lines, indent, prop.builderHint);
}
// Placeholder support flag — signals to the builder agent (and the runtime
@@ -1055,14 +1176,10 @@ function generateFixedCollectionType(
groupJsDocLines.push(`${INDENT.repeat(2)}/** ${desc}`);
}
if (group.builderHint) {
- const safeBuilderHint = group.builderHint.propertyHint
- .replace(/\*\//g, '*\\/')
- .replace(//g, '>');
if (groupJsDocLines.length === 0) {
groupJsDocLines.push(`${INDENT.repeat(2)}/**`);
}
- groupJsDocLines.push(`${INDENT.repeat(2)} * @builderHint ${safeBuilderHint}`);
+ emitBuilderHint(groupJsDocLines, INDENT.repeat(2), group.builderHint);
}
if (isMultipleValues && hasMinRequired) {
if (groupJsDocLines.length === 0) {
@@ -1859,7 +1976,9 @@ export function generateDiscriminatedUnion(node: NodeTypeDescription): string {
lines.push(`export type ${configName} = {`);
- // Add discriminator fields
+ emitNodeHintForCombo(lines, node, combo);
+
+ // Discriminator literal fields for this combo.
for (const [key, value] of Object.entries(combo)) {
if (value !== undefined) {
lines.push(`${INDENT}${key}: '${value}';`);
@@ -1914,11 +2033,7 @@ export function generatePropertyJSDoc(
// Builder hint - guidance for AI/workflow builders
if (prop.builderHint) {
- const safeBuilderHint = prop.builderHint.propertyHint
- .replace(/\*\//g, '*\\/')
- .replace(//g, '>');
- lines.push(` * @builderHint ${safeBuilderHint}`);
+ emitBuilderHint(lines, '', prop.builderHint);
}
// Placeholder support flag — signals to the builder agent (and the runtime
@@ -2025,6 +2140,18 @@ export function generateNodeJSDoc(node: NodeTypeDescription): string {
lines.push(` * @subnodeType ${subnodeType}`);
}
+ // Node-level builder hint — searchHint and unconditional extraTypeDefContent.
+ // `relatedNodes` and `inputs` are consumed elsewhere (search engine, subnode
+ // extraction). Variations with `displayOptions` are skipped here — they're
+ // emitted per-combo in narrowed config types via `emitNodeHintForCombo`.
+ const nodeHint = getNodeBuilderHint(node);
+ if (nodeHint && (nodeHint.searchHint || nodeHint.extraTypeDefContent?.length)) {
+ emitBuilderHint(lines, '', {
+ propertyHint: nodeHint.searchHint,
+ extraTypeDefContent: nodeHint.extraTypeDefContent,
+ });
+ }
+
lines.push(' */');
return lines.join('\n');
@@ -2533,7 +2660,9 @@ export function generateDiscriminatorFile(
}
lines.push(`export type ${configName} = {`);
- // Add discriminator fields
+ emitNodeHintForCombo(lines, node, combo);
+
+ // Discriminator literal fields for this combo.
for (const [key, value] of Object.entries(combo)) {
if (value !== undefined) {
lines.push(`${INDENT}${key}: '${value}';`);
@@ -3398,7 +3527,9 @@ function generateDiscriminatedUnionForEntry(
lines.push(`export type ${configName} = {`);
- // Add discriminator fields
+ emitNodeHintForCombo(lines, node, combo);
+
+ // Discriminator literal fields for this combo.
for (const [key, value] of Object.entries(combo)) {
if (value !== undefined) {
lines.push(`${INDENT}${key}: '${value}';`);
@@ -3568,7 +3699,18 @@ interface BuilderHintInput {
}
interface NodeBuilderHint {
+ searchHint?: string;
+ relatedNodes?: Array<{ nodeType: string; relationHint: string }>;
inputs?: Record;
+ /**
+ * Multi-line content (typically code examples wrapped in `...`)
+ * emitted into the generated `.d.ts` but NOT surfaced in
+ * `nodes(action="search")` results. Each variation may carry `displayOptions`
+ * so per-mode / per-resource / per-operation examples land only in their
+ * corresponding narrowed type. Variations with no `displayOptions` emit
+ * once at the file-level node header.
+ */
+ extraTypeDefContent?: BuilderHintVariation[];
}
/**
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 a9d2cbb191e..817763e17bb 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
@@ -10,23 +10,3 @@ Structured Output Parser: Prefer this over manually extracting/parsing AI output
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 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 [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') [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/index.ts b/packages/@n8n/workflow-sdk/src/prompts/node-selection/index.ts
index 7042ece554c..c2eee7f9397 100644
--- a/packages/@n8n/workflow-sdk/src/prompts/node-selection/index.ts
+++ b/packages/@n8n/workflow-sdk/src/prompts/node-selection/index.ts
@@ -1,5 +1,4 @@
-export { AI_NODE_SELECTION, AI_TOOL_PATTERNS } from './ai-nodes';
-export { NODE_SELECTION_PATTERNS, BASELINE_FLOW_CONTROL } from './use-case-patterns';
+export { AI_NODE_SELECTION } from './ai-nodes';
+export { NODE_SELECTION_PATTERNS } from './use-case-patterns';
export { TRIGGER_SELECTION } from './trigger-selection';
export { NATIVE_NODE_PREFERENCE } from './native-preference';
-export { CONNECTION_CHANGING_PARAMETERS } from './connection-parameters';
diff --git a/packages/@n8n/workflow-sdk/src/prompts/node-selection/use-case-patterns.ts b/packages/@n8n/workflow-sdk/src/prompts/node-selection/use-case-patterns.ts
index 846f7c8c3f1..a303541af0c 100644
--- a/packages/@n8n/workflow-sdk/src/prompts/node-selection/use-case-patterns.ts
+++ b/packages/@n8n/workflow-sdk/src/prompts/node-selection/use-case-patterns.ts
@@ -44,12 +44,3 @@ CHATBOTS:
MEDIA:
- OpenAI: DALL-E image generation, Sora video, Whisper transcription
- Google Gemini: Imagen image generation`;
-
-export const BASELINE_FLOW_CONTROL = `Baseline flow control nodes (used in most workflows):
-
-- n8n-nodes-base.aggregate: Combines multiple items into one item
-- n8n-nodes-base.if: Routes items based on true/false condition
-- n8n-nodes-base.switch: Routes items to different paths based on rules or expressions
-- n8n-nodes-base.splitOut: Expands a single item containing an array into multiple individual items
-- n8n-nodes-base.merge: Combines data from multiple parallel branches (for 3+ inputs: mode="append" + numberInputs)
-- n8n-nodes-base.set: Transforms and restructures data fields`;
diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts
index fe23a5b6b15..6b4d58906e0 100644
--- a/packages/nodes-base/nodes/Code/Code.node.ts
+++ b/packages/nodes-base/nodes/Code/Code.node.ts
@@ -97,6 +97,30 @@ export class Code implements INodeType {
relationHint: 'Use this instead for creating html pages',
},
],
+ extraTypeDefContent: [
+ {
+ content: `
+
+const codeNode = node({
+ type: 'n8n-nodes-base.code',
+ version: 2,
+ config: {
+ name: 'Process Data',
+ parameters: {
+ mode: 'runOnceForAllItems',
+ jsCode: \`
+const items = $input.all();
+return items.map(item => ({
+ json: { ...item.json, processed: true }
+}));
+\`.trim()
+ }
+ }
+});
+
+`,
+ },
+ ],
},
parameterPane: 'wide',
properties: [
diff --git a/packages/nodes-base/nodes/DataTable/DataTable.node.ts b/packages/nodes-base/nodes/DataTable/DataTable.node.ts
index 7876a9e22a4..9b13b4926e5 100644
--- a/packages/nodes-base/nodes/DataTable/DataTable.node.ts
+++ b/packages/nodes-base/nodes/DataTable/DataTable.node.ts
@@ -27,6 +27,40 @@ export class DataTable implements INodeType {
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
+ builderHint: {
+ extraTypeDefContent: [
+ {
+ displayOptions: { show: { resource: ['row'], operation: ['insert'] } },
+ content: `
+
+const storeData = node({
+ type: 'n8n-nodes-base.dataTable',
+ version: 1.1,
+ config: {
+ name: 'Store Data',
+ parameters: {
+ resource: 'row',
+ operation: 'insert',
+ dataTableId: { __rl: true, mode: 'name', value: 'my-table' },
+ columns: {
+ mappingMode: 'defineBelow',
+ value: {
+ name: expr('{{ $json.name }}'),
+ email: expr('{{ $json.email }}')
+ },
+ schema: [
+ { id: 'name', displayName: 'name', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true },
+ { id: 'email', displayName: 'email', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true }
+ ]
+ }
+ }
+ }
+});
+
+`,
+ },
+ ],
+ },
hints: [
{
message: 'The selected data table has no columns.',
diff --git a/packages/nodes-base/nodes/DataTable/actions/row/Row.resource.ts b/packages/nodes-base/nodes/DataTable/actions/row/Row.resource.ts
index 8c608792819..75774a3c850 100644
--- a/packages/nodes-base/nodes/DataTable/actions/row/Row.resource.ts
+++ b/packages/nodes-base/nodes/DataTable/actions/row/Row.resource.ts
@@ -34,6 +34,10 @@ export const description: INodeProperties[] = [
value: get.FIELD,
description: 'Get row(s)',
action: 'Get row(s)',
+ builderHint: {
+ propertyHint:
+ "There is no `getAll` operation. To fetch many rows, use `operation: 'get'` with `returnAll: true`.",
+ },
},
{
name: 'If Row Exists',
@@ -52,6 +56,10 @@ export const description: INodeProperties[] = [
value: insert.FIELD,
description: 'Insert a new row',
action: 'Insert row',
+ builderHint: {
+ propertyHint:
+ 'Row IDs are auto-generated. Do NOT define a custom `id` column or seed `id` on insert. The built-in row `id` is valid for filtering update/delete but is not part of the user-defined table schema.',
+ },
},
{
name: 'Update',
diff --git a/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts b/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts
index 5cbb33a7b81..a553680f937 100644
--- a/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts
+++ b/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts
@@ -29,6 +29,10 @@ export class FilterV2 implements INodeType {
outputs: [NodeConnectionTypes.Main],
outputNames: ['Kept', 'Discarded'],
parameterPane: 'wide',
+ builderHint: {
+ searchHint:
+ 'Filter emits 0 items when nothing matches and the chain stops cleanly — no IF gate needed before downstream loops.',
+ },
properties: [
{
displayName: 'Conditions',
@@ -43,6 +47,13 @@ export class FilterV2 implements INodeType {
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/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts
index 690c573ef80..a460fe70e19 100644
--- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts
+++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts
@@ -23,6 +23,38 @@ export class GoogleSheets extends VersionedNodeType {
relationHint: 'Prefer for workflow data storage with upsert',
},
],
+ extraTypeDefContent: [
+ {
+ displayOptions: {
+ show: {
+ resource: ['sheet'],
+ operation: ['append', 'appendOrUpdate', 'update'],
+ },
+ },
+ content: `
+
+columns: {
+ mappingMode: 'autoMapInputData',
+ value: {},
+ schema: [
+ { id: 'Name', displayName: 'Name', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true },
+ { id: 'Email', displayName: 'Email', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: false }
+ ]
+}
+
+
+columns: {
+ mappingMode: 'defineBelow',
+ value: { name: expr('{{ $json.name }}'), email: expr('{{ $json.email }}') },
+ schema: [
+ { id: 'name', displayName: 'name', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true },
+ { id: 'email', displayName: 'email', required: false, defaultMatch: false, display: true, type: 'string', canBeUsedToMatch: true }
+ ]
+}
+
+`,
+ },
+ ],
},
};
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 3c7841c27f6..02bfb5a36ca 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,10 @@ export const descriptions: INodeProperties[] = [
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
- builderHint: { propertyHint: "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. Resource locator value must be `{ __rl: true, mode, value }` — never a plain string or `expr()` wrapper.",
+ },
modes: [
{
displayName: 'From List',
@@ -139,7 +142,10 @@ export const descriptions: INodeProperties[] = [
default: { mode: 'list', value: '' },
// default: '', //empty string set to progresivly reveal fields
required: true,
- builderHint: { propertyHint: "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. Resource locator value must be `{ __rl: true, mode, value }` — never a plain string or `expr()` wrapper.",
+ },
typeOptions: {
loadOptionsDependsOn: ['documentId.value'],
},
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts
index d1eaabedc34..1458eb024c1 100644
--- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts
@@ -6,7 +6,12 @@ import {
type ResourceMapperField,
} from 'n8n-workflow';
-import { cellFormat, handlingExtraData, useAppendOption } from './commonDescription';
+import {
+ cellFormat,
+ columnsResourceMapperBuilderHint,
+ handlingExtraData,
+ useAppendOption,
+} from './commonDescription';
import type { GoogleSheet } from '../../helpers/GoogleSheet';
import type { SheetProperties, ValueInputOption } from '../../helpers/GoogleSheets.types';
import {
@@ -127,6 +132,7 @@ export const description: SheetProperties = [
value: null,
},
required: true,
+ builderHint: columnsResourceMapperBuilderHint,
typeOptions: {
loadOptionsDependsOn: ['sheetName.value'],
resourceMapper: {
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts
index fcb9fd6d963..0ad5fef785d 100644
--- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts
@@ -8,6 +8,7 @@ import { NodeOperationError } from 'n8n-workflow';
import {
cellFormat,
+ columnsResourceMapperBuilderHint,
handlingExtraData,
locationDefine,
useAppendOption,
@@ -171,6 +172,7 @@ export const description: SheetProperties = [
value: null,
},
required: true,
+ builderHint: columnsResourceMapperBuilderHint,
typeOptions: {
loadOptionsDependsOn: ['sheetName.value'],
resourceMapper: {
@@ -206,6 +208,7 @@ export const description: SheetProperties = [
value: null,
},
required: true,
+ builderHint: columnsResourceMapperBuilderHint,
typeOptions: {
loadOptionsDependsOn: ['sheetName.value'],
resourceMapper: {
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/commonDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/commonDescription.ts
index bd73aaaebfa..3a2c1713408 100644
--- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/commonDescription.ts
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/commonDescription.ts
@@ -1,4 +1,17 @@
-import type { INodeProperties } from 'n8n-workflow';
+import type { INodeProperties, IParameterBuilderHint } from 'n8n-workflow';
+
+/**
+ * Builder hint shared by every Google Sheets `columns` resourceMapper parameter
+ * (append, appendOrUpdate, update). The full resourceMapper object shape is
+ * non-obvious, and a bare string like `'autoMapInputData'` silently fails
+ * validation. The matching `` example lives on the node-level
+ * `builderHint.extraTypeDefContent` in `Google/Sheet/GoogleSheets.node.ts`,
+ * gated by `displayOptions: { show: { resource: ['sheet'], operation: [...] } }`.
+ */
+export const columnsResourceMapperBuilderHint: IParameterBuilderHint = {
+ propertyHint:
+ "Pass the full resourceMapper object: { mappingMode, value, schema }. A bare string like 'autoMapInputData' fails validation.",
+};
export const dataLocationOnSheet: INodeProperties = {
displayName: 'Data Location on Sheet',
diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts
index c2928027d6e..22c6bfd7ef0 100644
--- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts
+++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts
@@ -1,7 +1,12 @@
import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import { NodeOperationError, UserError } from 'n8n-workflow';
-import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
+import {
+ cellFormat,
+ columnsResourceMapperBuilderHint,
+ handlingExtraData,
+ locationDefine,
+} from './commonDescription';
import type { GoogleSheet } from '../../helpers/GoogleSheet';
import {
ROW_NUMBER,
@@ -157,6 +162,7 @@ export const description: SheetProperties = [
value: null,
},
required: true,
+ builderHint: columnsResourceMapperBuilderHint,
typeOptions: {
loadOptionsDependsOn: ['sheetName.value'],
resourceMapper: {
@@ -192,6 +198,7 @@ export const description: SheetProperties = [
value: null,
},
required: true,
+ builderHint: columnsResourceMapperBuilderHint,
typeOptions: {
loadOptionsDependsOn: ['sheetName.value'],
resourceMapper: {
diff --git a/packages/nodes-base/nodes/If/V2/IfV2.node.ts b/packages/nodes-base/nodes/If/V2/IfV2.node.ts
index e68366b17f0..57d856db5ac 100644
--- a/packages/nodes-base/nodes/If/V2/IfV2.node.ts
+++ b/packages/nodes-base/nodes/If/V2/IfV2.node.ts
@@ -29,6 +29,10 @@ export class IfV2 implements INodeType {
outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
outputNames: ['true', 'false'],
parameterPane: 'wide',
+ builderHint: {
+ searchHint:
+ 'After configuring, confirm the workflow wires both `.onTrue()` and `.onFalse()` (or only the relevant one) to the correct downstream node — IF has two named outputs and silently drops items routed to an unwired branch.',
+ },
properties: [
{
displayName: 'Conditions',
diff --git a/packages/nodes-base/nodes/Merge/v3/actions/versionDescription.ts b/packages/nodes-base/nodes/Merge/v3/actions/versionDescription.ts
index f1b50ba6656..27ee32cec93 100644
--- a/packages/nodes-base/nodes/Merge/v3/actions/versionDescription.ts
+++ b/packages/nodes-base/nodes/Merge/v3/actions/versionDescription.ts
@@ -13,6 +13,93 @@ export const versionDescription: INodeTypeDescription = {
defaults: {
name: 'Merge',
},
+ builderHint: {
+ searchHint:
+ 'Mode selection is the single most consequential decision on this node — the wrong mode silently drops or duplicates items rather than erroring. Pick by data shape: `append` to concatenate items from parallel branches; `combineByPosition` only when both branches emit the same number of items in the same order; `combineByFields` to join by a matching key (default; usually correct for "merge by ID"); `combineBySql` for >2 inputs or aggregation; `chooseBranch` to discard all but one input. Read each mode\'s @builderHint before picking.',
+ extraTypeDefContent: [
+ {
+ content: `
+
+const usersApi = node({
+ type: 'n8n-nodes-base.httpRequest',
+ version: 4.2,
+ config: { name: 'Fetch Users', parameters: { url: 'https://api.example.com/users' } }
+});
+
+const ordersApi = node({
+ type: 'n8n-nodes-base.httpRequest',
+ version: 4.2,
+ config: { name: 'Fetch Orders', parameters: { url: 'https://api.example.com/orders' } }
+});
+
+const mergeNode = merge({
+ type: 'n8n-nodes-base.merge',
+ version: 3.2,
+ config: {
+ name: 'Merge by ID',
+ parameters: {
+ mode: 'combine',
+ combineBy: 'combineByFields',
+ fieldsToMatchString: 'id',
+ joinMode: 'keepMatches',
+ outputDataFrom: 'both'
+ }
+ }
+});
+
+// Wire each upstream branch to a specific merge input slot.
+usersApi.to(mergeNode.input(0));
+ordersApi.to(mergeNode.input(1));
+
+
+
+const branchA = node({
+ type: 'n8n-nodes-base.set',
+ version: 3.4,
+ config: { name: 'Branch A', parameters: {} }
+});
+
+const branchB = node({
+ type: 'n8n-nodes-base.set',
+ version: 3.4,
+ config: { name: 'Branch B', parameters: {} }
+});
+
+const mergeNode = merge({
+ type: 'n8n-nodes-base.merge',
+ version: 3.2,
+ config: {
+ name: 'Append',
+ parameters: { mode: 'append', numberInputs: 2 }
+ }
+});
+
+branchA.to(mergeNode.input(0));
+branchB.to(mergeNode.input(1));
+
+
+
+const mergeNode = merge({
+ type: 'n8n-nodes-base.merge',
+ version: 3.2,
+ config: {
+ name: 'SQL Merge',
+ parameters: {
+ mode: 'combineBySql',
+ numberInputs: 3,
+ query: 'SELECT * FROM input1 LEFT JOIN input2 ON input1.id = input2.userId LEFT JOIN input3 ON input1.id = input3.userId'
+ }
+ }
+});
+
+input1Node.to(mergeNode.input(0));
+input2Node.to(mergeNode.input(1));
+input3Node.to(mergeNode.input(2));
+
+`,
+ },
+ ],
+ },
inputs: `={{(${configuredInputs})($parameter)}}`,
outputs: [NodeConnectionTypes.Main],
// If mode is chooseBranch data from both branches is required
diff --git a/packages/nodes-base/nodes/SplitInBatches/v3/SplitInBatchesV3.node.ts b/packages/nodes-base/nodes/SplitInBatches/v3/SplitInBatchesV3.node.ts
index e3234ccfc47..dd6adfac8d6 100644
--- a/packages/nodes-base/nodes/SplitInBatches/v3/SplitInBatchesV3.node.ts
+++ b/packages/nodes-base/nodes/SplitInBatches/v3/SplitInBatchesV3.node.ts
@@ -23,6 +23,30 @@ export class SplitInBatchesV3 implements INodeType {
outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
outputNames: ['done', 'loop'],
+ builderHint: {
+ searchHint:
+ "Loop pattern: connect splitInBatches → per-item work → back to splitInBatches via `nextBatch(splitInBatches)`. The `done` output fires automatically after all items are processed. Already no-ops on empty input — do NOT add an IF gate before it to check 'has items?'.",
+ extraTypeDefContent: [
+ {
+ content: `
+
+const sibNode = splitInBatches({
+ version: 3,
+ config: { name: 'Batch Process', parameters: { batchSize: 1 } }
+});
+
+export default workflow('id', 'name')
+ .add(startTrigger)
+ .to(fetchRecords)
+ .to(sibNode
+ .onDone(finalizeResults)
+ .onEachBatch(processRecord.to(nextBatch(sibNode)))
+ );
+
+`,
+ },
+ ],
+ },
properties: [
{
displayName:
diff --git a/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts b/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts
index 444b897520f..791c24d8d08 100644
--- a/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts
+++ b/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts
@@ -59,6 +59,52 @@ export class SwitchV3 implements INodeType {
},
inputs: [NodeConnectionTypes.Main],
outputs: `={{(${configuredOutputs})($parameter)}}`,
+ builderHint: {
+ extraTypeDefContent: [
+ {
+ displayOptions: { show: { mode: ['rules'] } },
+ content: `
+
+const routeByPriority = switchCase({
+ version: 3.2,
+ config: {
+ name: 'Route by Priority',
+ parameters: {
+ rules: {
+ values: [
+ {
+ 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'
+ }
+ }
+ ]
+ }
+ }
+ }
+});
+
+export default workflow('id', 'name')
+ .add(startTrigger)
+ .to(routeByPriority
+ .onCase('urgent', processUrgent.to(notifyTeam))
+ .onCase('normal', processNormal)
+ .onDefault(archive));
+
+`,
+ },
+ ],
+ },
properties: [
{
displayName: 'Mode',
@@ -128,6 +174,10 @@ export class SwitchV3 implements INodeType {
name: 'rules',
placeholder: 'Add Routing Rule',
type: 'fixedCollection',
+ builderHint: {
+ propertyHint:
+ "Use `rules.values` (NOT `rules.rules`). Each rule needs `outputKey` and a complete `conditions` object with these three sibling keys: `combinator` ('and' | 'or'), `conditions` (array of condition objects), `options` (`{ caseSensitive, leftValue, typeValidation }`). Same shape as IF. Each `outputKey` you define must be wired via `.onCase('')` to the intended downstream node — unwired cases silently drop their items.",
+ },
typeOptions: {
multipleValues: true,
sortable: true,
diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts
index d67eda7c55e..2c82f744a14 100644
--- a/packages/workflow/src/interfaces.ts
+++ b/packages/workflow/src/interfaces.ts
@@ -1908,6 +1908,16 @@ export interface INodePropertyCollection {
builderHint?: IParameterBuilderHint;
}
+/**
+ * One variation of `extraTypeDefContent`, optionally gated by `displayOptions`
+ * so per-mode / per-resource / per-operation examples land only in their
+ * corresponding narrowed type without cross-bleed.
+ */
+export interface IBuilderHintVariation {
+ content: string;
+ displayOptions?: IDisplayOptions;
+}
+
export interface IParameterBuilderHint {
propertyHint: string;
placeholderSupported?: boolean;
@@ -2555,6 +2565,14 @@ export interface IBuilderHint {
searchHint?: string;
/** Related nodes that work together with this node */
relatedNodes?: IRelatedNode[];
+ /**
+ * Multi-line content (typically code examples wrapped in `...`)
+ * emitted into the generated workflow-sdk `.d.ts` but NOT surfaced in
+ * `nodes(action="search")` results. Each variation may carry `displayOptions`
+ * so per-mode / per-resource / per-operation examples land only in their
+ * corresponding narrowed type.
+ */
+ extraTypeDefContent?: IBuilderHintVariation[];
}
export interface INodeTypeDescription extends INodeTypeBaseDescription {