Skip to main content
Glama

n8n-MCP

by 88-888
N8N_AI_WORKFLOW_BUILDER_ANALYSIS.md99.1 kB
# n8n AI Workflow Builder: Complete Technical Analysis **Version:** 1.114.0+ **Architecture:** LangGraph + LangChain + Claude Sonnet 4 **Type:** Enterprise Edition (`.ee`) **Repository:** https://github.com/n8n-io/n8n **Package:** `@n8n/ai-workflow-builder.ee` --- ## Table of Contents 1. [Executive Summary](#executive-summary) 2. [System Architecture](#system-architecture) 3. [Communication Flow](#communication-flow) 4. [Core Components](#core-components) 5. [The 7 Builder Tools](#the-7-builder-tools) 6. [Operations System](#operations-system) 7. [Design Patterns](#design-patterns) 8. [Prompt Engineering](#prompt-engineering) 9. [Performance & Optimization](#performance--optimization) 10. [Error Handling](#error-handling) 11. [Security & Validation](#security--validation) 12. [Best Practices](#best-practices) 13. [Implementation Details](#implementation-details) 14. [Appendix](#appendix) --- ## Executive Summary The n8n AI Workflow Builder is a sophisticated **text-to-workflow** system that enables users to create, modify, and manage n8n workflows using natural language. Built on Claude Sonnet 4, it implements a **7-tool architecture** with intelligent connection inference, parallel execution, and real-time streaming. ### Key Capabilities - **Natural Language Workflow Creation**: "Create a workflow that fetches weather data and sends it via email" - **Intelligent Node Connection**: Automatically infers connection types and corrects mistakes - **Parallel Tool Execution**: Multiple tools run simultaneously for maximum performance - **Real-time Streaming**: Progressive updates to the UI as workflows are built - **Context-Aware Configuration**: Uses workflow state and execution data for smart parameter updates ### Technology Stack ``` Frontend (n8n Editor UI) ↓ AI Workflow Builder Service (TypeScript) ↓ LangGraph State Machine ↓ Claude Sonnet 4 (via API Proxy) ↓ n8n Node Type System ``` ### Architecture Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ User Interface │ │ (Chat panel in n8n Editor) │ └─────────────────────┬───────────────────────────────────────┘ │ HTTP/SSE Streaming ↓ ┌─────────────────────────────────────────────────────────────┐ │ AI Workflow Builder Service │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ LangGraph State Machine │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ │ │ │ Agent │→ │ Tools │→ │ Process Ops Node │ │ │ │ │ └──────────┘ └──────────┘ └──────────────────┘ │ │ │ │ ↑ │ │ │ │ │ │ └──────────────┴──────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 7 Builder Tools │ │ │ │ • search_nodes • add_nodes │ │ │ │ • get_node_details • connect_nodes │ │ │ │ • update_node_parameters • remove_node │ │ │ │ • get_node_parameter │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Operations Processor │ │ │ │ (Applies queued mutations to workflow state) │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────┬───────────────────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────────────────────┐ │ AI Assistant SDK Proxy │ │ (Routes to Anthropic, handles auth, metering) │ └─────────────────────┬───────────────────────────────────────┘ │ ↓ Claude Sonnet 4 (claude-sonnet-4-20250514) ``` --- ## System Architecture ### Package Structure ``` packages/@n8n/ai-workflow-builder.ee/ ├── src/ │ ├── chains/ # LLM chains for specialized tasks │ │ ├── conversation-compact.ts │ │ ├── parameter-updater.ts │ │ ├── workflow-name.ts │ │ └── prompts/ │ │ ├── base/ # Core system prompts │ │ ├── examples/ # Node-specific examples │ │ ├── node-types/ │ │ └── parameter-types/ │ │ │ ├── tools/ # The 7 builder tools │ │ ├── add-node.tool.ts │ │ ├── connect-nodes.tool.ts │ │ ├── get-node-parameter.tool.ts │ │ ├── node-details.tool.ts │ │ ├── node-search.tool.ts │ │ ├── remove-node.tool.ts │ │ ├── update-node-parameters.tool.ts │ │ ├── builder-tools.ts # Tool factory │ │ ├── engines/ # Pure business logic │ │ ├── helpers/ # Shared utilities │ │ ├── prompts/ # Tool-specific prompts │ │ └── utils/ # Data transformation │ │ │ ├── database/ │ ├── evaluations/ # Testing framework │ ├── errors/ │ ├── types/ │ ├── utils/ │ │ ├── operations-processor.ts # State mutation engine │ │ ├── stream-processor.ts # Real-time updates │ │ ├── tool-executor.ts # Parallel execution │ │ └── trim-workflow-context.ts # Token optimization │ │ │ ├── ai-workflow-builder-agent.service.ts # Main service │ ├── session-manager.service.ts │ ├── workflow-builder-agent.ts # LangGraph workflow │ ├── workflow-state.ts # State definition │ ├── llm-config.ts # Model configurations │ └── constants.ts │ ├── evaluations/ # Evaluation framework └── test/ ``` ### Design Philosophy The architecture follows these core principles: 1. **Separation of Concerns**: Tools, helpers, engines, and state management are cleanly separated 2. **Immutable State**: Operations pattern ensures state is never mutated directly 3. **Progressive Disclosure**: Tools guide AI through increasing complexity 4. **Error Resilience**: Multiple validation layers with graceful degradation 5. **Token Efficiency**: Aggressive optimization for LLM context window 6. **Real-time UX**: Streaming updates create transparency 7. **Parallel Execution**: All tools support concurrent operation --- ## Communication Flow ### High-Level Flow ``` ┌──────────────────────────────────────────────────────────────┐ │ 1. USER INPUT │ │ User: "Create a workflow that fetches weather data" │ └────────────────────────┬─────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ 2. FRONTEND REQUEST │ │ POST /api/ai-workflow-builder/chat │ │ { │ │ message: "Create a workflow...", │ │ workflowContext: { currentWorkflow, executionData } │ │ } │ └────────────────────────┬─────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ 3. SERVICE INITIALIZATION │ │ AiWorkflowBuilderService.chat() │ │ ├─ Setup Claude Sonnet 4 via AI Assistant SDK │ │ ├─ Initialize session checkpointer (MemorySaver) │ │ ├─ Create WorkflowBuilderAgent with 7 tools │ │ └─ Start LangGraph stream │ └────────────────────────┬─────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ 4. LANGGRAPH STATE MACHINE │ │ │ │ START │ │ ↓ │ │ ┌─────────────────┐ │ │ │ shouldModifyState? │ │ └────┬───────┬────┘ │ │ │ │ │ │ [compact] [create_name] [agent] │ │ │ │ ↓ │ │ │ │ ┌──────────┐ │ │ │ │ │ Agent │ (LLM call) │ │ │ │ │ Node │ │ │ │ │ └────┬─────┘ │ │ │ │ │ │ │ │ │ shouldContinue? │ │ │ │ │ │ │ │ │ [tools] [END] │ │ │ │ │ │ │ │ │ ┌──────────┐ │ │ │ │ │ Tools │ (parallel execution) │ │ │ │ │ Node │ │ │ │ │ └────┬─────┘ │ │ │ │ │ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ Process Ops Node │ │ │ │ │ │ (Apply mutations) │ │ │ │ │ └────┬─────────────┘ │ │ │ │ │ │ │ │ └──────────────┴──────────┐ │ │ └──────────────────────────────────┘ │ │ ↓ │ │ Back to Agent │ └────────────────────────┬─────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ 5. TOOL EXECUTION (Parallel) │ │ │ │ Promise.all([ │ │ search_nodes({queries: [...]}), │ │ get_node_details({nodeName: "..."}), │ │ // More tools... │ │ ]) │ │ │ │ Each tool returns: │ │ { │ │ messages: [ToolMessage], │ │ workflowOperations: [Operation] │ │ } │ └────────────────────────┬─────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ 6. OPERATIONS PROCESSING │ │ │ │ Collected operations from all tools: │ │ [ │ │ { type: 'addNodes', nodes: [...] }, │ │ { type: 'mergeConnections', connections: {...} }, │ │ { type: 'updateNode', nodeId, updates: {...} } │ │ ] │ │ │ │ applyOperations(currentWorkflow, operations) │ │ → Returns updated workflow JSON │ └────────────────────────┬─────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ 7. STREAMING RESPONSE │ │ │ │ Stream chunks to frontend: │ │ { │ │ messages: [{ │ │ role: "assistant", │ │ type: "tool" | "message" | "workflow-updated", │ │ text: "Adding HTTP Request node..." │ │ }] │ │ } │ └────────────────────────┬─────────────────────────────────────┘ │ ↓ ┌──────────────────────────────────────────────────────────────┐ │ 8. FRONTEND UPDATE │ │ - Updates canvas with new nodes │ │ - Shows progress messages in chat │ │ - Enables "Save Workflow" button │ │ - User saves via standard n8n API (POST /api/workflows) │ └──────────────────────────────────────────────────────────────┘ ``` ### Request/Response Lifecycle #### Initial Request ```typescript // Frontend sends POST /api/ai-workflow-builder/chat { message: "Create a workflow that sends daily weather reports", workflowContext: { currentWorkflow: { nodes: [], connections: {}, name: "" }, executionSchema: [], executionData: null } } ``` #### Service Processing ```typescript // AiWorkflowBuilderService.chat() async *chat(payload: ChatPayload, user: IUser, abortSignal?: AbortSignal) { // 1. Setup models (Claude via AI Assistant SDK) const { anthropicClaude, tracingClient } = await this.setupModels(user); // 2. Create agent with tools const agent = new WorkflowBuilderAgent({ parsedNodeTypes: this.parsedNodeTypes, llmSimpleTask: anthropicClaude, llmComplexTask: anthropicClaude, checkpointer: this.sessionManager.getCheckpointer(), tracer: tracingClient, instanceUrl: this.instanceUrl }); // 3. Stream outputs for await (const output of agent.chat(payload, user.id, abortSignal)) { yield output; // Streams to frontend } } ``` #### LangGraph Execution ```typescript // WorkflowBuilderAgent.chat() async *chat(payload: ChatPayload, userId: string, abortSignal?: AbortSignal) { const workflow = this.createWorkflow(); // LangGraph const config: RunnableConfig = { configurable: { thread_id: `workflow-${workflowId}-user-${userId}` }, signal: abortSignal }; const stream = workflow.stream( { messages: [new HumanMessage(payload.message)] }, { ...config, streamMode: ['updates', 'custom'] as const } ); // Process and yield formatted chunks for await (const output of createStreamProcessor(stream)) { yield output; } } ``` #### Tool Parallel Execution ```typescript // executeToolsInParallel() const toolResults = await Promise.all( aiMessage.tool_calls.map(async (toolCall) => { const tool = toolMap.get(toolCall.name); return await tool.invoke(toolCall.args); }) ); // Collect all operations const allOperations: WorkflowOperation[] = []; for (const update of stateUpdates) { if (update.workflowOperations) { allOperations.push(...update.workflowOperations); } } return { messages: allMessages, workflowOperations: allOperations }; ``` #### Operations Processing ```typescript // processOperations() export function processOperations(state: WorkflowState) { const { workflowJSON, workflowOperations } = state; if (!workflowOperations || workflowOperations.length === 0) { return {}; } // Apply all operations sequentially const newWorkflow = applyOperations(workflowJSON, workflowOperations); return { workflowJSON: newWorkflow, workflowOperations: null // Clear queue }; } ``` #### Streaming Output ```typescript // Stream processor yields chunks { messages: [{ role: "assistant", type: "tool", toolName: "add_nodes", displayTitle: "Adding HTTP Request node", status: "in_progress" }] } // Later... { messages: [{ role: "assistant", type: "workflow-updated", codeSnippet: JSON.stringify(updatedWorkflow, null, 2) }] } // Finally... { messages: [{ role: "assistant", type: "message", text: "**⚙️ How to Setup**\n1. Configure API credentials\n..." }] } ``` --- ## Core Components ### 1. AI Assistant SDK Integration The service communicates with Claude through n8n's AI Assistant SDK proxy. ```typescript interface AiAssistantClient { // Authentication getBuilderApiProxyToken(user: IUser): Promise<{ tokenType: string, accessToken: string }>; // API Proxy getApiProxyBaseUrl(): string; // Returns: "https://ai-assistant.n8n.io/api/v1" // Metering markBuilderSuccess(user: IUser, authHeaders): Promise<{ creditsQuota: number, creditsClaimed: number }>; getBuilderInstanceCredits(user: IUser): Promise<{ creditsQuota: number, creditsClaimed: number }>; } ``` **API Routing:** ```typescript // Anthropic requests baseUrl + '/anthropic' // Routes to: https://ai-assistant.n8n.io/api/v1/anthropic // Langsmith tracing baseUrl + '/langsmith' // Routes to: https://ai-assistant.n8n.io/api/v1/langsmith ``` **Authentication Flow:** ``` 1. User makes request 2. Service calls getBuilderApiProxyToken(user) 3. SDK returns JWT access token 4. Service adds Authorization header to all LLM requests 5. Proxy validates token and routes to Anthropic 6. Response streams back through proxy ``` ### 2. LangGraph State Machine The workflow is a graph of nodes that process the conversation. ```typescript const workflow = new StateGraph(WorkflowState) .addNode('agent', callModel) .addNode('tools', customToolExecutor) .addNode('process_operations', processOperations) .addNode('delete_messages', deleteMessages) .addNode('compact_messages', compactSession) .addNode('auto_compact_messages', compactSession) .addNode('create_workflow_name', createWorkflowName) // Conditional routing .addConditionalEdges('__start__', shouldModifyState, { 'compact_messages': 'compact_messages', 'auto_compact_messages': 'auto_compact_messages', 'delete_messages': 'delete_messages', 'create_workflow_name': 'create_workflow_name', 'agent': 'agent' }) .addConditionalEdges('agent', shouldContinue, { 'tools': 'tools', [END]: END }) .addEdge('tools', 'process_operations') .addEdge('process_operations', 'agent') .addEdge('compact_messages', 'agent') .addEdge('auto_compact_messages', 'agent') .addEdge('delete_messages', END) .addEdge('create_workflow_name', 'agent'); ``` **Node Responsibilities:** | Node | Purpose | When Triggered | |------|---------|----------------| | `agent` | LLM invocation with tool binding | Default path | | `tools` | Parallel tool execution | When AI returns tool calls | | `process_operations` | Apply queued mutations | After tools complete | | `compact_messages` | Compress conversation history | User sends `/compact` | | `auto_compact_messages` | Automatic history compression | Token usage > 20K | | `delete_messages` | Clear conversation | User sends `/clear` | | `create_workflow_name` | Generate workflow name | First message, empty workflow | **Conditional Logic:** ```typescript function shouldModifyState(state: WorkflowState) { const lastMessage = state.messages.findLast(m => m instanceof HumanMessage); if (lastMessage.content === '/compact') return 'compact_messages'; if (lastMessage.content === '/clear') return 'delete_messages'; // Auto-generate name for new workflows if (state.workflowContext?.currentWorkflow?.nodes?.length === 0 && state.messages.length === 1) { return 'create_workflow_name'; } // Auto-compact when token usage exceeds threshold if (shouldAutoCompact(state)) { return 'auto_compact_messages'; } return 'agent'; } function shouldContinue(state: WorkflowState) { const lastMessage = state.messages[state.messages.length - 1]; if (lastMessage.tool_calls?.length) { return 'tools'; } // Success callback if (this.onGenerationSuccess) { void Promise.resolve(this.onGenerationSuccess()); } return END; } ``` ### 3. State Management ```typescript export const WorkflowState = Annotation.Root({ // Conversation history messages: Annotation<BaseMessage[]>({ reducer: messagesStateReducer, default: () => [] }), // Current workflow JSON workflowJSON: Annotation<SimpleWorkflow>({ reducer: (x, y) => y ?? x, default: () => ({ nodes: [], connections: {}, name: '' }) }), // Queued operations workflowOperations: Annotation<WorkflowOperation[] | null>({ reducer: operationsReducer, // Accumulates operations default: () => [] }), // Execution context workflowContext: Annotation<ChatPayload['workflowContext']>({ reducer: (x, y) => y ?? x }), // Compressed history previousSummary: Annotation<string>({ reducer: (x, y) => y ?? x, default: () => 'EMPTY' }) }); ``` **Operations Reducer:** ```typescript function operationsReducer( current: WorkflowOperation[], update: WorkflowOperation[] ): WorkflowOperation[] { if (update === null) return []; // Clear if (!update || update.length === 0) return current ?? []; // Clear operations reset everything if (update.some(op => op.type === 'clear')) { return update.filter(op => op.type === 'clear').slice(-1); } // Otherwise, accumulate return [...(current ?? []), ...update]; } ``` ### 4. Session Management ```typescript class SessionManagerService { private checkpointer: MemorySaver; // Generate unique thread ID per workflow+user static generateThreadId(workflowId?: string, userId?: string): string { return workflowId ? `workflow-${workflowId}-user-${userId ?? Date.now()}` : crypto.randomUUID(); } // Retrieve conversation history async getSessions(workflowId: string, userId: string) { const threadId = this.generateThreadId(workflowId, userId); const checkpoint = await this.checkpointer.getTuple({ configurable: { thread_id: threadId } }); if (checkpoint?.checkpoint) { return { sessionId: threadId, messages: formatMessages(checkpoint.checkpoint.channel_values?.messages), lastUpdated: checkpoint.checkpoint.ts }; } return { sessions: [] }; } } ``` **Checkpointing:** - Uses LangGraph's `MemorySaver` for in-memory persistence - State survives between chat turns within same session - Thread ID binds conversation to specific workflow+user - No database persistence (ephemeral, cloud-only feature) ### 5. Token Management ```typescript // Constants const MAX_TOTAL_TOKENS = 200_000; // Claude's context window const MAX_OUTPUT_TOKENS = 16_000; // Reserved for response const MAX_INPUT_TOKENS = 184_000; // 200k - 16k - 10k buffer const DEFAULT_AUTO_COMPACT_THRESHOLD = 20_000; // Auto-compress trigger const MAX_WORKFLOW_LENGTH_TOKENS = 30_000; // Workflow JSON limit const MAX_PARAMETER_VALUE_LENGTH = 30_000; // Single parameter limit // Token estimation function estimateTokenCountFromMessages(messages: BaseMessage[]): number { const totalChars = messages.reduce((sum, msg) => { return sum + JSON.stringify(msg.content).length; }, 0); return Math.ceil(totalChars / AVG_CHARS_PER_TOKEN_ANTHROPIC); } // Workflow trimming function trimWorkflowJSON(workflow: SimpleWorkflow): SimpleWorkflow { const estimatedTokens = estimateTokens(JSON.stringify(workflow)); if (estimatedTokens > MAX_WORKFLOW_LENGTH_TOKENS) { return { ...workflow, nodes: workflow.nodes.map(node => ({ ...node, parameters: trimLargeParameters(node.parameters) })) }; } return workflow; } ``` **Token Budget Allocation:** ``` Total Context Window: 200,000 tokens ├─ System Prompt: ~8,000 tokens (cached) ├─ Node Definitions: ~5,000 tokens (cached, varies) ├─ Workflow JSON: Up to 30,000 tokens (trimmed) ├─ Execution Data: ~2,000 tokens ├─ Previous Summary: ~1,000 tokens (after compact) ├─ Conversation History: ~20,000 tokens (trigger compact) └─ Reserved for Output: 16,000 tokens Total Input: ~184,000 tokens maximum ``` --- ## The 7 Builder Tools ### Tool 1: search_nodes **Purpose:** Multi-modal search for discovering available node types. **Schema:** ```typescript { queries: Array<{ queryType: 'name' | 'subNodeSearch', query?: string, connectionType?: NodeConnectionType }> } ``` **Examples:** ```typescript // Name-based search { queries: [{ queryType: "name", query: "http" }] } // Sub-node search { queries: [{ queryType: "subNodeSearch", connectionType: "ai_languageModel" }] } // Combined search { queries: [ { queryType: "name", query: "gmail" }, { queryType: "subNodeSearch", connectionType: "ai_tool", query: "calculator" } ] } ``` **Search Algorithm:** ```typescript class NodeSearchEngine { searchByName(query: string, limit: number = 5): NodeSearchResult[] { const normalizedQuery = query.toLowerCase(); const results: NodeSearchResult[] = []; for (const nodeType of this.nodeTypes) { let score = 0; // Exact matches (highest weight) if (nodeType.name.toLowerCase() === normalizedQuery) { score += SCORE_WEIGHTS.NAME_EXACT; // 20 } if (nodeType.displayName.toLowerCase() === normalizedQuery) { score += SCORE_WEIGHTS.DISPLAY_NAME_EXACT; // 15 } // Partial matches if (nodeType.name.toLowerCase().includes(normalizedQuery)) { score += SCORE_WEIGHTS.NAME_CONTAINS; // 10 } if (nodeType.displayName.toLowerCase().includes(normalizedQuery)) { score += SCORE_WEIGHTS.DISPLAY_NAME_CONTAINS; // 8 } if (nodeType.codex?.alias?.some(a => a.toLowerCase().includes(normalizedQuery))) { score += SCORE_WEIGHTS.ALIAS_CONTAINS; // 8 } if (nodeType.description?.toLowerCase().includes(normalizedQuery)) { score += SCORE_WEIGHTS.DESCRIPTION_CONTAINS; // 5 } if (score > 0) { results.push({ ...nodeType, score }); } } return results.sort((a, b) => b.score - a.score).slice(0, limit); } searchByConnectionType( connectionType: NodeConnectionType, limit: number = 5, nameFilter?: string ): NodeSearchResult[] { const results: NodeSearchResult[] = []; for (const nodeType of this.nodeTypes) { let score = 0; // Check if node outputs this connection type if (Array.isArray(nodeType.outputs)) { if (nodeType.outputs.includes(connectionType)) { score += SCORE_WEIGHTS.CONNECTION_EXACT; // 100 } } else if (typeof nodeType.outputs === 'string') { if (nodeType.outputs.includes(connectionType)) { score += SCORE_WEIGHTS.CONNECTION_IN_EXPRESSION; // 50 } } // Apply optional name filter if (nameFilter && score > 0) { const nameScore = this.calculateNameScore(nodeType, nameFilter); score += nameScore; } if (score > 0) { results.push({ ...nodeType, score }); } } return results.sort((a, b) => b.score - a.score).slice(0, limit); } } ``` **Output Format:** ```xml Found 3 nodes matching "http": <node> <node_name>n8n-nodes-base.httpRequest</node_name> <node_description>Makes HTTP requests to URLs</node_description> <node_inputs>["main"]</node_inputs> <node_outputs>["main"]</node_outputs> </node> <node> <node_name>n8n-nodes-base.httpBinTrigger</node_name> <node_description>Triggers on HTTP webhooks</node_description> <node_inputs>[]</node_inputs> <node_outputs>["main"]</node_outputs> </node> ``` **Performance:** - **Latency:** <50ms - **Parallelizable:** Yes - **LLM Calls:** 0 --- ### Tool 2: get_node_details **Purpose:** Retrieve comprehensive node specifications for understanding inputs, outputs, and parameters. **Schema:** ```typescript { nodeName: string, // Full type: "n8n-nodes-base.httpRequest" withParameters?: boolean, // Default: false withConnections?: boolean // Default: true } ``` **Examples:** ```typescript // Fast: connections only { nodeName: "n8n-nodes-base.httpRequest", withConnections: true } // Complete: including parameters { nodeName: "n8n-nodes-base.set", withParameters: true, withConnections: true } ``` **Output Format:** ```xml <node_details> <name>n8n-nodes-base.httpRequest</name> <display_name>HTTP Request</display_name> <description>Makes HTTP requests to retrieve data</description> <subtitle>={{ $parameter["method"] + ": " + $parameter["url"] }}</subtitle> <!-- Only if withParameters: true --> <properties> [ { "name": "method", "type": "options", "options": [ { "name": "GET", "value": "GET" }, { "name": "POST", "value": "POST" }, ... ], "default": "GET" }, { "name": "url", "type": "string", "default": "", "required": true }, ... ] </properties> <connections> <input>main</input> <output>main</output> </connections> </node_details> ``` **Usage Pattern:** ```typescript // AI workflow: Discovery → Details → Addition 1. search_nodes({queries: [{queryType: "name", query: "http"}]}) → Returns: n8n-nodes-base.httpRequest 2. get_node_details({nodeName: "n8n-nodes-base.httpRequest"}) → Understands: inputs=["main"], outputs=["main"] 3. add_nodes({ nodeType: "n8n-nodes-base.httpRequest", connectionParametersReasoning: "HTTP Request has static connections", connectionParameters: {} }) ``` **Performance:** - **Latency:** <50ms - **Parallelizable:** Yes (fetch multiple node types) - **LLM Calls:** 0 --- ### Tool 3: add_nodes **Purpose:** Create nodes with automatic positioning and connection parameter reasoning. **Schema:** ```typescript { nodeType: string, name: string, connectionParametersReasoning: string, // ⭐ REQUIRED connectionParameters: object } ``` **Connection Parameters by Node Type:** ```typescript // Vector Store - Dynamic inputs based on mode { nodeType: "@n8n/n8n-nodes-langchain.vectorStoreInMemory", name: "Store Embeddings", connectionParametersReasoning: "Vector Store mode determines inputs. Using 'insert' to accept document loader connections", connectionParameters: { mode: "insert" // Enables ai_document input } } // AI Agent - Output parser support { nodeType: "@n8n/n8n-nodes-langchain.agent", name: "Research Agent", connectionParametersReasoning: "AI Agent needs output parser for structured responses", connectionParameters: { hasOutputParser: true // Adds ai_outputParser input } } // Document Loader - Text splitting mode { nodeType: "@n8n/n8n-nodes-langchain.documentDefaultDataLoader", name: "PDF Loader", connectionParametersReasoning: "Document Loader with custom text splitting to accept splitter connections", connectionParameters: { textSplittingMode: "custom", // Enables ai_textSplitter input dataType: "binary" // Process files instead of JSON } } // HTTP Request - Static connections { nodeType: "n8n-nodes-base.httpRequest", name: "Fetch Weather Data", connectionParametersReasoning: "HTTP Request has static inputs/outputs, no special parameters needed", connectionParameters: {} } ``` **Node Creation Pipeline:** ```typescript function createNode( nodeType: INodeTypeDescription, customName: string, existingNodes: INode[], nodeTypes: INodeTypeDescription[], connectionParameters?: INodeParameters ): INode { // 1. Generate unique name const baseName = customName ?? nodeType.defaults?.name ?? nodeType.displayName; const uniqueName = generateUniqueName(baseName, existingNodes); // "HTTP Request" → "HTTP Request 2" if collision // 2. Calculate position const isSubNodeType = isSubNode(nodeType); const position = calculateNodePosition(existingNodes, isSubNodeType, nodeTypes); // Sub-nodes: [x, y + 200] (below main nodes) // Main nodes: [lastX + 240, y] (flow left-to-right) // 3. Create instance return { id: crypto.randomUUID(), name: uniqueName, type: nodeType.name, typeVersion: nodeType.version, position, parameters: { ...nodeType.defaults?.parameters, ...connectionParameters // Override defaults } }; } ``` **Positioning Algorithm:** ```typescript function calculateNodePosition( existingNodes: INode[], isSubNode: boolean, nodeTypes: INodeTypeDescription[] ): [number, number] { if (existingNodes.length === 0) { return [240, 300]; // First node position } if (isSubNode) { // Sub-nodes positioned below main flow const mainNodes = existingNodes.filter(n => { const type = nodeTypes.find(nt => nt.name === n.type); return !isSubNode(type); }); const avgX = mainNodes.reduce((sum, n) => sum + n.position[0], 0) / mainNodes.length; return [avgX, 600]; // Below main nodes } // Main nodes: continue the flow const lastNode = existingNodes[existingNodes.length - 1]; return [lastNode.position[0] + 240, lastNode.position[1]]; } ``` **Operation Result:** ```typescript { workflowOperations: [{ type: 'addNodes', nodes: [{ id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", name: "Fetch Weather Data", type: "n8n-nodes-base.httpRequest", typeVersion: 4.2, position: [240, 300], parameters: {} // connectionParameters merged with defaults }] }], messages: [ new ToolMessage({ content: 'Successfully added "Fetch Weather Data" (HTTP Request) with ID a1b2c3d4...', tool_call_id: "call_xyz" }) ] } ``` **Performance:** - **Latency:** <100ms - **Parallelizable:** Yes (multiple add_nodes calls) - **LLM Calls:** 0 --- ### Tool 4: connect_nodes **Purpose:** Establish connections with automatic type inference and direction correction. **Schema:** ```typescript { sourceNodeId: string, // For ai_*: should be sub-node targetNodeId: string, // For ai_*: should be main node sourceOutputIndex?: number, // Default: 0 targetInputIndex?: number // Default: 0 } ``` **Connection Type Inference:** ```typescript function inferConnectionType( sourceNode: INode, targetNode: INode, sourceNodeType: INodeTypeDescription, targetNodeType: INodeTypeDescription ): InferConnectionTypeResult { // 1. Extract possible output types from source const sourceOutputTypes = extractConnectionTypes(sourceNodeType.outputs); // ["main", "ai_tool"] // 2. Extract possible input types from target const targetInputTypes = extractConnectionTypes(targetNodeType.inputs); // ["main", "ai_tool", "ai_languageModel"] // 3. Find intersection const compatibleTypes = sourceOutputTypes.filter(type => targetInputTypes.includes(type) ); if (compatibleTypes.length === 0) { return { error: "No compatible connection types found", possibleTypes: { source: sourceOutputTypes, target: targetInputTypes } }; } if (compatibleTypes.length > 1) { return { error: "Multiple connection types possible. Please specify.", possibleTypes: compatibleTypes }; } const connectionType = compatibleTypes[0]; // 4. For AI connections, validate sub-node is source if (connectionType.startsWith('ai_')) { const sourceIsSubNode = isSubNode(sourceNodeType, sourceNode); const targetIsSubNode = isSubNode(targetNodeType, targetNode); if (!sourceIsSubNode && !targetIsSubNode) { return { error: "AI connections require a sub-node" }; } if (targetIsSubNode && !sourceIsSubNode) { // Wrong direction! Swap them return { connectionType, requiresSwap: true }; } } return { connectionType }; } ``` **Expression Parsing:** ```typescript function extractConnectionTypesFromExpression(expression: string): string[] { const types = new Set<string>(); // Pattern 1: type: "ai_tool" const pattern1 = /type\s*:\s*["']([^"']+)["']/g; // Pattern 2: type: NodeConnectionTypes.AiTool const pattern2 = /type\s*:\s*NodeConnectionTypes\.(\w+)/g; // Pattern 3: ["main", "ai_tool"] const pattern3 = /\[\s*["'](\w+)["']/g; // Apply all patterns for (const pattern of [pattern1, pattern2, pattern3]) { let match; while ((match = pattern.exec(expression)) !== null) { types.add(match[1]); } } return Array.from(types); } ``` **Auto-Correction Example:** ```typescript // User incorrectly specifies: connect_nodes({ sourceNodeId: "ai-agent-123", // Main node targetNodeId: "openai-model-456" // Sub-node }) // Tool detects: sourceIsSubNode = false targetIsSubNode = true connectionType = "ai_languageModel" // Tool auto-swaps: actualSource = "openai-model-456" // Sub-node becomes source actualTarget = "ai-agent-123" // Main node becomes target swapped = true // Creates correct connection: { "OpenAI Chat Model": { "ai_languageModel": [[{ node: "AI Agent", type: "ai_languageModel", index: 0 }]] } } ``` **Validation:** ```typescript function validateConnection( sourceNode: INode, targetNode: INode, connectionType: string, nodeTypes: INodeTypeDescription[] ): ConnectionValidationResult { const sourceType = findNodeType(sourceNode.type, nodeTypes); const targetType = findNodeType(targetNode.type, nodeTypes); // Validate source has output type if (!nodeHasOutputType(sourceType, connectionType)) { return { valid: false, error: `Source node "${sourceNode.name}" doesn't output ${connectionType}` }; } // Validate target accepts input type if (!nodeAcceptsInputType(targetType, connectionType)) { return { valid: false, error: `Target node "${targetNode.name}" doesn't accept ${connectionType}` }; } return { valid: true }; } ``` **Operation Result:** ```typescript { workflowOperations: [{ type: 'mergeConnections', connections: { "OpenAI Chat Model": { "ai_languageModel": [[{ node: "AI Agent", type: "ai_languageModel", index: 0 }]] } } }], messages: [ new ToolMessage({ content: 'Connected "OpenAI Chat Model" to "AI Agent" via ai_languageModel (swapped for correct direction)', tool_call_id: "call_abc" }) ] } ``` **Performance:** - **Latency:** <100ms - **Parallelizable:** Yes (multiple connections) - **LLM Calls:** 0 - **Complexity:** Highest (inference + validation + auto-correction) --- ### Tool 5: update_node_parameters **Purpose:** Configure node parameters using natural language via nested LLM chain. **Schema:** ```typescript { nodeId: string, changes: string[] // Natural language instructions } ``` **Examples:** ```typescript // HTTP Request configuration { nodeId: "http-node-123", changes: [ "Set the URL to https://api.weather.com/v1/forecast", "Set method to POST", "Add header Content-Type with value application/json", "Set body to { city: {{ $json.city }} }" ] } // Set node configuration { nodeId: "set-node-456", changes: [ "Add field 'status' with value 'processed'", "Add field 'timestamp' with current date", "Add field 'userId' from previous HTTP Request node" ] } // Tool node with $fromAI { nodeId: "gmail-tool-789", changes: [ "Set sendTo to {{ $fromAI('to') }}", "Set subject to {{ $fromAI('subject') }}", "Set message to {{ $fromAI('message_html') }}" ] } ``` **Processing Pipeline:** ```typescript async function processParameterUpdates( node: INode, nodeType: INodeTypeDescription, nodeId: string, changes: string[], state: WorkflowState, llm: BaseChatModel ): Promise<INodeParameters> { // 1. Extract current parameters const currentParameters = node.parameters; // 2. Build dynamic prompt const promptBuilder = new ParameterUpdatePromptBuilder(); const systemPrompt = promptBuilder.buildSystemPrompt({ nodeType: node.type, nodeDefinition: nodeType, requestedChanges: changes, hasResourceLocatorParams: promptBuilder.hasResourceLocatorParameters(nodeType) }); // 3. Create LLM chain with structured output const parametersSchema = z.object({ parameters: z.object({}).passthrough() }); const chain = createParameterUpdaterChain(llm, systemPrompt); // 4. Invoke LLM const result = await chain.invoke({ workflow_json: trimWorkflowJSON(state.workflowJSON), execution_data: state.workflowContext?.executionData, execution_schema: state.workflowContext?.executionSchema, node_id: nodeId, node_name: node.name, node_type: node.type, current_parameters: JSON.stringify(currentParameters, null, 2), node_definition: JSON.stringify(nodeType.properties, null, 2), changes: formatChangesForPrompt(changes) }); // 5. Fix expression prefixes const fixedParameters = fixExpressionPrefixes(result.parameters); return fixedParameters; } ``` **Dynamic Prompt Building:** ```typescript class ParameterUpdatePromptBuilder { buildSystemPrompt(context: { nodeType: string, nodeDefinition: INodeTypeDescription, requestedChanges: string[], hasResourceLocatorParams: boolean }): string { let prompt = CORE_INSTRUCTIONS; // Add node-type-specific examples const nodeCategory = this.getNodeTypeCategory(context.nodeType); if (nodeCategory === 'set') { prompt += SET_NODE_EXAMPLES; } else if (nodeCategory === 'if') { prompt += IF_NODE_EXAMPLES; } else if (nodeCategory === 'httpRequest') { prompt += HTTP_REQUEST_EXAMPLES; } else if (nodeCategory === 'tool') { prompt += TOOL_NODE_EXAMPLES; prompt += FROMAIEXPRESSIONS; } // Add resource locator examples if needed if (context.hasResourceLocatorParams) { prompt += RESOURCE_LOCATOR_EXAMPLES; } // Add expression rules if text fields present if (this.hasTextFields(context.nodeDefinition)) { prompt += EXPRESSION_RULES; } prompt += OUTPUT_FORMAT; return prompt; } getNodeTypeCategory(nodeType: string): string { if (nodeType.includes('.set')) return 'set'; if (nodeType.includes('.if')) return 'if'; if (nodeType.includes('.httpRequest')) return 'httpRequest'; if (nodeType.endsWith('Tool')) return 'tool'; return 'generic'; } } ``` **Expression Fixing:** ```typescript function fixExpressionPrefixes(parameters: any): any { if (typeof parameters === 'string') { // Fix common mistakes: // "{{ $json.field }}" → "={{ $json.field }}" // "{ $json.field }" → "={{ $json.field }}" if (parameters.match(/^\s*\{\{.*\}\}\s*$/)) { // Has {{ }} but missing = return '=' + parameters; } if (parameters.match(/^\s*\{[^{].*\}\s*$/)) { // Has single { } - should be {{ }} return '=' + parameters.replace(/^\s*\{/, '{{').replace(/\}\s*$/, '}}'); } return parameters; } if (Array.isArray(parameters)) { return parameters.map(fixExpressionPrefixes); } if (typeof parameters === 'object' && parameters !== null) { const fixed: any = {}; for (const [key, value] of Object.entries(parameters)) { fixed[key] = fixExpressionPrefixes(value); } return fixed; } return parameters; } ``` **Example Prompts:** **Set Node:** ``` CORE_INSTRUCTIONS: You are an expert n8n workflow architect who updates node parameters... SET_NODE_EXAMPLES: ### Example 1: Simple String Assignment Current Parameters: {} Requested Changes: Set message to "Hello World" Expected Output: { "parameters": { "assignments": { "assignments": [{ "id": "id-1", "name": "message", "value": "Hello World", "type": "string" }] } } } ... ``` **Tool Node:** ``` CORE_INSTRUCTIONS: ... TOOL_NODE_EXAMPLES: ### Example 1: Gmail Tool - Send Email with AI Current Parameters: {} Requested Changes: Let AI determine recipient, subject, and message Expected Output: { "parameters": { "sendTo": "={{ $fromAI('to') }}", "subject": "={{ $fromAI('subject') }}", "message": "={{ $fromAI('message_html') }}" } } ... FROMAIEXPRESSIONS: ## CRITICAL: $fromAI Expression Support Tool nodes support special $fromAI expressions that allow AI to dynamically fill parameters... ``` **Operation Result:** ```typescript { workflowOperations: [{ type: 'updateNode', nodeId: "http-node-123", updates: { parameters: { method: "POST", url: "https://api.weather.com/v1/forecast", sendHeaders: true, headerParameters: { parameters: [{ name: "Content-Type", value: "application/json" }] }, sendBody: true, bodyParameters: { parameters: [{ name: "city", value: "={{ $json.city }}" }] } } } }], messages: [ new ToolMessage({ content: 'Successfully updated parameters for node "HTTP Request":\n- Set URL to https://api.weather.com...', tool_call_id: "call_def" }) ] } ``` **Performance:** - **Latency:** 2-5 seconds (LLM call) - **Parallelizable:** Yes (different nodes) - **LLM Calls:** 1 per invocation - **Token Cost:** 3,000-8,000 tokens - **Caching:** System prompt and node definition cached --- ### Tool 6: remove_node **Purpose:** Delete nodes and automatically clean up connections. **Schema:** ```typescript { nodeId: string } ``` **Deletion Process:** ```typescript function removeNode(nodeId: string, workflow: SimpleWorkflow) { // 1. Count connections to be removed let connectionsRemoved = 0; // Outgoing connections if (workflow.connections[nodeId]) { for (const outputs of Object.values(workflow.connections[nodeId])) { if (Array.isArray(outputs)) { for (const conns of outputs) { connectionsRemoved += conns.length; } } } } // Incoming connections for (const [sourceId, nodeConns] of Object.entries(workflow.connections)) { for (const outputs of Object.values(nodeConns)) { if (Array.isArray(outputs)) { for (const conns of outputs) { connectionsRemoved += conns.filter(c => c.node === nodeId).length; } } } } return { connectionsRemoved }; } ``` **Operation Processing:** ```typescript // In operations-processor.ts case 'removeNode': { const nodesToRemove = new Set(operation.nodeIds); // Filter out nodes result.nodes = result.nodes.filter(n => !nodesToRemove.has(n.id)); // Clean connections const cleanedConnections: IConnections = {}; for (const [sourceId, nodeConns] of Object.entries(result.connections)) { // Skip if source is removed if (nodesToRemove.has(sourceId)) continue; cleanedConnections[sourceId] = {}; for (const [type, outputs] of Object.entries(nodeConns)) { if (Array.isArray(outputs)) { cleanedConnections[sourceId][type] = outputs.map(conns => // Filter out connections to removed nodes conns.filter(c => !nodesToRemove.has(c.node)) ); } } } result.connections = cleanedConnections; break; } ``` **Performance:** - **Latency:** <50ms - **Parallelizable:** Yes (multiple removes) - **LLM Calls:** 0 --- ### Tool 7: get_node_parameter **Purpose:** Retrieve specific parameter values when workflow JSON is trimmed. **Schema:** ```typescript { nodeId: string, path: string // Lodash path syntax } ``` **Examples:** ```typescript // Simple path { nodeId: "http-node-123", path: "url" } // Returns: "https://api.example.com" // Nested path { nodeId: "http-node-123", path: "headerParameters.parameters[0].value" } // Returns: "application/json" // Options path { nodeId: "set-node-456", path: "options.includeOtherFields" } // Returns: true ``` **Parameter Extraction:** ```typescript import get from 'lodash/get'; function extractParameterValue( node: INode, path: string ): NodeParameterValueType | undefined { return get(node.parameters, path); } ``` **Safety Checks:** ```typescript const MAX_PARAMETER_VALUE_LENGTH = 30_000; if (formattedValue.length > MAX_PARAMETER_VALUE_LENGTH) { throw new ValidationError( `Parameter value at path "${path}" exceeds maximum length of ${MAX_PARAMETER_VALUE_LENGTH} characters` ); } ``` **Use Case:** When workflow JSON is sent to the agent, large parameters are trimmed: ```typescript function trimWorkflowJSON(workflow: SimpleWorkflow): SimpleWorkflow { return { ...workflow, nodes: workflow.nodes.map(node => ({ ...node, parameters: trimLargeParameters(node.parameters) })) }; } function trimLargeParameters(params: any): any { if (typeof params === 'string' && params.length > 1000) { return '<value omitted - use get_node_parameter tool>'; } // Recursively trim nested objects/arrays ... } ``` The AI can then selectively fetch needed values: ```typescript // Workflow JSON shows: "body": "<value omitted - use get_node_parameter tool>" // AI calls: get_node_parameter({ nodeId: "http-node-123", path: "body" }) // Returns full value ``` **Performance:** - **Latency:** <50ms - **Parallelizable:** Yes - **LLM Calls:** 0 --- ## Operations System ### Operation Types ```typescript type WorkflowOperation = | { type: 'clear' } | { type: 'removeNode'; nodeIds: string[] } | { type: 'addNodes'; nodes: INode[] } | { type: 'updateNode'; nodeId: string; updates: Partial<INode> } | { type: 'setConnections'; connections: IConnections } | { type: 'mergeConnections'; connections: IConnections } | { type: 'setName'; name: string }; ``` ### Operations Processor The `process_operations` node applies all queued operations to the workflow state. ```typescript export function processOperations(state: WorkflowState): Partial<WorkflowState> { const { workflowJSON, workflowOperations } = state; if (!workflowOperations || workflowOperations.length === 0) { return {}; } // Apply all operations sequentially const newWorkflow = applyOperations(workflowJSON, workflowOperations); return { workflowJSON: newWorkflow, workflowOperations: null // Clear queue }; } export function applyOperations( workflow: SimpleWorkflow, operations: WorkflowOperation[] ): SimpleWorkflow { let result = { nodes: [...workflow.nodes], connections: { ...workflow.connections }, name: workflow.name || '' }; for (const operation of operations) { switch (operation.type) { case 'clear': result = { nodes: [], connections: {}, name: '' }; break; case 'addNodes': { const nodeMap = new Map(result.nodes.map(n => [n.id, n])); operation.nodes.forEach(node => nodeMap.set(node.id, node)); result.nodes = Array.from(nodeMap.values()); break; } case 'updateNode': { result.nodes = result.nodes.map(node => node.id === operation.nodeId ? { ...node, ...operation.updates } : node ); break; } case 'removeNode': { const nodesToRemove = new Set(operation.nodeIds); result.nodes = result.nodes.filter(n => !nodesToRemove.has(n.id)); // Clean connections const cleanedConnections: IConnections = {}; for (const [sourceId, nodeConns] of Object.entries(result.connections)) { if (!nodesToRemove.has(sourceId)) { cleanedConnections[sourceId] = {}; for (const [type, outputs] of Object.entries(nodeConns)) { if (Array.isArray(outputs)) { cleanedConnections[sourceId][type] = outputs.map(conns => conns.filter(c => !nodesToRemove.has(c.node)) ); } } } } result.connections = cleanedConnections; break; } case 'setConnections': { result.connections = operation.connections; break; } case 'mergeConnections': { for (const [sourceId, nodeConns] of Object.entries(operation.connections)) { if (!result.connections[sourceId]) { result.connections[sourceId] = nodeConns; } else { for (const [type, newOutputs] of Object.entries(nodeConns)) { if (!result.connections[sourceId][type]) { result.connections[sourceId][type] = newOutputs; } else { // Deep merge arrays, avoid duplicates const existing = result.connections[sourceId][type]; if (Array.isArray(newOutputs) && Array.isArray(existing)) { for (let i = 0; i < Math.max(newOutputs.length, existing.length); i++) { if (!newOutputs[i]) continue; if (!existing[i]) { existing[i] = newOutputs[i]; } else { // Merge connections, check duplicates const existingSet = new Set( existing[i].map(c => JSON.stringify(c)) ); newOutputs[i].forEach(conn => { const key = JSON.stringify(conn); if (!existingSet.has(key)) { existing[i].push(conn); } }); } } } } } } } break; } case 'setName': { result.name = operation.name; break; } } } return result; } ``` ### Parallel Execution Flow ``` ┌─────────────────────────────────────────────┐ │ Agent returns 3 tool calls: │ │ 1. add_nodes (HTTP Request) │ │ 2. add_nodes (Set) │ │ 3. connect_nodes (HTTP → Set) │ └──────────────────┬──────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────┐ │ executeToolsInParallel() │ │ │ │ Promise.all([ │ │ addNodesTool.invoke({...}), // Returns: │ │ → { workflowOperations: [{ │ │ type: 'addNodes', │ │ nodes: [httpNode] │ │ }]} │ │ │ │ addNodesTool.invoke({...}), // Returns: │ │ → { workflowOperations: [{ │ │ type: 'addNodes', │ │ nodes: [setNode] │ │ }]} │ │ │ │ connectNodesTool.invoke({...}) // Returns:│ │ → { workflowOperations: [{ │ │ type: 'mergeConnections', │ │ connections: {...} │ │ }]} │ │ ]) │ └──────────────────┬──────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────┐ │ Collect all operations: │ │ [ │ │ { type: 'addNodes', nodes: [httpNode] }, │ │ { type: 'addNodes', nodes: [setNode] }, │ │ { type: 'mergeConnections', ... } │ │ ] │ └──────────────────┬──────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────┐ │ Return to LangGraph: │ │ { │ │ messages: [ToolMessage, ...], │ │ workflowOperations: [...] │ │ } │ └──────────────────┬──────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────┐ │ Graph transitions to process_operations │ │ │ │ applyOperations(workflow, operations) │ │ → Processes operations sequentially │ │ → Returns updated workflow │ └──────────────────┬──────────────────────────┘ │ ↓ ┌─────────────────────────────────────────────┐ │ Updated state: │ │ { │ │ workflowJSON: { nodes: [http, set], ... }│ │ workflowOperations: null │ │ } │ └─────────────────────────────────────────────┘ ``` ### Why This Design? **Benefits:** 1. **Parallel Safety**: Tools never mutate state directly, avoiding race conditions 2. **Transaction Semantics**: All operations from one agent turn are applied atomically 3. **Audit Trail**: Operations are first-class data that can be logged/inspected 4. **Undo/Redo**: Operations could be reversed or replayed 5. **Order Independence**: Tools can execute in any order; operations apply sequentially 6. **Determinism**: Same operations always produce same result 7. **Testing**: Operations can be tested independently of tools **Trade-offs:** - Extra indirection layer - Operations must be serializable - Can't inspect intermediate state during batch --- ## Design Patterns ### 1. Command Pattern Tools return **operations** (commands) instead of mutating state directly. ```typescript // Instead of: function addNode(node: INode) { workflow.nodes.push(node); // ❌ Direct mutation } // Use: function addNode(node: INode) { return { workflowOperations: [{ type: 'addNodes', nodes: [node] }] }; // ✅ Return command } ``` ### 2. Repository Pattern State access goes through helper functions, not direct access. ```typescript // helpers/state.ts export function getCurrentWorkflow(state: WorkflowState): SimpleWorkflow { return state.workflowJSON; } export function addNodeToWorkflow(node: INode): Partial<WorkflowState> { return { workflowOperations: [{ type: 'addNodes', nodes: [node] }] }; } ``` ### 3. Factory Pattern Tools are created by factory functions, not instantiated directly. ```typescript export function createAddNodeTool(nodeTypes: INodeTypeDescription[]): BuilderTool { const dynamicTool = tool( (input, config) => { /* implementation */ }, { name: 'add_nodes', description: '...', schema: nodeCreationSchema } ); return { tool: dynamicTool, toolName: 'add_nodes', displayTitle: 'Adding nodes' }; } ``` ### 4. Strategy Pattern Different node types get different prompts via `ParameterUpdatePromptBuilder`. ```typescript buildSystemPrompt(context) { let prompt = CORE_INSTRUCTIONS; if (isSetNode) prompt += SET_NODE_EXAMPLES; else if (isIfNode) prompt += IF_NODE_EXAMPLES; else if (isToolNode) prompt += TOOL_NODE_EXAMPLES; return prompt; } ``` ### 5. Template Method Pattern All tools follow the same structure: ```typescript tool((input, config) => { const reporter = createProgressReporter(config); try { const validated = schema.parse(input); reporter.start(validated); // Business logic here reporter.complete(output); return createSuccessResponse(config, message, stateUpdates); } catch (error) { reporter.error(error); return createErrorResponse(config, error); } }) ``` ### 6. Observer Pattern Progress streaming via `reporter`: ```typescript reporter.start(input); // → Frontend: "Starting..." reporter.progress("..."); // → Frontend: "In progress..." reporter.complete(output); // → Frontend: "Complete!" reporter.error(error); // → Frontend: "Error!" ``` ### 7. Adapter Pattern `NodeSearchEngine` adapts different search modes to unified interface: ```typescript class NodeSearchEngine { searchByName(query, limit): NodeSearchResult[] searchByConnectionType(type, limit, filter): NodeSearchResult[] } ``` ### 8. Builder Pattern `ParameterUpdatePromptBuilder` constructs complex prompts incrementally: ```typescript let prompt = CORE_INSTRUCTIONS; prompt += nodeTypeExamples; if (hasResourceLocator) prompt += RESOURCE_LOCATOR_EXAMPLES; if (hasTextFields) prompt += EXPRESSION_RULES; prompt += OUTPUT_FORMAT; ``` ### 9. Decorator Pattern LLM is enhanced with structured output: ```typescript const llm = new ChatAnthropic({...}); const llmWithStructure = llm.withStructuredOutput(parametersSchema); ``` ### 10. Chain of Responsibility Validation happens at multiple levels: ``` Input → Zod Schema → Business Logic → Semantic Validation → Operations Processor ``` ### 11. Memento Pattern Checkpointer saves/restores conversation state: ```typescript const checkpoint = await checkpointer.getTuple(config); // Later: restore from checkpoint ``` ### 12. Singleton Pattern SessionManagerService maintains single checkpointer instance: ```typescript class SessionManagerService { private checkpointer: MemorySaver; getCheckpointer(): MemorySaver { return this.checkpointer; } } ``` ### 13. Specification Pattern Node type matching uses specifications: ```typescript isSubNode(nodeType, node) → boolean nodeHasOutputType(nodeType, connectionType) → boolean nodeAcceptsInputType(nodeType, connectionType) → boolean ``` ### 14. Null Object Pattern Empty operations list instead of null: ```typescript workflowOperations: Annotation<WorkflowOperation[] | null>({ reducer: operationsReducer, default: () => [] // Not null }); ``` ### 15. Proxy Pattern AI Assistant SDK proxies requests to Anthropic: ``` Service → SDK → AI Assistant Proxy → Anthropic ``` --- ## Prompt Engineering ### Main Agent Prompt Structure ```typescript const mainAgentPrompt = ChatPromptTemplate.fromMessages([ ['system', [ { type: 'text', text: systemPrompt }, // Cached { type: 'text', text: instanceUrlPrompt }, { type: 'text', text: currentWorkflowJson }, { type: 'text', text: currentExecutionData }, { type: 'text', text: currentExecutionNodesSchemas }, { type: 'text', text: responsePatterns }, // Cached { type: 'text', text: previousConversationSummary } // Cached ]], ['placeholder', '{messages}'] ]); ``` ### System Prompt Components **1. Core Principle:** ``` After receiving tool results, reflect on their quality and determine optimal next steps. Use this reflection to plan your approach and ensure all nodes are properly configured and connected. ``` **2. Communication Style:** ``` Keep responses concise. CRITICAL: Do NOT provide commentary between tool calls. Execute tools silently. - NO progress messages like "Perfect!", "Now let me...", "Excellent!" - NO descriptions of what was built or how it works - Only respond AFTER all tools are complete ``` **3. Parallel Execution Guidelines:** ``` ALL tools support parallel execution, including add_nodes - Information gathering: Call search_nodes and get_node_details in parallel - Node creation: Add multiple nodes by calling add_nodes multiple times - Parameter updates: Update different nodes simultaneously ``` **4. Workflow Creation Sequence:** ``` 1. Discovery Phase (parallel execution) - Search for all required node types simultaneously 2. Analysis Phase (parallel execution) - Get details for ALL nodes before proceeding 3. Creation Phase (parallel execution) - Add nodes individually by calling add_nodes for each node 4. Connection Phase (parallel execution) - Connect all nodes based on discovered input/output structure 5. Configuration Phase (parallel execution) - MANDATORY - ALWAYS configure nodes using update_node_parameters ``` **5. Connection Rules:** ``` AI sub-nodes PROVIDE capabilities, making them the SOURCE: - OpenAI Chat Model → AI Agent [ai_languageModel] - Calculator Tool → AI Agent [ai_tool] - Token Splitter → Default Data Loader [ai_textSplitter] ``` **6. Critical Warnings:** ``` ⚠️ CRITICAL: NEVER RELY ON DEFAULT PARAMETER VALUES ⚠️ Default values are a common source of runtime failures. You MUST explicitly configure ALL parameters that control node behavior. ``` **7. Workflow Configuration Node:** ``` CRITICAL: Always include a Workflow Configuration node at the start of every workflow. Placement: Trigger → Workflow Configuration → First processing node This creates a single source of truth for workflow parameters. ``` **8. $fromAI Expressions:** ``` Tool nodes (nodes ending with "Tool") support special $fromAI expressions: {{ $fromAI('key', 'description', 'type', defaultValue) }} Example: { "sendTo": "={{ $fromAI('to') }}", "subject": "={{ $fromAI('subject') }}" } ``` **9. Response Patterns:** ``` IMPORTANT: Only provide ONE response AFTER all tool executions are complete. Response format conditions: - Include "**⚙️ How to Setup**" ONLY if this is the initial workflow creation - Include "**📝 What's changed**" ONLY for non-initial modifications ``` ### Parameter Updater Prompt Structure ```typescript const systemPrompt = ` You are an expert n8n workflow architect who updates node parameters based on natural language instructions. ## Your Task Update the parameters of an existing n8n node. Return the COMPLETE parameters object with both modified and unmodified parameters. ## Reference Information 1. The original user workflow request 2. The current workflow JSON 3. The selected node's current configuration 4. The node type's parameter definitions 5. Natural language changes to apply ## Parameter Update Guidelines 1. START WITH CURRENT: If current parameters is empty {}, start with an empty object and add the requested parameters 2. PRESERVE EXISTING VALUES: Only modify parameters mentioned in the requested changes 3. CHECK FOR RESOURCELOCATOR: If a parameter is type 'resourceLocator', it MUST use the ResourceLocator structure 4. USE PROPER EXPRESSIONS: Follow n8n expression syntax 5. VALIDATE TYPES: Ensure parameter values match their expected types `; const nodeDefinitionPrompt = ` The node accepts these properties: <node_properties_definition> {node_definition} </node_properties_definition> `; const workflowContextPrompt = ` <current_workflow_json> {workflow_json} </current_workflow_json> <selected_node> Name: {node_name} Type: {node_type} Current Parameters: {current_parameters} </selected_node> <requested_changes> {changes} </requested_changes> `; ``` ### Prompt Caching Strategy Anthropic's prompt caching is used for static content: ```typescript { type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } // ← Cached } ``` **Cached Sections:** - Main system prompt (~8K tokens) - Response patterns (~2K tokens) - Previous conversation summary (variable) - Node definition in parameter updater (variable) **Not Cached:** - Workflow JSON (changes frequently) - Execution data (changes frequently) - User messages (always new) **Cache Benefits:** - ~90% cache hit rate on subsequent turns - Reduces input tokens by ~10K per turn - Significant cost savings (cached tokens are ~10% cost) --- ## Performance & Optimization ### Token Budget Management ``` Maximum Context: 200,000 tokens Allocation: ├─ System Prompt: 8,000 tokens (cached) ├─ Node Definitions: 5,000 tokens (cached) ├─ Workflow JSON: 30,000 tokens (trimmed) ├─ Execution Data: 2,000 tokens ├─ Conversation History: 20,000 tokens (auto-compact) ├─ Previous Summary: 1,000 tokens (after compact) ├─ Buffer: 10,000 tokens └─ Output Reserved: 16,000 tokens Total: 92,000 / 200,000 used Remaining: 108,000 tokens for conversation growth ``` ### Workflow JSON Trimming ```typescript function trimWorkflowJSON(workflow: SimpleWorkflow): SimpleWorkflow { const estimatedTokens = estimateTokens(JSON.stringify(workflow)); if (estimatedTokens <= MAX_WORKFLOW_LENGTH_TOKENS) { return workflow; } return { ...workflow, nodes: workflow.nodes.map(node => ({ ...node, parameters: trimParameters(node.parameters) })) }; } function trimParameters(params: any): any { if (typeof params === 'string' && params.length > 1000) { return '<value omitted - use get_node_parameter tool>'; } if (Array.isArray(params)) { return params.map(trimParameters); } if (typeof params === 'object' && params !== null) { const trimmed: any = {}; for (const [key, value] of Object.entries(params)) { trimmed[key] = trimParameters(value); } return trimmed; } return params; } ``` ### Auto-Compaction When conversation exceeds token threshold: ```typescript function shouldAutoCompact(state: WorkflowState): boolean { const tokenUsage = extractLastTokenUsage(state.messages); const tokensUsed = tokenUsage.input_tokens + tokenUsage.output_tokens; return tokensUsed > DEFAULT_AUTO_COMPACT_THRESHOLD; // 20,000 } async function compactSession(state: WorkflowState) { const { messages, previousSummary } = state; // Call LLM to compress history const compacted = await conversationCompactChain( llm, messages, previousSummary ); return { previousSummary: compacted.summaryPlain, messages: [ ...messages.map(m => new RemoveMessage({ id: m.id })), new HumanMessage('Please compress the conversation history'), new AIMessage('Successfully compacted conversation history') ] }; } ``` ### Parallel Execution Metrics ```typescript // Sequential execution (slow) await search_nodes(); await get_node_details(); await add_nodes(); await connect_nodes(); // Total: ~400ms // Parallel execution (fast) await Promise.all([ search_nodes(), get_node_details(), add_nodes(), connect_nodes() ]); // Total: ~100ms (75% faster) ``` ### Latency Breakdown ``` User sends message: 0ms ├─ Frontend → Service: 10ms ├─ Setup LLM client: 50ms ├─ Agent initialization: 20ms ├─ First LLM call (with tools): 2000ms │ ├─ Prompt construction: 10ms │ ├─ API request: 50ms │ ├─ LLM processing: 1800ms │ └─ Response parsing: 140ms ├─ Tool execution (parallel): 100ms │ ├─ search_nodes: 30ms │ ├─ get_node_details: 40ms │ └─ add_nodes: 50ms ├─ Process operations: 10ms ├─ Second LLM call (response): 1500ms └─ Stream to frontend: 50ms Total: ~3740ms (~3.7 seconds) ``` ### Optimization Strategies 1. **Prompt Caching**: 90% cache hit rate saves ~10K tokens/turn 2. **Parallel Tools**: 75% latency reduction on multi-tool calls 3. **Lazy Loading**: Fetch large parameters only when needed 4. **Workflow Trimming**: Keeps JSON under 30K token limit 5. **Auto-Compaction**: Prevents context overflow 6. **Batch Operations**: Single API call for multiple changes 7. **Streaming**: Progressive UI updates improve perceived performance --- ## Error Handling ### Error Hierarchy ```typescript // Base error class WorkflowBuilderError extends Error { constructor( message: string, public code: string, public details?: Record<string, unknown> ) { super(message); } } // Specific errors class ValidationError extends WorkflowBuilderError class NodeNotFoundError extends WorkflowBuilderError class NodeTypeNotFoundError extends WorkflowBuilderError class ConnectionError extends WorkflowBuilderError class ParameterUpdateError extends WorkflowBuilderError class ToolExecutionError extends WorkflowBuilderError class LLMServiceError extends WorkflowBuilderError class WorkflowStateError extends WorkflowBuilderError ``` ### Error Response Format ```typescript interface ToolError { message: string; code: string; details?: Record<string, unknown>; } function createErrorResponse(config: ToolRunnableConfig, error: ToolError): Command { return new Command({ update: { messages: [ new ToolMessage({ content: `Error: ${error.message}`, tool_call_id: config.toolCall.id, additional_kwargs: { error: true, code: error.code } }) ] } }); } ``` ### Error Handling Pattern All tools follow this pattern: ```typescript tool((input, config) => { const reporter = createProgressReporter(config); try { // 1. Schema validation const validated = schema.parse(input); // 2. Business logic validation const node = validateNodeExists(nodeId, nodes); if (!node) { throw new NodeNotFoundError(nodeId); } // 3. Semantic validation const validation = validateConnection(...); if (!validation.valid) { throw new ConnectionError(validation.error); } // 4. Execute reporter.complete(output); return createSuccessResponse(config, message, stateUpdates); } catch (error) { // 5. Error categorization if (error instanceof z.ZodError) { const toolError = new ValidationError('Invalid input', { errors: error.errors }); reporter.error(toolError); return createErrorResponse(config, toolError); } if (error instanceof WorkflowBuilderError) { reporter.error(error); return createErrorResponse(config, error); } // 6. Unknown errors const toolError = new ToolExecutionError( error instanceof Error ? error.message : 'Unknown error' ); reporter.error(toolError); return createErrorResponse(config, toolError); } }) ``` ### Validation Layers ``` Input Data ↓ ┌─────────────────────┐ │ Layer 1: Zod Schema │ ← Type checking, required fields └──────────┬──────────┘ ↓ ┌─────────────────────┐ │ Layer 2: Business │ ← Node exists? Type valid? │ Logic │ └──────────┬──────────┘ ↓ ┌─────────────────────┐ │ Layer 3: Semantic │ ← Connection compatible? │ Rules │ Parameter type correct? └──────────┬──────────┘ ↓ ┌─────────────────────┐ │ Layer 4: Operations │ ← Final integrity check │ Processor │ during state mutation └─────────────────────┘ ``` ### Graceful Degradation ```typescript // If one tool fails in parallel batch, others continue const toolResults = await Promise.all( aiMessage.tool_calls.map(async (toolCall) => { try { return await tool.invoke(toolCall.args); } catch (error) { // Return ToolMessage with error instead of throwing return new ToolMessage({ content: `Tool ${toolCall.name} failed: ${error.message}`, tool_call_id: toolCall.id, additional_kwargs: { error: true } }); } }) ); // Agent sees errors and can retry or adjust approach ``` ### Error Recovery Strategies **1. Auto-Correction (connect_nodes):** ```typescript // Wrong direction detected → auto-swap instead of error if (targetIsSubNode && !sourceIsSubNode) { return { valid: true, shouldSwap: true, swappedSource: targetNode, swappedTarget: sourceNode }; } ``` **2. Helpful Error Messages:** ```typescript throw new ConnectionError( 'No compatible connection types found', { sourceNode: source.name, targetNode: target.name, possibleTypes: { source: sourceOutputTypes, target: targetInputTypes } } ); ``` **3. Fallback Values:** ```typescript const name = customName ?? nodeType.defaults?.name ?? nodeType.displayName; ``` **4. Safe Defaults:** ```typescript const limit = validatedInput.limit ?? 5; const withParameters = validatedInput.withParameters ?? false; ``` --- ## Security & Validation ### Input Validation **Zod Schemas:** ```typescript const nodeCreationSchema = z.object({ nodeType: z.string(), name: z.string(), connectionParametersReasoning: z.string(), connectionParameters: z.object({}).passthrough() }); const nodeConnectionSchema = z.object({ sourceNodeId: z.string(), targetNodeId: z.string(), sourceOutputIndex: z.number().optional(), targetInputIndex: z.number().optional() }); ``` **Runtime Validation:** ```typescript try { const validated = schema.parse(input); } catch (error) { if (error instanceof z.ZodError) { throw new ValidationError('Invalid input', { errors: error.errors }); } } ``` ### SQL Injection Prevention Not applicable - no SQL queries. All data access through in-memory structures. ### Command Injection Prevention Not applicable - no shell commands executed from user input. ### Authorization ```typescript // Every request requires authenticated user async *chat(payload: ChatPayload, user: IUser, abortSignal?: AbortSignal) { if (!user || !user.id) { throw new Error('Unauthorized'); } // Get user-specific auth token const authHeaders = await this.getApiProxyAuthHeaders(user); // All LLM requests include user's JWT const llm = await setupModel({ authHeaders }); } ``` ### Rate Limiting Handled by AI Assistant SDK proxy: - Credits-based metering - Per-user quotas - Usage tracking ```typescript await this.client.markBuilderSuccess(user, authHeaders); // Returns: { creditsQuota, creditsClaimed } if (creditsClaimed >= creditsQuota) { throw new Error('Credit quota exceeded'); } ``` ### Data Sanitization **Expression Fixing:** ```typescript function fixExpressionPrefixes(parameters: any): any { // Prevent malicious expressions if (typeof parameters === 'string') { // Only fix n8n expression syntax, don't execute return fixExpressionFormat(parameters); } // Recursively sanitize nested structures return recursiveSanitize(parameters); } ``` **Size Limits:** ```typescript const MAX_AI_BUILDER_PROMPT_LENGTH = 1000; // User input limit const MAX_PARAMETER_VALUE_LENGTH = 30_000; // Parameter size limit const MAX_WORKFLOW_LENGTH_TOKENS = 30_000; // Workflow JSON limit ``` ### Secrets Protection ```typescript // No credentials stored in workflow JSON // Credentials managed separately by n8n core // AI never has access to credential values ``` --- ## Best Practices ### For AI Workflow Generation **1. Always Discovery → Details → Action:** ```typescript ✅ Good: 1. search_nodes({queries: [{queryType: "name", query: "http"}]}) 2. get_node_details({nodeName: "n8n-nodes-base.httpRequest"}) 3. add_nodes({...}) ❌ Bad: 1. add_nodes({nodeType: "n8n-nodes-base.httpRequest", ...}) // Might not exist! Should search first. ``` **2. Parallel Execution When Possible:** ```typescript ✅ Good: Promise.all([ add_nodes({nodeType: "n8n-nodes-base.httpRequest", ...}), add_nodes({nodeType: "n8n-nodes-base.set", ...}) ]) ❌ Bad: await add_nodes({nodeType: "n8n-nodes-base.httpRequest", ...}); await add_nodes({nodeType: "n8n-nodes-base.set", ...}); // Sequential = slower ``` **3. Always Configure Nodes:** ```typescript ✅ Good: 1. add_nodes({...}) 2. connect_nodes({...}) 3. update_node_parameters({ nodeId: "...", changes: ["Set URL to https://...", "Set method to POST"] }) ❌ Bad: 1. add_nodes({...}) 2. connect_nodes({...}) // Node not configured! Will fail at runtime. ``` **4. Use Connection Parameters Thoughtfully:** ```typescript ✅ Good: { nodeType: "@n8n/n8n-nodes-langchain.vectorStoreInMemory", connectionParametersReasoning: "Vector Store mode determines inputs. Using 'insert' for document processing.", connectionParameters: { mode: "insert" } } ❌ Bad: { nodeType: "@n8n/n8n-nodes-langchain.vectorStoreInMemory", connectionParametersReasoning: "Adding vector store", connectionParameters: {} // Missing critical mode parameter! } ``` **5. Batch Related Changes:** ```typescript ✅ Good: update_node_parameters({ nodeId: "http-123", changes: [ "Set URL to https://api.example.com", "Set method to POST", "Add header Content-Type: application/json", "Set body to {\"key\": \"value\"}" ] }) ❌ Bad: update_node_parameters({nodeId: "http-123", changes: ["Set URL..."]}); update_node_parameters({nodeId: "http-123", changes: ["Set method..."]}); update_node_parameters({nodeId: "http-123", changes: ["Add header..."]}); // Multiple LLM calls = slow and expensive ``` ### For Implementation **1. Use TypeScript Strict Mode:** ```json { "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true } } ``` **2. Validate Everything:** ```typescript // Input validation const validated = schema.parse(input); // Business validation const node = validateNodeExists(nodeId, nodes); // Semantic validation const result = validateConnection(source, target, type); ``` **3. Use Discriminated Unions:** ```typescript type WorkflowOperation = | { type: 'addNodes'; nodes: INode[] } | { type: 'removeNode'; nodeIds: string[] } | { type: 'updateNode'; nodeId: string; updates: Partial<INode> }; // TypeScript narrows type based on discriminant function applyOperation(op: WorkflowOperation) { switch (op.type) { case 'addNodes': op.nodes // ← TypeScript knows this exists break; case 'removeNode': op.nodeIds // ← TypeScript knows this exists break; } } ``` **4. Separate Pure Logic from I/O:** ```typescript // ✅ Good: Pure business logic class NodeSearchEngine { searchByName(query: string): NodeSearchResult[] { // No I/O, easily testable } } // ✅ Good: I/O wrapper function createNodeSearchTool(nodeTypes: INodeTypeDescription[]) { const engine = new NodeSearchEngine(nodeTypes); return tool((input, config) => { const results = engine.searchByName(input.query); return createSuccessResponse(config, formatResults(results)); }); } ``` **5. Use Builders for Complex Objects:** ```typescript class ParameterUpdatePromptBuilder { private prompt = ''; addCoreInstructions() { this.prompt += CORE_INSTRUCTIONS; return this; } addNodeExamples(nodeType: string) { if (isSetNode(nodeType)) this.prompt += SET_NODE_EXAMPLES; return this; } build() { return this.prompt; } } const prompt = new ParameterUpdatePromptBuilder() .addCoreInstructions() .addNodeExamples(nodeType) .build(); ``` --- ## Implementation Details ### Key Files **AI Workflow Builder Service:** ```typescript // packages/@n8n/ai-workflow-builder.ee/src/ai-workflow-builder-agent.service.ts @Service() export class AiWorkflowBuilderService { async *chat(payload: ChatPayload, user: IUser, abortSignal?: AbortSignal) { const agent = await this.getAgent(user); for await (const output of agent.chat(payload, user.id, abortSignal)) { yield output; } } } ``` **Workflow Builder Agent:** ```typescript // packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts export class WorkflowBuilderAgent { private createWorkflow() { const workflow = new StateGraph(WorkflowState) .addNode('agent', callModel) .addNode('tools', customToolExecutor) .addNode('process_operations', processOperations) // ... more nodes return workflow.compile({ checkpointer: this.checkpointer }); } async *chat(payload: ChatPayload, userId: string) { const workflow = this.createWorkflow(); const stream = workflow.stream(initialState, config); for await (const output of createStreamProcessor(stream)) { yield output; } } } ``` **Operations Processor:** ```typescript // packages/@n8n/ai-workflow-builder.ee/src/utils/operations-processor.ts export function processOperations(state: WorkflowState) { const newWorkflow = applyOperations( state.workflowJSON, state.workflowOperations ); return { workflowJSON: newWorkflow, workflowOperations: null }; } ``` **Tool Executor:** ```typescript // packages/@n8n/ai-workflow-builder.ee/src/utils/tool-executor.ts export async function executeToolsInParallel(options: ToolExecutorOptions) { const toolResults = await Promise.all( aiMessage.tool_calls.map(toolCall => tool.invoke(toolCall.args)) ); // Collect all operations const allOperations: WorkflowOperation[] = []; for (const update of stateUpdates) { if (update.workflowOperations) { allOperations.push(...update.workflowOperations); } } return { messages: allMessages, workflowOperations: allOperations }; } ``` ### Dependencies ```json { "dependencies": { "@langchain/anthropic": "^0.3.x", "@langchain/core": "^0.3.x", "@langchain/langgraph": "^0.2.x", "@n8n_io/ai-assistant-sdk": "^1.15.x", "n8n-workflow": "workspace:*", "zod": "^3.23.x", "lodash": "^4.17.x" } } ``` ### Environment Variables ```bash # Self-hosted mode (optional) N8N_AI_ANTHROPIC_KEY=sk-ant-xxx # Cloud mode (via AI Assistant SDK) # No env vars needed - uses SDK client ``` --- ## Appendix ### A. Connection Types Reference ```typescript enum NodeConnectionTypes { // Main data flow Main = 'main', // AI connections AiLanguageModel = 'ai_languageModel', AiTool = 'ai_tool', AiMemory = 'ai_memory', AiDocument = 'ai_document', AiVectorStore = 'ai_vectorStore', AiEmbedding = 'ai_embedding', AiOutputParser = 'ai_outputParser', AiTextSplitter = 'ai_textSplitter', AiRetriever = 'ai_retriever', AiChain = 'ai_chain', AiAgent = 'ai_agent', AiToolkit = 'ai_toolkit' } ``` ### B. Common Node Types **Triggers:** - `n8n-nodes-base.scheduleTrigger` - Schedule - `n8n-nodes-base.webhook` - Webhook - `n8n-nodes-base.manualTrigger` - Manual **Actions:** - `n8n-nodes-base.httpRequest` - HTTP Request - `n8n-nodes-base.set` - Set - `n8n-nodes-base.code` - Code - `n8n-nodes-base.if` - IF **AI Nodes:** - `@n8n/n8n-nodes-langchain.agent` - AI Agent - `@n8n/n8n-nodes-langchain.chainLlm` - Basic LLM Chain - `@n8n/n8n-nodes-langchain.chainSummarization` - Summarization Chain **AI Sub-nodes:** - `@n8n/n8n-nodes-langchain.lmChatAnthropic` - Anthropic Chat Model - `@n8n/n8n-nodes-langchain.lmChatOpenAi` - OpenAI Chat Model - `@n8n/n8n-nodes-langchain.toolCalculator` - Calculator Tool - `@n8n/n8n-nodes-langchain.toolCode` - Code Tool ### C. Token Estimation ```typescript const AVG_CHARS_PER_TOKEN_ANTHROPIC = 2.5; function estimateTokens(text: string): number { return Math.ceil(text.length / AVG_CHARS_PER_TOKEN_ANTHROPIC); } // Examples: estimateTokens("Hello world") // → 5 tokens estimateTokens(JSON.stringify(workflow)) // → ~12,000 tokens for typical workflow ``` ### D. Workflow JSON Structure ```typescript interface SimpleWorkflow { name: string; nodes: INode[]; connections: IConnections; } interface INode { id: string; name: string; type: string; typeVersion: number; position: [number, number]; parameters: INodeParameters; credentials?: INodeCredentials; } interface IConnections { [nodeName: string]: { [connectionType: string]: Array<Array<IConnection>>; }; } interface IConnection { node: string; // Target node name type: NodeConnectionType; index: number; // Target input index } ``` **Example:** ```json { "name": "Weather Workflow", "nodes": [ { "id": "abc-123", "name": "Schedule Trigger", "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1, "position": [240, 300], "parameters": { "rule": { "interval": [{ "field": "hours", "hoursInterval": 1 }] } } }, { "id": "def-456", "name": "HTTP Request", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [480, 300], "parameters": { "method": "GET", "url": "https://api.weather.com/forecast" } } ], "connections": { "Schedule Trigger": { "main": [[ { "node": "HTTP Request", "type": "main", "index": 0 } ]] } } } ``` ### E. Glossary - **Agent**: LangGraph node that calls the LLM - **Builder Tool**: One of the 7 tools for workflow manipulation - **Checkpointer**: Persists conversation state between turns - **Connection Type**: Type of link between nodes (main, ai_tool, etc.) - **LangGraph**: State machine framework for agentic workflows - **Operation**: Command object representing a state mutation - **Reporter**: Progress streaming interface - **Sub-node**: AI capability provider (OpenAI, Calculator, etc.) - **Tool**: LangChain function the LLM can invoke - **Workflow JSON**: Simplified n8n workflow representation ### F. Comparison with Alternatives | Feature | n8n AI Builder | Zapier AI | Make.com AI | Custom LangChain | |---------|----------------|-----------|-------------|------------------| | **Architecture** | LangGraph + Tools | Proprietary | Proprietary | DIY | | **Node Library** | 400+ nodes | 5000+ apps | 1500+ apps | Custom | | **Parallel Tools** | ✅ Yes | ❌ No | ❌ No | 🤷 Depends | | **Connection Inference** | ✅ Advanced | ⚠️ Basic | ⚠️ Basic | 🤷 Depends | | **Auto-correction** | ✅ Yes | ❌ No | ❌ No | 🤷 Depends | | **Self-hosted** | ✅ Yes | ❌ No | ❌ No | ✅ Yes | | **Streaming** | ✅ Real-time | ⚠️ Polling | ⚠️ Polling | 🤷 Depends | | **Token Optimization** | ✅ Aggressive | ⚠️ Basic | ⚠️ Basic | 🤷 Depends | | **Open Source** | ✅ Fair-code | ❌ No | ❌ No | ✅ Yes | ### G. Future Improvements **Potential Enhancements:** 1. **Multi-turn Configuration Wizard**: Guide users through complex node setup 2. **Template Library Integration**: Suggest relevant templates during creation 3. **Execution Preview**: Show expected execution flow before saving 4. **Smart Defaults**: Learn user preferences for common patterns 5. **Error Prediction**: Warn about likely runtime issues 6. **Performance Optimization**: Suggest workflow improvements 7. **Natural Language Debugging**: "Why is this node failing?" 8. **Version Control Integration**: Git-based workflow management 9. **Collaborative Editing**: Real-time multi-user workflow building 10. **A/B Testing**: Compare workflow variants --- ## Conclusion The n8n AI Workflow Builder represents a **state-of-the-art implementation** of LLM-powered workflow automation. Its architecture demonstrates: 1. **Thoughtful Design**: 15+ design patterns working in harmony 2. **Production Quality**: Comprehensive error handling and validation 3. **Performance Focus**: Parallel execution and token optimization 4. **User Experience**: Real-time streaming and auto-correction 5. **Extensibility**: Clean separation of concerns for easy enhancement The **7-tool architecture** provides a complete CRUD interface for workflows while maintaining simplicity. The **operations pattern** enables parallel execution without race conditions. The **intelligent connection inference** prevents common mistakes. And the **nested LLM approach** for parameter updates showcases creative AI-powered AI. This system serves as an **excellent reference implementation** for anyone building LLM-powered tools that manipulate complex state. **Key Takeaways:** - Operations over direct mutations = parallel-safe execution - Auto-correction over errors = better UX - Reasoning parameters = better AI decisions - Progressive disclosure = guided complexity - Token budgets are real = aggressive optimization required --- **Document Version:** 1.0 **Last Updated:** 2025-01-10 **Author:** Technical Analysis **License:** For reference and educational purposes

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/88-888/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server