Variable scoping issues in n8n prompts occur when data from upstream nodes is inaccessible in the LLM node because of node execution order, parallel branches, or item index mismatches. Fix this by using the $('NodeName') reference syntax, merging parallel branches before the LLM node, and building a consolidated context object in a Code node that collects all user data into a single output item for the prompt.
Passing Variables Correctly Through Nodes to LLM Prompts in n8n
You have user context data from a database query, webhook payload, and previous conversation history that all need to reach the LLM prompt. But some variables show up as undefined, others reference the wrong item, and data from parallel branches is completely invisible. These are variable scoping issues: the LLM node cannot access data that exists in the workflow but is not in its direct data path. This tutorial explains how data flows through n8n, why certain variables are out of scope, and how to collect all necessary context into a single accessible object.
Prerequisites
- A running n8n workflow with multiple data sources feeding into an LLM node
- Understanding of n8n's node connection model and data flow
- Basic familiarity with n8n expressions ({{ }} syntax)
- An AI Agent or Basic LLM Chain node in the workflow
Step-by-step guide
Understand how data scoping works in n8n
Understand how data scoping works in n8n
In n8n, each node receives data from its directly connected upstream node. The data is an array of items, each with a json property containing the node's output. The expression $json refers to the first item from the immediately previous node. To access data from a node further upstream, you must use the $('NodeName') syntax which looks up the output of any executed node by name. Critically, $('NodeName') only works for nodes that are in the same execution branch and have already executed. If a node is on a parallel branch that has not been merged, its output is inaccessible. Understanding this scoping model is essential for building prompts that require data from multiple sources.
1// $json = first item from immediately previous node2// $input.all() = all items from immediately previous node3// $('Webhook').first().json = first item from the 'Webhook' node4// $('Database Query').all() = all items from the 'Database Query' node5// $('Set User Data').first().json.name = specific field from a named nodeExpected result: You understand that data access depends on node execution order and branch connectivity.
Identify which variables are out of scope in your LLM prompt
Identify which variables are out of scope in your LLM prompt
Open the LLM node (AI Agent or Basic LLM Chain) and click on the expression editor for the prompt or system message field. Use the variable selector on the left panel to browse available data from upstream nodes. Nodes that appear in the selector are in scope; those that are missing are either on an unmerged parallel branch or have not been connected upstream. For each variable in your prompt that shows 'undefined', check whether the source node appears in the variable selector. If it does not, you have a scoping issue. Note which nodes are missing and trace the workflow connections to understand why they are out of scope.
Expected result: You have a list of which variables are accessible and which are out of scope in the LLM prompt.
Merge parallel branch data using a Merge node
Merge parallel branch data using a Merge node
If your user context data comes from parallel branches (e.g., one branch fetches user profile from a database while another fetches conversation history from Redis), these branches must be merged before the data reaches the LLM node. Add a Merge node that combines both branches. Use the Combine mode with Merge by Position to create a single item that contains data from both branches. Then use a Code node after the Merge to combine the fields from both sources into a clean context object. Without the Merge, the LLM node only sees data from whichever branch is directly connected to it.
1// Code node after Merge: Combine context from both branches2// Mode: Run Once for All Items34const items = $input.all();56// After Merge by Position, each item has fields from both branches7const merged = items[0]?.json || {};89// Build consolidated context10const context = {11 // From database branch12 userName: merged.name || merged.userName || 'User',13 email: merged.email || '',14 plan: merged.subscriptionPlan || 'free',15 16 // From conversation history branch17 previousMessages: merged.messages || merged.history || [],18 lastInteraction: merged.lastMessageAt || null,19 20 // From webhook (original trigger)21 currentMessage: merged.message || merged.text || '',22 sessionId: merged.sessionId || ''23};2425return [{ json: context }];Expected result: Data from all parallel branches is combined into a single item accessible by the LLM node.
Build a consolidated context object in a Code node
Build a consolidated context object in a Code node
The most reliable approach is to add a Code node immediately before the LLM node that collects all necessary data into a single context object. This node uses the $('NodeName') syntax to reach back to any upstream node, regardless of how many nodes are between them. It then outputs a single item with all fields the LLM prompt needs. The LLM node can then reference any field with simple $json.fieldName expressions. This pattern centralizes all data access in one place, making it easy to debug and maintain.
1// Code node: Context Collector2// Mode: Run Once for All Items34// Reach back to specific upstream nodes5const webhookData = $('Webhook').first().json;6const userData = $('Fetch User Profile').first().json;7const historyData = $('Load Chat History').all();89// Build the context object10const context = {11 // User identity12 chatInput: webhookData.body?.message || '',13 sessionId: webhookData.body?.sessionId || '',14 15 // User profile16 userName: userData.name || 'User',17 userPlan: userData.plan || 'free',18 userTimezone: userData.timezone || 'UTC',19 20 // Conversation history (last 10 messages)21 conversationHistory: historyData22 .slice(-10)23 .map(item => ({24 role: item.json.role,25 content: item.json.content26 })),27 28 // Dynamic system prompt additions29 systemContext: `The user's name is ${userData.name || 'User'}. `30 + `They are on the ${userData.plan || 'free'} plan. `31 + `Their timezone is ${userData.timezone || 'UTC'}.`32};3334return [{ json: context }];Expected result: A single item containing all user context data, accessible from the LLM node with simple $json references.
Handle multi-item data when the LLM expects a single input
Handle multi-item data when the LLM expects a single input
When an upstream node outputs multiple items (e.g., a database query returning multiple rows), the LLM node processes each item separately by default. If you need all items combined into a single prompt (e.g., a list of user orders to summarize), you must aggregate them first. Use a Code node set to Run Once for All Items that collects all items and combines them into a single output item. Format arrays as readable text for the prompt: convert rows to a numbered list or a formatted table. Do not pass raw JSON arrays to the LLM prompt unless you specifically want JSON output.
1// Code node: Aggregate multiple items into single prompt context2// Mode: Run Once for All Items34const items = $input.all();56// Example: Convert order records into a readable list7const orders = items.map((item, i) => {8 const o = item.json;9 return `${i + 1}. Order #${o.orderId} - ${o.product} - $${o.amount} - ${o.status}`;10});1112const orderSummary = orders.length > 013 ? `The user has ${orders.length} recent orders:\n${orders.join('\n')}`14 : 'The user has no recent orders.';1516return [{17 json: {18 orderContext: orderSummary,19 totalOrders: orders.length20 }21}];Expected result: Multiple data items are aggregated into a single formatted text that the LLM can process as part of the prompt.
Pass the consolidated context to the LLM prompt using simple expressions
Pass the consolidated context to the LLM prompt using simple expressions
With the context object built in the previous step, configure your LLM node to reference it with straightforward expressions. In the AI Agent's system message, use {{ $json.systemContext }}. In the prompt or chatInput field, use {{ $json.chatInput }}. If you need to inject the order summary into the system prompt, include {{ $json.orderContext }} in the system message template. Because the Code node outputs a single item with all fields at the top level, every expression is a simple $json.fieldName lookup with no complex paths or node name references needed. This makes the LLM node configuration clean and easy to maintain.
1// AI Agent node configuration:23// System Message field:4// {{ $json.systemContext }}5// 6// The user's recent order history:7// {{ $json.orderContext }}89// Prompt / chatInput field:10// {{ $json.chatInput }}1112// Session ID (in Memory sub-node):13// {{ $json.sessionId }}Expected result: The LLM receives a complete prompt with all user context variables correctly populated.
Complete working example
1// Code node: Production Context Collector2// Mode: Run Once for All Items3// Place immediately before the AI Agent or LLM Chain node4// Collects data from ALL upstream nodes into a single object56function safeGet(nodeName) {7 try {8 return $('"' + nodeName + '"').first().json;9 } catch (e) {10 console.log(`Node "${nodeName}" not found or has no data`);11 return {};12 }13}1415function safeGetAll(nodeName) {16 try {17 return $('"' + nodeName + '"').all().map(i => i.json);18 } catch (e) {19 console.log(`Node "${nodeName}" not found or has no data`);20 return [];21 }22}2324// Collect from all sources25const webhook = $('Webhook').first().json;26const body = webhook.body || webhook;2728// User profile (from database or API)29let userProfile = {};30try {31 userProfile = $('Fetch User Profile').first().json;32} catch (e) {33 console.log('User profile node not found, using defaults');34}3536// Conversation history37let history = [];38try {39 history = $('Load Chat History').all().map(i => i.json);40} catch (e) {41 console.log('Chat history node not found');42}4344// Build the consolidated context45const context = {46 // Core input47 chatInput: body.message || body.text || body.content || '',48 sessionId: body.sessionId || body.userId || 'default',4950 // User profile context51 userName: userProfile.name || body.userName || 'User',52 userEmail: userProfile.email || '',53 userPlan: userProfile.plan || 'free',54 userLocale: userProfile.locale || 'en',5556 // Formatted history for system prompt57 conversationSummary: history.length > 058 ? `Previous conversation (${history.length} messages):\n`59 + history.slice(-5).map(60 m => `${m.role}: ${(m.content || '').substring(0, 200)}`61 ).join('\n')62 : 'No previous conversation.',6364 // System prompt with injected context65 systemContext: [66 `User: ${userProfile.name || 'Unknown'}`,67 `Plan: ${userProfile.plan || 'free'}`,68 `Language: ${userProfile.locale || 'en'}`,69 history.length > 070 ? `This is message #${history.length + 1} in the conversation.`71 : 'This is the start of a new conversation.'72 ].join('\n'),7374 // Metadata for logging75 _meta: {76 timestamp: new Date().toISOString(),77 executionId: $execution.id,78 sourcesAvailable: {79 webhook: true,80 userProfile: Object.keys(userProfile).length > 0,81 history: history.length > 082 }83 }84};8586return [{ json: context }];Common mistakes when fixing Variable Scoping Issues with User Context in n8n Prompts
Why it's a problem: Using $json to access data from a node that is not the immediately previous one
How to avoid: Use $('NodeName').first().json to reference any upstream node by name, regardless of position in the pipeline.
Why it's a problem: Not merging parallel branches before the LLM node, making one branch's data invisible
How to avoid: Add a Merge node to combine parallel branches into a single data stream before the LLM node.
Why it's a problem: Passing an array of items to the LLM node when it expects a single prompt string
How to avoid: Add a Code node that aggregates multiple items into a single formatted text string.
Why it's a problem: Using hardcoded node names in expressions that break when nodes are renamed
How to avoid: Use descriptive, stable node names and avoid renaming nodes after building expressions that reference them.
Why it's a problem: Not handling the case where an upstream node has no output data
How to avoid: Wrap $('NodeName') calls in try-catch blocks and provide defaults when the node data is unavailable.
Best practices
- Use a dedicated Context Collector Code node before the LLM to centralize all data access in one place
- Reference upstream nodes by name using $('NodeName') instead of relying on $json which only sees the previous node
- Merge parallel branches with a Merge node before data needs to reach the LLM node
- Aggregate multi-item data into a single formatted text before passing to the LLM prompt
- Rename nodes to descriptive names to make $('NodeName') references self-documenting
- Always provide fallback defaults for optional context fields to prevent 'undefined' in prompts
- Keep the context object flat (no deep nesting) so LLM node expressions are simple $json.fieldName references
- Log which data sources were available in the context object for debugging missing data
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My n8n workflow has a Webhook, a Database Query, and a Redis lookup on parallel branches that all need to feed into an AI Agent prompt. Some variables show as undefined in the prompt. How do I collect data from all branches into a single context object that the AI Agent can access?
Create a Code node that uses $('NodeName') to reach back to my Webhook, Postgres, and Redis nodes, collects all relevant user context into a single output item, and makes it available to the AI Agent node with simple $json expressions.
Frequently asked questions
Why does $json.userName show undefined even though the Webhook has the data?
$json references the immediately previous node, not the Webhook. If there are other nodes between the Webhook and your current node, use $('Webhook').first().json.body.userName to reference the Webhook directly.
Can I access data from a node on a different branch?
No, you can only access nodes that are in your direct upstream path. If data comes from a parallel branch, add a Merge node to bring both branches together before the node that needs the data.
What does $input.all() return vs $json?
$json is shorthand for $input.first().json and returns the JSON data of the first item. $input.all() returns an array of all items from the previous node, each wrapped in { json: {...} }.
How do I pass 50 database rows to an LLM prompt?
Use a Code node to aggregate the rows into a formatted text string. Convert each row to a readable line and join them. Then pass the formatted string as a single field that the LLM prompt references.
Can I use $('NodeName') in the AI Agent's system message field?
Yes, $('NodeName') works in any expression field. However, for complex prompts with multiple references, build the prompt in a Code node and reference the output with a single {{ $json.systemPrompt }} expression for cleaner configuration.
What happens if the node I reference with $('NodeName') has not executed?
The expression throws an error or returns undefined. Wrap the reference in a try-catch in a Code node, or ensure the referenced node is always in the execution path by restructuring the workflow.
How do I rename a node without breaking existing expressions?
n8n automatically updates expressions when you rename a node through the UI. However, Code node strings that reference $('OldName') are not updated automatically. Search your Code nodes for the old name and update manually.
Can RapidDev help design n8n workflows with complex data flows?
Yes, RapidDev builds production n8n workflows with properly scoped variables, multi-source context aggregation, and clean data pipelines. Their team can restructure workflows to eliminate scoping issues and improve maintainability.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation