n8n Expression Testing
<default_to_action> When testing n8n expressions:
-
VALIDATE syntax before execution
-
TEST with multiple context scenarios
-
CHECK for null/undefined handling
-
VERIFY type safety
-
SCAN for security vulnerabilities
Quick Expression Checklist:
-
Valid JavaScript syntax
-
Context variables properly referenced ($json, $node)
-
Null-safe access patterns (?., ??)
-
No dangerous functions (eval, Function)
-
Efficient for large data sets
Common Pitfalls:
-
Accessing nested properties without null checks
-
Type coercion issues
-
Missing fallback values
-
Inefficient array operations </default_to_action>
Quick Reference Card
n8n Expression Syntax
Pattern Example Description
Basic access {{ $json.field }}
Access JSON field
Nested access {{ $json.user.email }}
Access nested property
Array access {{ $json.items[0] }}
Access array element
Node reference {{ $node["Name"].json.id }}
Access other node's data
Method call {{ $json.name.toLowerCase() }}
Call string method
Conditional {{ $json.x ? "yes" : "no" }}
Ternary expression
Context Variables
Variable Description Example
$json
Current item data {{ $json.email }}
$node["Name"]
Other node's data {{ $node["HTTP"].json.body }}
$items()
Multiple items {{ $items("Node", 0, 0).json }}
$now
Current timestamp {{ $now.toISO() }}
$today
Today's date {{ $today }}
$runIndex
Run iteration {{ $runIndex }}
$workflow
Workflow info {{ $workflow.name }}
Expression Syntax Patterns
Safe Data Access
// BAD: Can fail if nested objects are null {{ $json.user.profile.email }}
// GOOD: Optional chaining with fallback {{ $json.user?.profile?.email ?? '' }}
// BAD: Array access without bounds check {{ $json.items[0].name }}
// GOOD: Safe array access {{ $json.items?.[0]?.name ?? 'No items' }}
Type Conversions
// String to Number {{ parseInt($json.quantity, 10) }} {{ parseFloat($json.price) }} {{ Number($json.value) }}
// Number to String {{ String($json.id) }} {{ $json.amount.toString() }} {{ $json.count.toFixed(2) }}
// Date handling {{ new Date($json.timestamp).toISOString() }} {{ DateTime.fromISO($json.date).toFormat('yyyy-MM-dd') }}
// Boolean conversion {{ Boolean($json.active) }} {{ $json.enabled === 'true' }}
String Operations
// Case conversion {{ $json.name.toLowerCase() }} {{ $json.name.toUpperCase() }} {{ $json.name.charAt(0).toUpperCase() + $json.name.slice(1) }}
// String manipulation {{ $json.text.trim() }} {{ $json.text.replace(/\s+/g, ' ') }} {{ $json.text.substring(0, 100) }}
// Template strings
{{ Hello, ${$json.firstName} ${$json.lastName}! }}
{{ Order #${$json.orderId} - ${$json.status} }}
Array Operations
// Mapping {{ $json.items.map(item => item.name) }} {{ $json.items.map(item => ({ id: item.id, total: item.price * item.qty })) }}
// Filtering {{ $json.items.filter(item => item.active) }} {{ $json.items.filter(item => item.price > 100) }}
// Reducing {{ $json.items.reduce((sum, item) => sum + item.price, 0) }} {{ $json.items.reduce((acc, item) => ({ ...acc, [item.id]: item }), {}) }}
// Finding {{ $json.items.find(item => item.id === $json.targetId) }} {{ $json.items.findIndex(item => item.name === 'target') }}
// Joining {{ $json.tags.join(', ') }} {{ $json.items.map(i => i.name).join(' | ') }}
Validation Patterns
// Validate expression syntax function validateExpressionSyntax(expression: string): ValidationResult { // Remove n8n template markers const code = expression.replace(/{{|}}/g, '').trim();
try {
// Check if valid JavaScript
new Function(return (${code}));
return { valid: true };
} catch (error) {
return {
valid: false,
error: error.message,
suggestion: suggestFix(error.message, code)
};
}
}
// Validate context variables function validateContextVariables(expression: string): string[] { const contextVars = ['$json', '$node', '$items', '$now', '$today', '$runIndex', '$workflow']; const usedVars = []; const invalidVars = [];
// Find all $ prefixed variables const varPattern = /$\w+/g; let match;
while ((match = varPattern.exec(expression)) !== null) { const varName = match[0]; if (contextVars.some(cv => varName.startsWith(cv))) { usedVars.push(varName); } else { invalidVars.push(varName); } }
return { usedVars, invalidVars }; }
// Test expression with sample data function testExpression(expression: string, context: any): TestResult { const code = expression.replace(/{{|}}/g, '').trim();
try {
// Create function with context
const fn = new Function('$json', '$node', '$items', '$now', '$today',
return (${code}));
const result = fn(
context.$json || {},
context.$node || {},
context.$items || (() => ({})),
context.$now || new Date(),
context.$today || new Date()
);
return { success: true, result };
} catch (error) { return { success: false, error: error.message }; } }
Common Errors and Fixes
Undefined Property Access
// ERROR: Cannot read property 'email' of undefined {{ $json.user.email }}
// FIX 1: Optional chaining {{ $json.user?.email }}
// FIX 2: With fallback {{ $json.user?.email ?? 'no-email@example.com' }}
// FIX 3: Conditional {{ $json.user ? $json.user.email : '' }}
Type Errors
// ERROR: toLowerCase is not a function (when null) {{ $json.name.toLowerCase() }}
// FIX: Null check first {{ $json.name?.toLowerCase() ?? '' }}
// ERROR: toFixed is not a function (string instead of number) {{ $json.price.toFixed(2) }}
// FIX: Parse as number first {{ parseFloat($json.price).toFixed(2) }}
// ERROR: map is not a function (not an array) {{ $json.items.map(i => i.name) }}
// FIX: Ensure array {{ (Array.isArray($json.items) ? $json.items : []).map(i => i.name) }}
Node Reference Errors
// ERROR: Node "Previous Node" not found {{ $node["Previous Node"].json.data }}
// FIX: Use exact node name (case-sensitive) {{ $node["Previous Node1"].json.data }}
// FIX: Add fallback for safety {{ $node["Previous Node"]?.json?.data ?? {} }}
Security Patterns
Dangerous Functions to Avoid
// DANGEROUS: Never use eval {{ eval($json.code) }}
// DANGEROUS: Dynamic function creation {{ new Function($json.code)() }}
// DANGEROUS: setTimeout with string {{ setTimeout($json.code, 1000) }}
// SAFE: Use explicit operations instead {{ $json.value * 2 }} {{ JSON.parse($json.jsonString) }}
Input Validation
// Validate email format {{ /^[^\s@]+@[^\s@]+.[^\s@]+$/.test($json.email) ? $json.email : '' }}
// Sanitize for HTML (basic) {{ $json.text.replace(/[<>&"']/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[c])) }}
// Limit string length {{ $json.input.substring(0, 1000) }}
// Validate number range {{ Math.min(Math.max(parseInt($json.value), 0), 100) }}
Performance Optimization
Efficient Array Operations
// SLOW: Multiple iterations {{ $json.items.filter(i => i.active).map(i => i.name).join(', ') }}
// FASTER: Single reduce
{{ $json.items.reduce((acc, i) => i.active ? (acc ? ${acc}, ${i.name} : i.name) : acc, '') }}
// SLOW: Nested loops {{ $json.items.map(i => $json.categories.find(c => c.id === i.categoryId)) }}
// FASTER: Create lookup map first (in Code node) const categoryMap = Object.fromEntries($json.categories.map(c => [c.id, c])); return $json.items.map(i => categoryMap[i.categoryId]);
Avoid in Expressions
// AVOID: Complex logic in expressions {{ $json.items.reduce((acc, item) => { const category = $json.categories.find(c => c.id === item.catId); if (category && category.active) { acc.push({ ...item, categoryName: category.name }); } return acc; }, []) }}
// BETTER: Move to Code node for complex transformations
Testing Patterns
// Expression test suite const expressionTests = [ { name: 'Basic property access', expression: '{{ $json.name }}', context: { $json: { name: 'John' } }, expected: 'John' }, { name: 'Nested with optional chaining', expression: '{{ $json.user?.email ?? "default" }}', context: { $json: { user: null } }, expected: 'default' }, { name: 'Array mapping', expression: '{{ $json.items.map(i => i.id).join(",") }}', context: { $json: { items: [{ id: 1 }, { id: 2 }] } }, expected: '1,2' }, { name: 'Conditional expression', expression: '{{ $json.score >= 70 ? "Pass" : "Fail" }}', context: { $json: { score: 85 } }, expected: 'Pass' }, { name: 'Node reference', expression: '{{ $node["Previous"].json.result }}', context: { $node: { Previous: { json: { result: 'success' } } } }, expected: 'success' } ];
// Run tests
for (const test of expressionTests) {
const result = testExpression(test.expression, test.context);
console.log(${test.name}: ${result.result === test.expected ? 'PASS' : 'FAIL'});
}
Agent Coordination
Memory Namespace
aqe/n8n/expressions/ ├── validations/* - Expression validation results ├── patterns/* - Discovered expression patterns ├── errors/* - Common error catalog └── optimizations/* - Performance suggestions
Fleet Coordination
// Coordinate expression validation with workflow testing await Task("Validate expressions", { workflowId: "wf-123", validateAll: true, testWithSampleData: true }, "n8n-expression-validator");
Related Skills
-
n8n-workflow-testing-fundamentals - Workflow testing
-
n8n-security-testing - Security validation
Remember
n8n expressions are JavaScript-like with special context variables ($json, $node, etc.). Testing requires:
-
Syntax validation
-
Context variable verification
-
Null safety checks
-
Type compatibility
-
Security scanning
Key patterns: Use optional chaining (?. ) and nullish coalescing (?? ) for safety. Move complex logic to Code nodes. Always test with edge cases (null, undefined, empty arrays).