Broken variable interpolation in n8n system prompts happens when you use incorrect expression syntax, reference nodes that have not executed yet, or mix literal curly braces with n8n's {{ }} expression delimiters. Fix it by using the correct {{ $json.field }} syntax, ensuring referenced nodes are upstream in the workflow, and escaping literal curly braces with the $json approach or by using a Code node to build the prompt string.
Fixing Expression Syntax and Variable Interpolation in n8n LLM Prompts
n8n uses the {{ }} expression syntax to inject dynamic values into node fields. When this syntax is used in system prompts for LLM nodes, several issues can arise: the expression fails silently and injects 'undefined', literal curly braces in prompt templates conflict with n8n's expression parser, or the referenced node has not executed yet. This tutorial covers every common cause of broken variable interpolation and provides reliable patterns for dynamic system prompts.
Prerequisites
- A running n8n instance with an AI Agent or Basic LLM Chain node
- At least one upstream node providing dynamic data (Webhook, Database, etc.)
- Basic understanding of n8n expressions and the {{ }} syntax
- Familiarity with JSON data structures
Step-by-step guide
Understand how n8n evaluates expressions in prompt fields
Understand how n8n evaluates expressions in prompt fields
Every field in n8n that shows a small expression icon (the fx symbol) supports expressions. When you type {{ $json.name }} in a field, n8n evaluates this at runtime and replaces it with the actual value from the incoming data. The evaluation happens just before the node executes, using data from the most recent upstream node. Critical rules: expressions must be wrapped in {{ }}. The dollar sign variables $json, $input, and $('NodeName') are n8n-specific. If the expression cannot be resolved, n8n injects the string 'undefined' without throwing an error. This silent failure is the most common cause of broken interpolation because the workflow continues to run but the prompt contains the literal text 'undefined' instead of the expected value.
1// Correct syntax examples:2{{ $json.userName }} // Field from previous node3{{ $('Webhook').first().json.body.message }} // Field from named node4{{ $input.first().json.email }} // Explicit input reference5{{ $execution.id }} // Execution metadata67// WRONG syntax:8{ $json.userName } // Missing outer braces9{{ json.userName }} // Missing $ prefix10{{ $json['user name'] }} // Correct for keys with spaces11{{ $json.user.name }} // Correct for nested objectsExpected result: You understand that expressions use {{ }} syntax and that unresolved expressions silently inject 'undefined'.
Fix references to nodes that have not executed yet
Fix references to nodes that have not executed yet
A common mistake is referencing a node that is downstream or on a different branch of the workflow. Expressions can only reference nodes that have already executed in the current run and are connected upstream via the data flow. If you reference $('DatabaseQuery') but the Database Query node is on a parallel branch that has not completed, the expression resolves to undefined. To fix this, ensure the referenced node is directly upstream of the LLM node. If data comes from a parallel branch, add a Merge node before the LLM node that combines both branches. Use the Merge node in Combine mode with Merge by Position to ensure all data is available before the prompt is built.
Expected result: All referenced nodes appear in the expression editor's variable selector and their values resolve correctly.
Escape literal curly braces in prompt templates
Escape literal curly braces in prompt templates
If your system prompt contains literal curly braces, for example a JSON template like '{"name": "value"}' that you want the LLM to follow as a format example, n8n's expression parser will try to evaluate everything inside {{ }}. This breaks the prompt. There are two solutions. First, if the curly braces are NOT inside {{ }}, they are treated as literal text in most n8n versions since v1.0. Only double curly braces {{ trigger expression evaluation. However, if you need to show the literal text {{ in your prompt, you must build the prompt in a Code node where you have full control over the string. Second, use a Code node to construct the entire prompt as a JavaScript string and output it as a field, then reference that field in the LLM node's system prompt.
1// Code node: Build system prompt with literal curly braces2const userName = $input.first().json.userName || 'User';3const outputFormat = '{"response": "your answer", "confidence": 0.95}';45const systemPrompt = `You are a helpful assistant for ${userName}.67Always respond in this exact JSON format:8${outputFormat}910Do not include any text outside the JSON object.`;1112return [{13 json: {14 systemPrompt: systemPrompt15 }16}];Expected result: The system prompt contains literal curly braces as intended, without n8n trying to evaluate them as expressions.
Handle undefined values with fallback defaults
Handle undefined values with fallback defaults
When a variable might not exist in the incoming data, your expression should include a fallback. Without a fallback, the prompt receives the string 'undefined' which confuses the LLM. Use JavaScript's logical OR operator or nullish coalescing inside the expression to provide defaults. For example, {{ $json.userName || 'valued customer' }} returns 'valued customer' when userName is undefined, null, or empty. For more complex fallback logic, use a Code node that validates all required fields and assigns defaults before the data reaches the LLM node.
1// In expression fields, use OR for fallbacks:2{{ $json.userName || 'there' }}3{{ $json.language || 'English' }}4{{ $json.context || 'No additional context provided.' }}56// Code node for comprehensive validation:7const item = $input.first().json;89const validated = {10 userName: item.userName || item.name || 'valued customer',11 language: item.language || item.locale || 'English',12 context: item.context || '',13 maxLength: item.maxLength || 500,14 tone: item.tone || 'professional'15};1617return [{ json: validated }];Expected result: Dynamic variables in the system prompt always resolve to meaningful values, never to 'undefined' or 'null'.
Debug expressions using the expression editor preview
Debug expressions using the expression editor preview
n8n's expression editor shows a live preview of the resolved value. Use this to verify that your expressions work before running the workflow. Click the fx icon next to any field to open the editor. On the left, browse available nodes and their output fields. On the right, see the expression and its resolved value. If the preview shows 'undefined' or an error, the expression is broken. Common issues visible in the preview include: the referenced node showing no data (it has not been executed in the current test), the field path being incorrect (typo in the property name), or the data type being wrong (referencing an array index that does not exist). Fix the expression until the preview shows the expected value, then close the editor.
Expected result: The expression editor preview shows the correct resolved value for every variable in the system prompt.
Use a Code node to build complex multi-section system prompts
Use a Code node to build complex multi-section system prompts
For system prompts with multiple dynamic sections, conditional content, and formatted text, a Code node is more reliable and maintainable than inline expressions. Build the entire prompt as a JavaScript template literal in the Code node, referencing all necessary input data. This approach lets you use conditionals, loops, and string manipulation that are impossible with inline expressions. Output the completed prompt as a single field, then reference it in the LLM node's system prompt with a simple {{ $json.systemPrompt }} expression. This pattern separates prompt construction logic from the LLM configuration.
1// Code node: Complex System Prompt Builder2const data = $input.first().json;34const sections = [];56// Role section7sections.push(`You are a ${data.role || 'helpful assistant'} for ${data.company || 'our company'}.`);89// Knowledge section (conditional)10if (data.knowledgeBase) {11 sections.push(`## Knowledge Base\nUse the following information to answer questions:\n${data.knowledgeBase}`);12}1314// Rules section15const rules = data.rules || ['Be concise', 'Be accurate'];16sections.push('## Rules');17rules.forEach((rule, i) => {18 sections.push(`${i + 1}. ${rule}`);19});2021// Output format section22if (data.outputFormat === 'json') {23 sections.push('## Output Format\nRespond ONLY with valid JSON. No text outside the JSON object.');24}2526return [{27 json: {28 systemPrompt: sections.join('\n\n')29 }30}];Expected result: A complete, dynamically constructed system prompt with all variables resolved and conditional sections included.
Complete working example
1// Code node: Production-Ready Dynamic Prompt Builder2// Mode: Run Once for All Items3// Place before any AI Agent or LLM Chain node45const items = $input.all();6const results = [];78for (const item of items) {9 const d = item.json;1011 // Validate required fields12 const warnings = [];13 if (!d.userName) warnings.push('userName is missing, using default');14 if (!d.userMessage) warnings.push('userMessage is missing');1516 // Build system prompt with safe defaults17 const role = d.agentRole || 'customer support specialist';18 const company = d.companyName || 'Acme Corp';19 const lang = d.language || 'English';20 const maxWords = d.maxResponseWords || 200;2122 let prompt = `You are a ${role} for ${company}.\n`;23 prompt += `Respond in ${lang}.\n`;24 prompt += `Keep responses under ${maxWords} words.\n\n`;2526 // Add knowledge base if provided27 if (d.knowledgeBase && d.knowledgeBase.trim()) {28 prompt += `## Reference Information\n${d.knowledgeBase}\n\n`;29 }3031 // Add output format instructions32 if (d.outputFormat === 'json') {33 const schema = d.jsonSchema || '{"answer": "string"}';34 prompt += `## Output Format\n`;35 prompt += `Respond ONLY with valid JSON matching this schema:\n`;36 prompt += `${schema}\n\n`;37 } else {38 prompt += `## Output Format\nUse markdown formatting.\n\n`;39 }4041 // Add custom rules42 if (d.rules && Array.isArray(d.rules)) {43 prompt += `## Rules\n`;44 d.rules.forEach((rule, i) => {45 prompt += `${i + 1}. ${rule}\n`;46 });47 }4849 results.push({50 json: {51 systemPrompt: prompt,52 userMessage: d.userMessage || '',53 userName: d.userName || 'User',54 warnings: warnings55 }56 });57}5859return results;Common mistakes when fixing Broken Variable Interpolation in System Prompts in n8n
Why it's a problem: Using single curly braces { } instead of double {{ }} for expressions
How to avoid: Always use double curly braces {{ expression }} for n8n expressions. Single braces are treated as literal text.
Why it's a problem: Referencing a node that has not executed yet or is on a different branch
How to avoid: Ensure the referenced node is directly upstream. Add a Merge node if data comes from a parallel branch.
Why it's a problem: Not providing fallback values, causing 'undefined' to appear in the prompt
How to avoid: Use {{ $json.field || 'default value' }} for every dynamic variable to ensure a fallback.
Why it's a problem: Including literal {{ }} in prompt templates, which n8n interprets as expressions
How to avoid: Build prompts with literal curly braces in a Code node using JavaScript template literals, then reference the output field.
Why it's a problem: Using $json without the dollar sign, writing {{ json.field }} instead of {{ $json.field }}
How to avoid: Always include the $ prefix: $json, $input, $execution. Without the $, n8n cannot resolve the variable.
Best practices
- Always use the expression editor preview to verify values resolve correctly before running the workflow
- Provide fallback defaults for every dynamic variable using the || operator to prevent 'undefined' in prompts
- Build complex prompts in a Code node rather than chaining multiple inline expressions
- Reference the built prompt with a single simple expression like {{ $json.systemPrompt }} in the LLM node
- Validate required fields in a Code node before building the prompt to catch missing data early
- Use the variable selector in the expression editor to browse available fields instead of typing paths manually
- Keep literal curly braces in prompts by building the prompt string in a Code node where they are not parsed as expressions
- Test expressions by executing upstream nodes first so the expression editor has real data to preview
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My n8n workflow has a system prompt with {{ $json.userName }} but it keeps showing 'undefined' in the prompt sent to the LLM. The Webhook node provides the userName field. How do I fix the expression and add proper fallback values?
Create a Code node that takes user data from a Webhook and builds a dynamic system prompt with the user's name, preferred language, and a JSON output format template that includes literal curly braces. Output it as a systemPrompt field I can reference in the AI Agent node.
Frequently asked questions
Why does my expression show 'undefined' instead of the actual value?
The field path is wrong, the referenced node has not executed, or the incoming data does not contain that field. Open the expression editor and use the variable selector to browse available fields. Check that the upstream node has been executed.
Can I use JavaScript functions inside {{ }} expressions?
Yes, n8n expressions support JavaScript. You can use string methods like {{ $json.name.toUpperCase() }}, ternary operators like {{ $json.age > 18 ? 'adult' : 'minor' }}, and Math functions. However, complex logic is better placed in a Code node.
How do I include a literal {{ in my system prompt?
You cannot include literal {{ }} in expression-enabled fields because n8n will try to evaluate them. Build the prompt in a Code node using JavaScript strings where curly braces are treated as regular characters.
What is the difference between $json and $input.first().json?
$json is a shortcut for $input.first().json when there is only one input. They are equivalent in most cases. Use $input.all() when you need to process multiple items, and $('NodeName') when referencing a specific upstream node.
Can I reference data from a node that is two steps upstream?
Yes, use the $('NodeName') syntax to reference any upstream node by name, regardless of how many nodes are between them. For example, {{ $('Webhook').first().json.body.userId }} reaches back to the Webhook node.
Why does my expression work in the editor preview but fail at runtime?
The editor preview uses pinned or cached data from the last execution. If the runtime data has a different structure (e.g., the field was renamed or is missing in some cases), the expression fails. Add fallback defaults to handle missing fields.
How do I pass an array to a system prompt?
Convert the array to a string first. Use {{ $json.items.join(', ') }} for a comma-separated list, or build a formatted list in a Code node with items.map((item, i) => (i+1) + '. ' + item).join('\n').
Can RapidDev help with complex n8n expression and prompt configuration?
Yes, RapidDev's engineering team builds production n8n workflows with dynamic prompt systems. They can help design reliable expression patterns, Code node prompt builders, and error-resistant data flows for LLM integrations.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation