added explicit 10-step procedure with inputs/outputs, created decision point branches for code nodes/webhook data/node references, documented n8n instance setup and $env blocking as input dependency, formalized output contract with validation checklist, and added outcome signals tied to editor preview behavior and workflow execution.
intent
use this skill when writing n8n expressions, troubleshooting {{}} syntax errors, accessing $json/$node variables, or validating dynamic content in workflows. the skill covers expression format, core variables ($json, $node, $now, $env), common patterns, validation rules, and debugging techniques. return to this whenever you're unsure if an expression is valid or why it's failing.
inputs
n8n workflow context
- current node and its output data structure
- names of any previously executed nodes you want to reference
- webhook node output (if applicable)
external connections
- n8n instance with expression editor available (accessed via "fx" icon on any field supporting expressions)
- optional: n8n-mcp validation tools for automated expression checking
- optional: environment variables (requires N8N_BLOCK_ENV_ACCESS_IN_NODE not enabled; default is access allowed)
environment setup
- for $env access: verify your n8n admin has not enabled N8N_BLOCK_ENV_ACCESS_IN_NODE restriction
- for credential access: use n8n's credential system directly, not expressions
- for webhook data: confirm webhook node is upstream in workflow
procedure
identify the expression context
- input: node type (webhook, HTTP request, code node, etc.), field name where expression will run
- output: confirmed whether expressions are allowed in this field
- note: code nodes do NOT use {{}} syntax; credential fields do NOT use expressions
choose the correct variable
- input: where you need data from (current node output, other node output, environment, timestamp)
- decision: use $json for current node, $node["name"] for other nodes, $env for env vars, $now for timestamps
- output: variable prefix selected
construct the path to your data
- input: data structure of the source (nested objects, arrays, field names with spaces)
- if field name has spaces or special characters: use bracket notation {{$json['field name']}}
- if field name is simple alphanumeric: use dot notation {{$json.fieldname}}
- if accessing array element: use index {{$json.items[0].name}}
- if accessing nested objects: chain with dot {{$json.user.profile.email}}
- output: complete data path constructed
wrap in double curly braces
- input: constructed expression path
- output: {{expression}} with exact braces and no nesting
- validate: check for {{ }} (correct), { } (wrong), {{{ }}} (wrong)
handle webhook data specially
- input: if source is webhook node, confirm data location
- webhook structure: webhook wraps incoming data under .body property, with headers/params/query at root
- correct: {{$json.body.name}}, {{$json.body.email}}
- incorrect: {{$json.name}}, {{$json.email}}
- output: webhook data accessed from .body path only
reference other nodes with quoted names
- input: target node name from workflow
- if node name has spaces: {{$node["HTTP Request"].json.data}}
- if node name has no spaces: {{$node["Set"].json.value}}
- case-sensitive: "http request" != "HTTP Request" - must match exactly
- output: node reference with correct name and quoting
test in expression editor
- input: completed expression
- click "fx" icon on the field
- expression editor opens with live preview
- output: preview shows either result value or error message in red
- resolve: if red error appears, check for typos, missing braces, incorrect node names, invalid paths
handle $env with fallbacks if access is blocked
- input: $env variable reference
- test: if {{$env.API_KEY}} returns error about blocked access
- fallback option 1: store value in n8n credentials instead of environment
- fallback option 2: use Set node with manually entered value
- fallback option 3: pass value through webhook query parameter {{$json.query.api_key}}
- output: confirmed working variable source
use advanced methods for data transformation
- input: need to manipulate strings, arrays, dates, or numbers
- string methods: .toLowerCase(), .toUpperCase(), .trim(), .replace(), .substring(), .split()
- array methods: .length, .map(), .filter(), .find(), .join(), .slice()
- date methods: .toFormat('yyyy-MM-dd'), .toISO(), .plus({days: 7}), .minus({hours: 24})
- number operations: * 1.1 (multiply), + 5 (add), % (modulo)
- output: expression with method chained
validate final expression
- input: completed expression with all braces, paths, and methods
- checklist: (1) wrapped in {{ }}, (2) no double nesting {{{ }}}, (3) node names quoted if they have spaces, (4) webhook data under .body if applicable, (5) no expressions in code nodes or credential fields, (6) test in editor shows result not error
- output: expression validated and ready to deploy
decision points
is this a code node?
- if yes: do NOT use {{ }} syntax. use direct javascript access like $json.field or $input.item.json.field
- if no: use {{ }} syntax for all dynamic content
does the field name have spaces or special characters?
- if yes: use bracket notation {{$json['field name']}} or {{$json['Cena brutto zł']}}
- if no: use dot notation {{$json.fieldname}}
are you accessing a webhook node?
- if yes: data is under .body property. use {{$json.body.name}} not {{$json.name}}
- if no: access root level data directly with {{$json.fieldname}}
are you referencing another node?
- if yes: use $node["Node Name"].json.path, with exact name match (case-sensitive) and quotes around any name with spaces
- if no: use $json for current node data
does your node name have spaces?
- if yes: wrap in quotes {{$node["HTTP Request"].json.data}}
- if no: no quotes needed {{$node["Set"].json.value}}
do you need environment variables and $env returns an error?
- if error says "blocked": fallback to n8n credentials system or Set node with static values
- if error says "undefined": variable name is wrong, check env var name spelling
- if working: use {{$env.VARIABLE_NAME}} as-is
are you in a credential field?
- if yes: do NOT use expressions. use n8n credential system
- if no: expressions are allowed in most node fields
do you need to transform data (uppercase, format dates, math)?
- if yes: chain methods onto your variable like {{$json.name.toUpperCase()}} or {{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}
- if no: use variable as-is
output contract
valid expression format
- wrapped in {{ }} with no nesting
- all node names with spaces quoted in double quotes
- all field names with spaces or special characters in bracket notation
- data paths correctly chained with dots for objects, brackets for arrays
- webhook data accessed under .body if applicable
- methods (if used) correctly chained and spelled
test result
- expression editor shows computed value (not error in red)
- if testing in workflow: output matches expected data type and content
- if field expects string: result is string
- if field expects number: result is number
- if field expects object: result is object
error-free deployment
- expression does not throw "Cannot read property X of undefined"
- expression does not throw "X is not a function"
- expression does not appear as literal text in output (which means braces were missing)
- workflow executes successfully without expression-related failures
outcome signal
- expression editor preview displays the correct value without red error text
- workflow node executes without throwing expression syntax errors
- subsequent nodes receive the expected data from your expression
- if debugging: you identified which validation rule was broken and fixed it (missing braces, wrong node name case, missing quotes, wrong variable path)
validation rules quick reference
- always use {{ }} - expressions must be wrapped in double curly braces
- use quotes for spaces and special characters - bracket notation is mandatory for keys with spaces, diacritics, or symbols
- match exact node names - node references are case-sensitive and must match the workflow exactly
- no nested {{ }} - don't double-wrap expressions
- webhook data is under .body - most common mistake is trying to access {{$json.name}} when it's {{$json.body.name}}
common mistakes and fixes
| mistake |
fix |
| $json.field |
{{$json.field}} |
| {{$json.field name}} |
{{$json['field name']}} |
| {{$node.HTTP Request}} |
{{$node["HTTP Request"]}} |
| {{{$json.field}}} |
{{$json.field}} |
| {{$json.name}} (webhook) |
{{$json.body.name}} |
| '={{$json.email}}' (code node) |
$json.email |
| {{$node.Set}} (node with spaces) |
{{$node["Set"]}} or {{$node["HTTP Request"]}} if that's the name |
working examples
example 1: webhook to slack
webhook receives:
{
"body": {
"name": "John Doe",
"email": "john@example.com",
"message": "Hello!"
}
}
in slack node text field:
New form submission!
Name: {{$json.body.name}}
Email: {{$json.body.email}}
Message: {{$json.body.message}}
example 2: http request to email
http request returns:
{
"data": {
"items": [
{"name": "Product 1", "price": 29.99}
]
}
}
in email node (referencing http request):
Product: {{$node["HTTP Request"].json.data.items[0].name}}
Price: ${{$node["HTTP Request"].json.data.items[0].price}}
example 3: format timestamp
{{$now.toFormat('yyyy-MM-dd')}} // 2025-10-20
{{$now.toFormat('HH:mm:ss')}} // 14:30:45
{{$now.toFormat('yyyy-MM-dd HH:mm')}} // 2025-10-20 14:30
{{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}} // add 7 days
data type handling
arrays
{{$json.users[0].email}} // first item
{{$json.users.length}} // array length
{{$json.users[$json.users.length - 1].name}} // last item
objects
{{$json.user.email}} // dot notation (no spaces)
{{$json['user data'].email}} // bracket notation (with spaces)
strings
Hello {{$json.name}}! // concatenation (automatic)
{{$json.email.toLowerCase()}} // string method
{{$json.name.toUpperCase()}}
numbers
{{$json.price}} // direct use
{{$json.price * 1.1}} // add 10%
{{$json.quantity + 5}} // add 5
advanced patterns
conditional content
{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}
{{$json.email || 'no-email@example.com'}} // default value
date manipulation
{{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}
{{$now.minus({hours: 24}).toISO()}}
{{DateTime.fromISO('2025-12-25').toFormat('MMMM dd, yyyy')}}
string manipulation
{{$json.email.substring(0, 5)}}
{{$json.message.replace('old', 'new')}}
{{$json.tags.split(',').join(', ')}}
debugging expressions
test in expression editor
- click field with expression
- open expression editor (click "fx" icon)
- see live preview of result
- check for errors highlighted in red
common error messages
- "Cannot read property X of undefined" - parent object doesn't exist, check your data path
- "X is not a function" - trying to call method on non-function, check variable type
- expression shows as literal text - missing {{ }}, add curly braces
best practices
do
- always use {{ }} for dynamic content
- use bracket notation for field names with spaces
- reference webhook data from .body
- use $node for data from other nodes
- test expressions in expression editor
don't
- don't use expressions in code nodes
- don't forget quotes around node names with spaces
- don't double-wrap with extra {{ }}
- don't assume webhook data is at root (it's under .body)
- don't use expressions in webhook paths or credential fields
originally written by czlonkowski at skills.sh.