fix: correct misleading Code node documentation based on real-world testing

Critical fixes based on Claude Desktop feedback:

1. Fixed crypto documentation: require('crypto') IS available despite editor warnings
   - Added clear examples of crypto usage
   - Updated validation to guide correct require() usage

2. Clarified $helpers vs standalone functions
   - $getWorkflowStaticData() is standalone, NOT $helpers.getWorkflowStaticData()
   - Added validation to catch incorrect usage (prevents '$helpers is not defined' errors)
   - Enhanced examples showing proper $helpers availability checks

3. Fixed JMESPath numeric literal documentation
   - n8n requires backticks around numbers in filters: [?age >= `18`]
   - Added multiple examples and validation to detect missing backticks
   - Prevents 'JMESPath syntax error' that Claude Desktop encountered

4. Fixed webhook data access gotcha
   - Webhook payload is at items[0].json.body, NOT items[0].json
   - Added dedicated 'Webhook Data Access' section with clear examples
   - Created process_webhook_data task template
   - Added validation to detect incorrect webhook data access patterns

All fixes based on production workflows TaNqYoZNNeHC4Hne and JZ9urD7PNClDZ1bm

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-10 09:22:34 +02:00
parent 20f018f8dc
commit 99e74cf22a
14 changed files with 3072 additions and 73 deletions

View File

@@ -391,12 +391,18 @@ export class ConfigValidator {
* Check for common configuration issues
*/
private static checkCommonIssues(
_nodeType: string,
nodeType: string,
config: Record<string, any>,
properties: any[],
warnings: ValidationWarning[],
suggestions: string[]
): void {
// Skip visibility checks for Code nodes as they have simple property structure
if (nodeType === 'nodes-base.code') {
// Code nodes don't have complex displayOptions, so skip visibility warnings
return;
}
// Check for properties that won't be used
const visibleProps = properties.filter(p => this.isPropertyVisible(p, config));
const configuredKeys = Object.keys(config);
@@ -562,20 +568,133 @@ export class ConfigValidator {
warnings.push({
type: 'missing_common',
message: 'No return statement found',
suggestion: 'Code node should return data for the next node. Add: return items (Python) or return items; (JavaScript)'
suggestion: 'Code node must return data. Example: return [{json: {result: "success"}}]'
});
}
// Check for common n8n patterns
// Check return format for JavaScript
if (language === 'javascript' && hasReturn) {
// Check for common incorrect return patterns
if (/return\s+items\s*;/.test(code) && !code.includes('.map') && !code.includes('json:')) {
warnings.push({
type: 'best_practice',
message: 'Returning items directly - ensure each item has {json: ...} structure',
suggestion: 'If modifying items, use: return items.map(item => ({json: {...item.json, newField: "value"}}))'
});
}
// Check for return without array
if (/return\s+{[^}]+}\s*;/.test(code) && !code.includes('[') && !code.includes(']')) {
warnings.push({
type: 'invalid_value',
message: 'Return value must be an array',
suggestion: 'Wrap your return object in an array: return [{json: {your: "data"}}]'
});
}
// Check for direct data return without json wrapper
if (/return\s+\[['"`]/.test(code) || /return\s+\[\d/.test(code)) {
warnings.push({
type: 'invalid_value',
message: 'Items must be objects with json property',
suggestion: 'Use format: return [{json: {value: "data"}}] not return ["data"]'
});
}
}
// Check return format for Python
if (language === 'python' && hasReturn) {
// Check for common incorrect patterns
if (/return\s+items\s*$/.test(code) && !code.includes('json') && !code.includes('dict')) {
warnings.push({
type: 'best_practice',
message: 'Returning items directly - ensure each item is a dict with "json" key',
suggestion: 'Use: return [{"json": item.json} for item in items]'
});
}
// Check for dict return without list
if (/return\s+{['"]/.test(code) && !code.includes('[') && !code.includes(']')) {
warnings.push({
type: 'invalid_value',
message: 'Return value must be a list',
suggestion: 'Wrap your return dict in a list: return [{"json": {"your": "data"}}]'
});
}
}
// Check for common n8n variables and patterns
if (language === 'javascript') {
if (!code.includes('items') && !code.includes('$input')) {
// Check if accessing items/input
if (!code.includes('items') && !code.includes('$input') && !code.includes('$json')) {
warnings.push({
type: 'missing_common',
message: 'Code doesn\'t reference input items',
suggestion: 'Access input data with: items or $input.all()'
message: 'Code doesn\'t reference input data',
suggestion: 'Access input with: items, $input.all(), or $json (in single-item mode)'
});
}
// Check for common mistakes with $json
if (code.includes('$json') && !code.includes('mode')) {
warnings.push({
type: 'best_practice',
message: '$json only works in "Run Once for Each Item" mode',
suggestion: 'For all items mode, use: items[0].json or loop through items'
});
}
// Check for undefined variable usage
const commonVars = ['$node', '$workflow', '$execution', '$prevNode', 'DateTime', 'jmespath'];
const usedVars = commonVars.filter(v => code.includes(v));
// Check for incorrect $helpers usage patterns
if (code.includes('$helpers.getWorkflowStaticData')) {
warnings.push({
type: 'invalid_value',
message: '$helpers.getWorkflowStaticData() is incorrect - causes "$helpers is not defined" error',
suggestion: 'Use $getWorkflowStaticData() as a standalone function (no $helpers prefix)'
});
}
// Check for $helpers usage without checking availability
if (code.includes('$helpers') && !code.includes('typeof $helpers')) {
warnings.push({
type: 'best_practice',
message: '$helpers availability varies by n8n version',
suggestion: 'Check availability first: if (typeof $helpers !== "undefined" && $helpers.httpRequest) { ... }'
});
}
// Check for async without await
if (code.includes('async') || code.includes('.then(')) {
if (!code.includes('await')) {
warnings.push({
type: 'best_practice',
message: 'Using async operations without await',
suggestion: 'Use await for async operations: await $helpers.httpRequest(...)'
});
}
}
// Check for crypto usage without require
if ((code.includes('crypto.') || code.includes('randomBytes') || code.includes('randomUUID')) && !code.includes('require')) {
warnings.push({
type: 'invalid_value',
message: 'Using crypto without require statement',
suggestion: 'Add: const crypto = require("crypto"); at the beginning (ignore editor warnings)'
});
}
// Check for console.log (informational)
if (code.includes('console.log')) {
warnings.push({
type: 'best_practice',
message: 'console.log output appears in n8n execution logs',
suggestion: 'Remove console.log statements in production or use them sparingly'
});
}
} else if (language === 'python') {
// Python-specific checks
if (!code.includes('items') && !code.includes('_input')) {
warnings.push({
type: 'missing_common',
@@ -583,6 +702,44 @@ export class ConfigValidator {
suggestion: 'Access input data with: items variable'
});
}
// Check for print statements
if (code.includes('print(')) {
warnings.push({
type: 'best_practice',
message: 'print() output appears in n8n execution logs',
suggestion: 'Remove print statements in production or use them sparingly'
});
}
// Check for common Python mistakes
if (code.includes('import requests') || code.includes('import pandas')) {
warnings.push({
type: 'invalid_value',
message: 'External libraries not available in Code node',
suggestion: 'Only Python standard library is available. For HTTP requests, use JavaScript with $helpers.httpRequest'
});
}
}
// Check for infinite loops
if (/while\s*\(\s*true\s*\)|while\s+True:/.test(code)) {
warnings.push({
type: 'security',
message: 'Infinite loop detected',
suggestion: 'Add a break condition or use a for loop with limits'
});
}
// Check for error handling
if (!code.includes('try') && !code.includes('catch') && !code.includes('except')) {
if (code.length > 200) { // Only suggest for non-trivial code
warnings.push({
type: 'best_practice',
message: 'No error handling found',
suggestion: 'Consider adding try/catch (JavaScript) or try/except (Python) for robust error handling'
});
}
}
}
}

View File

@@ -213,7 +213,7 @@ export class EnhancedConfigValidator extends ConfigValidator {
break;
case 'nodes-base.code':
// Code node uses base validation which includes syntax checks
NodeSpecificValidators.validateCode(context);
break;
case 'nodes-base.openAi':

View File

@@ -75,50 +75,330 @@ export class ExampleGenerator {
}
},
// Webhook data processing example
'nodes-base.code.webhookProcessing': {
minimal: {
language: 'javaScript',
jsCode: `// ⚠️ CRITICAL: Webhook data is nested under 'body' property!
// This Code node should be connected after a Webhook node
// ❌ WRONG - This will be undefined:
// const command = items[0].json.testCommand;
// ✅ CORRECT - Access webhook data through body:
const webhookData = items[0].json.body;
const headers = items[0].json.headers;
const query = items[0].json.query;
// Process webhook payload
return [{
json: {
// Extract data from webhook body
command: webhookData.testCommand,
userId: webhookData.userId,
data: webhookData.data,
// Add metadata
timestamp: DateTime.now().toISO(),
requestId: headers['x-request-id'] || crypto.randomUUID(),
source: query.source || 'webhook',
// Original webhook info
httpMethod: items[0].json.httpMethod,
webhookPath: items[0].json.webhookPath
}
}];`
}
},
// Code - Custom logic
'nodes-base.code': {
minimal: {
language: 'javaScript',
jsCode: 'return items;'
jsCode: 'return [{json: {result: "success"}}];'
},
common: {
language: 'javaScript',
jsCode: `// Access input items
jsCode: `// Process each item and add timestamp
return items.map(item => ({
json: {
...item.json,
processed: true,
timestamp: DateTime.now().toISO()
}
}));`,
onError: 'continueRegularOutput'
},
advanced: {
language: 'javaScript',
jsCode: `// Advanced data processing with proper helper checks
const crypto = require('crypto');
const results = [];
for (const item of items) {
// Process each item
results.push({
json: {
...item.json,
processed: true,
timestamp: new Date().toISOString()
try {
// Validate required fields
if (!item.json.email || !item.json.name) {
throw new Error('Missing required fields: email or name');
}
});
// Generate secure API key
const apiKey = crypto.randomBytes(16).toString('hex');
// Check if $helpers is available before using
let response;
if (typeof $helpers !== 'undefined' && $helpers.httpRequest) {
response = await $helpers.httpRequest({
method: 'POST',
url: 'https://api.example.com/process',
body: {
email: item.json.email,
name: item.json.name,
apiKey
},
headers: {
'Content-Type': 'application/json'
}
});
} else {
// Fallback if $helpers not available
response = { message: 'HTTP requests not available in this n8n version' };
}
// Add to results with response data
results.push({
json: {
...item.json,
apiResponse: response,
processedAt: DateTime.now().toISO(),
status: 'success'
}
});
} catch (error) {
// Include failed items with error info
results.push({
json: {
...item.json,
error: error.message,
status: 'failed',
processedAt: DateTime.now().toISO()
}
});
}
}
return results;`
},
advanced: {
language: 'python',
pythonCode: `import json
from datetime import datetime
return results;`,
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 2
}
},
// Additional Code node examples
'nodes-base.code.dataTransform': {
minimal: {
language: 'javaScript',
jsCode: `// Transform CSV-like data to JSON
return items.map(item => {
const lines = item.json.data.split('\\n');
const headers = lines[0].split(',');
const rows = lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, i) => {
obj[header.trim()] = values[i]?.trim() || '';
return obj;
}, {});
});
return {json: {rows, count: rows.length}};
});`
}
},
'nodes-base.code.aggregation': {
minimal: {
language: 'javaScript',
jsCode: `// Aggregate data from all items
const totals = items.reduce((acc, item) => {
acc.count++;
acc.sum += item.json.amount || 0;
acc.categories[item.json.category] = (acc.categories[item.json.category] || 0) + 1;
return acc;
}, {count: 0, sum: 0, categories: {}});
return [{
json: {
totalItems: totals.count,
totalAmount: totals.sum,
averageAmount: totals.sum / totals.count,
categoryCounts: totals.categories,
processedAt: DateTime.now().toISO()
}
}];`
}
},
'nodes-base.code.filtering': {
minimal: {
language: 'javaScript',
jsCode: `// Filter items based on conditions
return items
.filter(item => {
const amount = item.json.amount || 0;
const status = item.json.status || '';
return amount > 100 && status === 'active';
})
.map(item => ({json: item.json}));`
}
},
'nodes-base.code.jmespathFiltering': {
minimal: {
language: 'javaScript',
jsCode: `// JMESPath filtering - IMPORTANT: Use backticks for numeric literals!
const allItems = items.map(item => item.json);
// ✅ CORRECT - Filter with numeric literals using backticks
const expensiveItems = $jmespath(allItems, '[?price >= \`100\`]');
const lowStock = $jmespath(allItems, '[?inventory < \`10\`]');
const highPriority = $jmespath(allItems, '[?priority == \`1\`]');
// Combine multiple conditions
const urgentExpensive = $jmespath(allItems, '[?price >= \`100\` && priority == \`1\`]');
// String comparisons don't need backticks
const activeItems = $jmespath(allItems, '[?status == "active"]');
// Return filtered results
return expensiveItems.map(item => ({json: item}));`
}
},
'nodes-base.code.pythonExample': {
minimal: {
language: 'python',
pythonCode: `# Python data processing - use underscore prefix for built-in variables
import json
from datetime import datetime
import re
# Access input items
results = []
for item in items:
# Process each item
processed_item = item.json.copy()
processed_item['processed'] = True
processed_item['timestamp'] = datetime.now().isoformat()
# Use _input.all() to get items in Python
for item in _input.all():
# Convert JsProxy to Python dict to avoid issues with null values
item_data = item.json.to_py()
results.append({'json': processed_item})
# Clean email addresses
email = item_data.get('email', '')
if email and re.match(r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$', email):
cleaned_data = {
'email': email.lower(),
'name': item_data.get('name', '').title(),
'validated': True,
'timestamp': datetime.now().isoformat()
}
else:
# Spread operator doesn't work with JsProxy, use dict()
cleaned_data = dict(item_data)
cleaned_data['validated'] = False
cleaned_data['error'] = 'Invalid email format'
results.append({'json': cleaned_data})
return results`
}
},
'nodes-base.code.aiTool': {
minimal: {
language: 'javaScript',
mode: 'runOnceForEachItem',
jsCode: `// Code node as AI tool - calculate discount
const quantity = $json.quantity || 1;
const price = $json.price || 0;
let discountRate = 0;
if (quantity >= 100) discountRate = 0.20;
else if (quantity >= 50) discountRate = 0.15;
else if (quantity >= 20) discountRate = 0.10;
else if (quantity >= 10) discountRate = 0.05;
const subtotal = price * quantity;
const discount = subtotal * discountRate;
const total = subtotal - discount;
return [{
json: {
quantity,
price,
subtotal,
discountRate: discountRate * 100,
discountAmount: discount,
total,
savings: discount
}
}];`
}
},
'nodes-base.code.crypto': {
minimal: {
language: 'javaScript',
jsCode: `// Using crypto in Code nodes - it IS available!
const crypto = require('crypto');
// Generate secure tokens
const token = crypto.randomBytes(32).toString('hex');
const uuid = crypto.randomUUID();
// Create hashes
const hash = crypto.createHash('sha256')
.update(items[0].json.data || 'test')
.digest('hex');
return [{
json: {
token,
uuid,
hash,
timestamp: DateTime.now().toISO()
}
}];`
}
},
'nodes-base.code.staticData': {
minimal: {
language: 'javaScript',
jsCode: `// Using workflow static data correctly
// IMPORTANT: $getWorkflowStaticData is a standalone function!
const staticData = $getWorkflowStaticData('global');
// Initialize counter if not exists
if (!staticData.processCount) {
staticData.processCount = 0;
staticData.firstRun = DateTime.now().toISO();
}
// Update counter
staticData.processCount++;
staticData.lastRun = DateTime.now().toISO();
// Process items
const results = items.map(item => ({
json: {
...item.json,
runNumber: staticData.processCount,
processed: true
}
}));
return results;`
}
},
// Set - Data manipulation
'nodes-base.set': {
minimal: {

View File

@@ -1056,4 +1056,501 @@ export class NodeSpecificValidators {
suggestions.push('Consider adding webhook validation (HMAC signature verification)');
suggestions.push('Implement rate limiting for public webhooks');
}
/**
* Validate Code node configuration with n8n-specific patterns
*/
static validateCode(context: NodeValidationContext): void {
const { config, errors, warnings, suggestions, autofix } = context;
const language = config.language || 'javaScript';
const codeField = language === 'python' ? 'pythonCode' : 'jsCode';
const code = config[codeField] || '';
// Check for empty code
if (!code || code.trim() === '') {
errors.push({
type: 'missing_required',
property: codeField,
message: 'Code cannot be empty',
fix: 'Add your code logic. Start with: return [{json: {result: "success"}}]'
});
return;
}
// Language-specific validation
if (language === 'javaScript') {
this.validateJavaScriptCode(code, errors, warnings, suggestions);
} else if (language === 'python') {
this.validatePythonCode(code, errors, warnings, suggestions);
}
// Check return statement and format
this.validateReturnStatement(code, language, errors, warnings, suggestions);
// Check n8n variable usage
this.validateN8nVariables(code, language, warnings, suggestions, errors);
// Security and best practices
this.validateCodeSecurity(code, language, warnings);
// Error handling recommendations
if (!config.onError && code.length > 100) {
warnings.push({
type: 'best_practice',
property: 'errorHandling',
message: 'Code nodes can throw errors - consider error handling',
suggestion: 'Add onError: "continueRegularOutput" to handle errors gracefully'
});
autofix.onError = 'continueRegularOutput';
}
// Mode-specific suggestions
if (config.mode === 'runOnceForEachItem' && code.includes('items')) {
warnings.push({
type: 'best_practice',
message: 'In "Run Once for Each Item" mode, use $json instead of items array',
suggestion: 'Access current item data with $json.fieldName'
});
}
if (!config.mode && code.includes('$json')) {
warnings.push({
type: 'best_practice',
message: '$json only works in "Run Once for Each Item" mode',
suggestion: 'Either set mode: "runOnceForEachItem" or use items[0].json'
});
}
}
private static validateJavaScriptCode(
code: string,
errors: ValidationError[],
warnings: ValidationWarning[],
suggestions: string[]
): void {
// Check for syntax patterns that might fail
const syntaxPatterns = [
{ pattern: /const\s+const/, message: 'Duplicate const declaration' },
{ pattern: /let\s+let/, message: 'Duplicate let declaration' },
{ pattern: /\)\s*\)\s*{/, message: 'Extra closing parenthesis before {' },
{ pattern: /}\s*}$/, message: 'Extra closing brace at end' }
];
syntaxPatterns.forEach(({ pattern, message }) => {
if (pattern.test(code)) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: `Syntax error: ${message}`,
fix: 'Check your JavaScript syntax'
});
}
});
// Common async/await issues
// Check for await inside a non-async function (but top-level await is fine)
const functionWithAwait = /function\s+\w*\s*\([^)]*\)\s*{[^}]*await/;
const arrowWithAwait = /\([^)]*\)\s*=>\s*{[^}]*await/;
if ((functionWithAwait.test(code) || arrowWithAwait.test(code)) && !code.includes('async')) {
warnings.push({
type: 'best_practice',
message: 'Using await inside a non-async function',
suggestion: 'Add async keyword to the function, or use top-level await (Code nodes support it)'
});
}
// Check for common helper usage
if (code.includes('$helpers.httpRequest')) {
suggestions.push('$helpers.httpRequest is async - use: const response = await $helpers.httpRequest(...)');
}
if (code.includes('DateTime') && !code.includes('DateTime.')) {
warnings.push({
type: 'best_practice',
message: 'DateTime is from Luxon library',
suggestion: 'Use DateTime.now() or DateTime.fromISO() for date operations'
});
}
}
private static validatePythonCode(
code: string,
errors: ValidationError[],
warnings: ValidationWarning[],
suggestions: string[]
): void {
// Python-specific validation
const lines = code.split('\n');
// Check for tab/space mixing (already done in base validator)
// Check for common Python mistakes in n8n context
if (code.includes('__name__') && code.includes('__main__')) {
warnings.push({
type: 'inefficient',
message: 'if __name__ == "__main__" is not needed in Code nodes',
suggestion: 'Code node Python runs directly - remove the main check'
});
}
// Check for unavailable imports
const unavailableImports = [
{ module: 'requests', suggestion: 'Use JavaScript Code node with $helpers.httpRequest for HTTP requests' },
{ module: 'pandas', suggestion: 'Use built-in list/dict operations or JavaScript for data manipulation' },
{ module: 'numpy', suggestion: 'Use standard Python math operations' },
{ module: 'pip', suggestion: 'External packages cannot be installed in Code nodes' }
];
unavailableImports.forEach(({ module, suggestion }) => {
if (code.includes(`import ${module}`) || code.includes(`from ${module}`)) {
errors.push({
type: 'invalid_value',
property: 'pythonCode',
message: `Module '${module}' is not available in Code nodes`,
fix: suggestion
});
}
});
// Check indentation after colons
lines.forEach((line, i) => {
if (line.trim().endsWith(':') && i < lines.length - 1) {
const nextLine = lines[i + 1];
if (nextLine.trim() && !nextLine.startsWith(' ') && !nextLine.startsWith('\t')) {
errors.push({
type: 'invalid_value',
property: 'pythonCode',
message: `Missing indentation after line ${i + 1}`,
fix: 'Indent the line after the colon'
});
}
}
});
}
private static validateReturnStatement(
code: string,
language: string,
errors: ValidationError[],
warnings: ValidationWarning[],
suggestions: string[]
): void {
const hasReturn = /return\s+/.test(code);
if (!hasReturn) {
errors.push({
type: 'missing_required',
property: language === 'python' ? 'pythonCode' : 'jsCode',
message: 'Code must return data for the next node',
fix: language === 'python'
? 'Add: return [{"json": {"result": "success"}}]'
: 'Add: return [{json: {result: "success"}}]'
});
return;
}
// JavaScript return format validation
if (language === 'javaScript') {
// Check for object return without array
if (/return\s+{(?!.*\[).*}\s*;?$/s.test(code) && !code.includes('json:')) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Return value must be an array of objects',
fix: 'Wrap in array: return [{json: yourObject}]'
});
}
// Check for primitive return
if (/return\s+(true|false|null|undefined|\d+|['"`])/m.test(code)) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Cannot return primitive values directly',
fix: 'Return array of objects: return [{json: {value: yourData}}]'
});
}
// Check for array of non-objects
if (/return\s+\[[\s\n]*['"`\d]/.test(code)) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Array items must be objects with json property',
fix: 'Use: return [{json: {value: "data"}}] not return ["data"]'
});
}
// Suggest proper return format for items
if (/return\s+items\s*;?$/.test(code) && !code.includes('map')) {
suggestions.push(
'Returning items directly is fine if they already have {json: ...} structure. ' +
'To modify: return items.map(item => ({json: {...item.json, newField: "value"}}))'
);
}
}
// Python return format validation
if (language === 'python') {
// Check for dict return without list
if (/return\s+{(?!.*\[).*}$/s.test(code)) {
errors.push({
type: 'invalid_value',
property: 'pythonCode',
message: 'Return value must be a list of dicts',
fix: 'Wrap in list: return [{"json": your_dict}]'
});
}
// Check for primitive return
if (/return\s+(True|False|None|\d+|['"`])/m.test(code)) {
errors.push({
type: 'invalid_value',
property: 'pythonCode',
message: 'Cannot return primitive values directly',
fix: 'Return list of dicts: return [{"json": {"value": your_data}}]'
});
}
}
}
private static validateN8nVariables(
code: string,
language: string,
warnings: ValidationWarning[],
suggestions: string[],
errors: ValidationError[]
): void {
// Check if code accesses input data
const inputPatterns = language === 'javaScript'
? ['items', '$input', '$json', '$node', '$prevNode']
: ['items', '_input'];
const usesInput = inputPatterns.some(pattern => code.includes(pattern));
if (!usesInput && code.length > 50) {
warnings.push({
type: 'missing_common',
message: 'Code doesn\'t reference input data',
suggestion: language === 'javaScript'
? 'Access input with: items, $input.all(), or $json (single-item mode)'
: 'Access input with: items variable'
});
}
// Check for expression syntax in Code nodes
if (code.includes('{{') && code.includes('}}')) {
errors.push({
type: 'invalid_value',
property: language === 'python' ? 'pythonCode' : 'jsCode',
message: 'Expression syntax {{...}} is not valid in Code nodes',
fix: 'Use regular JavaScript/Python syntax without double curly braces'
});
}
// Check for wrong $node syntax
if (code.includes('$node[')) {
warnings.push({
type: 'invalid_value',
property: language === 'python' ? 'pythonCode' : 'jsCode',
message: 'Use $(\'Node Name\') instead of $node[\'Node Name\'] in Code nodes',
suggestion: 'Replace $node[\'NodeName\'] with $(\'NodeName\')'
});
}
// Check for expression-only functions
const expressionOnlyFunctions = ['$now()', '$today()', '$tomorrow()', '.unique()', '.pluck(', '.keys()', '.hash('];
expressionOnlyFunctions.forEach(func => {
if (code.includes(func)) {
warnings.push({
type: 'invalid_value',
property: language === 'python' ? 'pythonCode' : 'jsCode',
message: `${func} is an expression-only function not available in Code nodes`,
suggestion: 'See Code node documentation for alternatives'
});
}
});
// Check for common variable mistakes
if (language === 'javaScript') {
// Using $ without proper variable
if (/\$(?![a-zA-Z])/.test(code) && !code.includes('${')) {
warnings.push({
type: 'best_practice',
message: 'Invalid $ usage detected',
suggestion: 'n8n variables start with $: $json, $input, $node, $workflow, $execution'
});
}
// Check for helpers usage
if (code.includes('helpers.') && !code.includes('$helpers')) {
warnings.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Use $helpers not helpers',
suggestion: 'Change helpers. to $helpers.'
});
}
// Check for $helpers usage without availability check
if (code.includes('$helpers') && !code.includes('typeof $helpers')) {
warnings.push({
type: 'best_practice',
message: '$helpers availability varies by n8n version',
suggestion: 'Check availability first: if (typeof $helpers !== "undefined" && $helpers.httpRequest) { ... }'
});
}
// Suggest available helpers
if (code.includes('$helpers')) {
suggestions.push(
'Common $helpers methods: httpRequest(), prepareBinaryData(). Note: getWorkflowStaticData is a standalone function - use $getWorkflowStaticData() instead'
);
}
// Check for incorrect getWorkflowStaticData usage
if (code.includes('$helpers.getWorkflowStaticData')) {
errors.push({
type: 'invalid_value',
property: 'jsCode',
message: '$helpers.getWorkflowStaticData() will cause "$helpers is not defined" error',
fix: 'Use $getWorkflowStaticData("global") or $getWorkflowStaticData("node") directly'
});
}
// Check for wrong JMESPath parameter order
if (code.includes('$jmespath(') && /\$jmespath\s*\(\s*['"`]/.test(code)) {
warnings.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Code node $jmespath has reversed parameter order: $jmespath(data, query)',
suggestion: 'Use: $jmespath(dataObject, "query.path") not $jmespath("query.path", dataObject)'
});
}
// Check for webhook data access patterns
if (code.includes('items[0].json') && !code.includes('.json.body')) {
// Check if previous node reference suggests webhook
if (code.includes('Webhook') || code.includes('webhook') ||
code.includes('$("Webhook")') || code.includes("$('Webhook')")) {
warnings.push({
type: 'invalid_value',
property: 'jsCode',
message: 'Webhook data is nested under .body property',
suggestion: 'Use items[0].json.body.fieldName instead of items[0].json.fieldName for webhook data'
});
}
// Also check for common webhook field names that suggest webhook data
else if (/items\[0\]\.json\.(payload|data|command|action|event|message)\b/.test(code)) {
warnings.push({
type: 'best_practice',
message: 'If processing webhook data, remember it\'s nested under .body',
suggestion: 'Webhook payloads are at items[0].json.body, not items[0].json'
});
}
}
}
// Check for JMESPath filters with unquoted numeric literals (both JS and Python)
const jmespathFunction = language === 'javaScript' ? '$jmespath' : '_jmespath';
if (code.includes(jmespathFunction + '(')) {
// Look for filter expressions with comparison operators and numbers
const filterPattern = /\[?\?[^[\]]*(?:>=?|<=?|==|!=)\s*(\d+(?:\.\d+)?)\s*\]/g;
let match;
while ((match = filterPattern.exec(code)) !== null) {
const number = match[1];
// Check if the number is NOT wrapped in backticks
const beforeNumber = code.substring(match.index, match.index + match[0].indexOf(number));
const afterNumber = code.substring(match.index + match[0].indexOf(number) + number.length);
if (!beforeNumber.includes('`') || !afterNumber.startsWith('`')) {
errors.push({
type: 'invalid_value',
property: language === 'python' ? 'pythonCode' : 'jsCode',
message: `JMESPath numeric literal ${number} must be wrapped in backticks`,
fix: `Change [?field >= ${number}] to [?field >= \`${number}\`]`
});
}
}
// Also provide a general suggestion if JMESPath is used
suggestions.push(
'JMESPath in n8n requires backticks around numeric literals in filters: [?age >= `18`]'
);
}
}
private static validateCodeSecurity(
code: string,
language: string,
warnings: ValidationWarning[]
): void {
// Security checks
const dangerousPatterns = [
{ pattern: /eval\s*\(/, message: 'Avoid eval() - it\'s a security risk' },
{ pattern: /Function\s*\(/, message: 'Avoid Function constructor - use regular functions' },
{ pattern: language === 'python' ? /exec\s*\(/ : /exec\s*\(/, message: 'Avoid exec() - it\'s a security risk' },
{ pattern: /process\.env/, message: 'Limited environment access in Code nodes' },
{ pattern: /import\s+\*/, message: 'Avoid import * - be specific about imports' }
];
dangerousPatterns.forEach(({ pattern, message }) => {
if (pattern.test(code)) {
warnings.push({
type: 'security',
message,
suggestion: 'Use safer alternatives or built-in functions'
});
}
});
// Special handling for require() - it's allowed for built-in modules
if (code.includes('require(')) {
// Check if it's requiring a built-in module
const builtinModules = ['crypto', 'util', 'querystring', 'url', 'buffer'];
const requirePattern = /require\s*\(\s*['"`](\w+)['"`]\s*\)/g;
let match;
while ((match = requirePattern.exec(code)) !== null) {
const moduleName = match[1];
if (!builtinModules.includes(moduleName)) {
warnings.push({
type: 'security',
message: `Cannot require('${moduleName}') - only built-in Node.js modules are available`,
suggestion: `Available modules: ${builtinModules.join(', ')}`
});
}
}
// If require is used without quotes, it might be dynamic
if (/require\s*\([^'"`]/.test(code)) {
warnings.push({
type: 'security',
message: 'Dynamic require() not supported',
suggestion: 'Use static require with string literals: require("crypto")'
});
}
}
// Check for crypto usage without require
if ((code.includes('crypto.') || code.includes('randomBytes') || code.includes('randomUUID')) &&
!code.includes('require') && language === 'javaScript') {
warnings.push({
type: 'invalid_value',
message: 'Using crypto without require statement',
suggestion: 'Add: const crypto = require("crypto"); at the beginning (ignore editor warnings)'
});
}
// File system access warning
if (/\b(fs|path|child_process)\b/.test(code)) {
warnings.push({
type: 'security',
message: 'File system and process access not available in Code nodes',
suggestion: 'Use other n8n nodes for file operations (e.g., Read/Write Files node)'
});
}
}
}

View File

@@ -227,6 +227,94 @@ export class TaskTemplates {
]
},
'process_webhook_data': {
task: 'process_webhook_data',
description: 'Process incoming webhook data with Code node (shows correct data access)',
nodeType: 'nodes-base.code',
configuration: {
language: 'javaScript',
jsCode: `// ⚠️ CRITICAL: Webhook data is nested under 'body' property!
// Connect this Code node after a Webhook node
// Access webhook payload data - it's under .body, not directly under .json
const webhookData = items[0].json.body; // ✅ CORRECT
const headers = items[0].json.headers; // HTTP headers
const query = items[0].json.query; // Query parameters
// Common mistake to avoid:
// const command = items[0].json.testCommand; // ❌ WRONG - will be undefined!
// const command = items[0].json.body.testCommand; // ✅ CORRECT
// Process the webhook data
try {
// Validate required fields
if (!webhookData.command) {
throw new Error('Missing required field: command');
}
// Process based on command
let result = {};
switch (webhookData.command) {
case 'process':
result = {
status: 'processed',
data: webhookData.data,
processedAt: DateTime.now().toISO()
};
break;
case 'validate':
result = {
status: 'validated',
isValid: true,
validatedFields: Object.keys(webhookData.data || {})
};
break;
default:
result = {
status: 'unknown_command',
command: webhookData.command
};
}
// Return processed data
return [{
json: {
...result,
requestId: headers['x-request-id'] || crypto.randomUUID(),
source: query.source || 'webhook',
originalCommand: webhookData.command,
metadata: {
httpMethod: items[0].json.httpMethod,
webhookPath: items[0].json.webhookPath,
timestamp: DateTime.now().toISO()
}
}
}];
} catch (error) {
// Return error response
return [{
json: {
status: 'error',
error: error.message,
timestamp: DateTime.now().toISO()
}
}];
}`,
onError: 'continueRegularOutput'
},
userMustProvide: [],
notes: [
'⚠️ WEBHOOK DATA IS AT items[0].json.body, NOT items[0].json',
'This is the most common webhook processing mistake',
'Headers are at items[0].json.headers',
'Query parameters are at items[0].json.query',
'Connect this Code node directly after a Webhook node'
]
},
// Database Tasks
'query_postgres': {
task: 'query_postgres',
@@ -873,6 +961,479 @@ return results;`,
'Consider implementing exponential backoff in Code node',
'Monitor usage to stay within quotas'
]
},
// Code Node Tasks
'custom_ai_tool': {
task: 'custom_ai_tool',
description: 'Create a custom tool for AI agents using Code node',
nodeType: 'nodes-base.code',
configuration: {
language: 'javaScript',
mode: 'runOnceForEachItem',
jsCode: `// Custom AI Tool - Example: Text Analysis
// This code will be called by AI agents with $json containing the input
// Access the input from the AI agent
const text = $json.text || '';
const operation = $json.operation || 'analyze';
// Perform the requested operation
let result = {};
switch (operation) {
case 'wordCount':
result = {
wordCount: text.split(/\\s+/).filter(word => word.length > 0).length,
characterCount: text.length,
lineCount: text.split('\\n').length
};
break;
case 'extract':
// Extract specific patterns (emails, URLs, etc.)
result = {
emails: text.match(/[\\w.-]+@[\\w.-]+\\.\\w+/g) || [],
urls: text.match(/https?:\\/\\/[^\\s]+/g) || [],
numbers: text.match(/\\b\\d+\\b/g) || []
};
break;
default:
result = {
error: 'Unknown operation',
availableOperations: ['wordCount', 'extract']
};
}
return [{
json: {
...result,
originalText: text,
operation: operation,
processedAt: DateTime.now().toISO()
}
}];`,
onError: 'continueRegularOutput'
},
userMustProvide: [],
notes: [
'Connect this to AI Agent node\'s tool input',
'AI will pass data in $json',
'Use "Run Once for Each Item" mode for AI tools',
'Return structured data the AI can understand'
]
},
'aggregate_data': {
task: 'aggregate_data',
description: 'Aggregate data from multiple items into summary statistics',
nodeType: 'nodes-base.code',
configuration: {
language: 'javaScript',
jsCode: `// Aggregate data from all items
const stats = {
count: 0,
sum: 0,
min: Infinity,
max: -Infinity,
values: [],
categories: {},
errors: []
};
// Process each item
for (const item of items) {
try {
const value = item.json.value || item.json.amount || 0;
const category = item.json.category || 'uncategorized';
stats.count++;
stats.sum += value;
stats.min = Math.min(stats.min, value);
stats.max = Math.max(stats.max, value);
stats.values.push(value);
// Count by category
stats.categories[category] = (stats.categories[category] || 0) + 1;
} catch (error) {
stats.errors.push({
item: item.json,
error: error.message
});
}
}
// Calculate additional statistics
const average = stats.count > 0 ? stats.sum / stats.count : 0;
const sorted = [...stats.values].sort((a, b) => a - b);
const median = sorted.length > 0
? sorted[Math.floor(sorted.length / 2)]
: 0;
return [{
json: {
totalItems: stats.count,
sum: stats.sum,
average: average,
median: median,
min: stats.min === Infinity ? 0 : stats.min,
max: stats.max === -Infinity ? 0 : stats.max,
categoryCounts: stats.categories,
errorCount: stats.errors.length,
errors: stats.errors,
processedAt: DateTime.now().toISO()
}
}];`,
onError: 'continueRegularOutput'
},
userMustProvide: [],
notes: [
'Assumes items have "value" or "amount" field',
'Groups by "category" field if present',
'Returns single item with all statistics',
'Handles errors gracefully'
]
},
'batch_process_with_api': {
task: 'batch_process_with_api',
description: 'Process items in batches with API calls',
nodeType: 'nodes-base.code',
configuration: {
language: 'javaScript',
jsCode: `// Batch process items with API calls
const BATCH_SIZE = 10;
const API_URL = 'https://api.example.com/batch-process'; // USER MUST UPDATE
const results = [];
// Process items in batches
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
try {
// Prepare batch data
const batchData = batch.map(item => ({
id: item.json.id,
data: item.json
}));
// Make API request for batch
const response = await $helpers.httpRequest({
method: 'POST',
url: API_URL,
body: {
items: batchData
},
headers: {
'Content-Type': 'application/json'
}
});
// Add results
if (response.results && Array.isArray(response.results)) {
response.results.forEach((result, index) => {
results.push({
json: {
...batch[index].json,
...result,
batchNumber: Math.floor(i / BATCH_SIZE) + 1,
processedAt: DateTime.now().toISO()
}
});
});
}
// Add delay between batches to avoid rate limits
if (i + BATCH_SIZE < items.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
// Add failed batch items with error
batch.forEach(item => {
results.push({
json: {
...item.json,
error: error.message,
status: 'failed',
batchNumber: Math.floor(i / BATCH_SIZE) + 1
}
});
});
}
}
return results;`,
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 2
},
userMustProvide: [
{
property: 'jsCode',
description: 'Update API_URL in the code',
example: 'https://your-api.com/batch'
}
],
notes: [
'Processes items in batches of 10',
'Includes delay between batches',
'Handles batch failures gracefully',
'Update API_URL and adjust BATCH_SIZE as needed'
]
},
'error_safe_transform': {
task: 'error_safe_transform',
description: 'Transform data with comprehensive error handling',
nodeType: 'nodes-base.code',
configuration: {
language: 'javaScript',
jsCode: `// Safe data transformation with validation
const results = [];
const errors = [];
for (const item of items) {
try {
// Validate required fields
const required = ['id', 'name']; // USER SHOULD UPDATE
const missing = required.filter(field => !item.json[field]);
if (missing.length > 0) {
throw new Error(\`Missing required fields: \${missing.join(', ')}\`);
}
// Transform data with type checking
const transformed = {
// Ensure ID is string
id: String(item.json.id),
// Clean and validate name
name: String(item.json.name).trim(),
// Parse numbers safely
amount: parseFloat(item.json.amount) || 0,
// Parse dates safely
date: item.json.date
? DateTime.fromISO(item.json.date).isValid
? DateTime.fromISO(item.json.date).toISO()
: null
: null,
// Boolean conversion
isActive: Boolean(item.json.active || item.json.isActive),
// Array handling
tags: Array.isArray(item.json.tags)
? item.json.tags.filter(tag => typeof tag === 'string')
: [],
// Nested object handling
metadata: typeof item.json.metadata === 'object'
? item.json.metadata
: {},
// Add processing info
processedAt: DateTime.now().toISO(),
originalIndex: items.indexOf(item)
};
results.push({
json: transformed
});
} catch (error) {
errors.push({
json: {
error: error.message,
originalData: item.json,
index: items.indexOf(item),
status: 'failed'
}
});
}
}
// Add summary at the end
results.push({
json: {
_summary: {
totalProcessed: results.length - errors.length,
totalErrors: errors.length,
successRate: ((results.length - errors.length) / items.length * 100).toFixed(2) + '%',
timestamp: DateTime.now().toISO()
}
}
});
// Include errors at the end
return [...results, ...errors];`,
onError: 'continueRegularOutput'
},
userMustProvide: [
{
property: 'jsCode',
description: 'Update required fields array',
example: "const required = ['id', 'email', 'name'];"
}
],
notes: [
'Validates all data types',
'Handles missing/invalid data gracefully',
'Returns both successful and failed items',
'Includes processing summary'
]
},
'async_data_processing': {
task: 'async_data_processing',
description: 'Process data with async operations and proper error handling',
nodeType: 'nodes-base.code',
configuration: {
language: 'javaScript',
jsCode: `// Async processing with concurrent limits
const CONCURRENT_LIMIT = 5;
const results = [];
// Process items with concurrency control
async function processItem(item, index) {
try {
// Simulate async operation (replace with actual logic)
// Example: API call, database query, file operation
await new Promise(resolve => setTimeout(resolve, 100));
// Actual processing logic here
const processed = {
...item.json,
processed: true,
index: index,
timestamp: DateTime.now().toISO()
};
// Example async operation - external API call
if (item.json.needsEnrichment) {
const enrichment = await $helpers.httpRequest({
method: 'GET',
url: \`https://api.example.com/enrich/\${item.json.id}\`
});
processed.enrichment = enrichment;
}
return { json: processed };
} catch (error) {
return {
json: {
...item.json,
error: error.message,
status: 'failed',
index: index
}
};
}
}
// Process in batches with concurrency limit
for (let i = 0; i < items.length; i += CONCURRENT_LIMIT) {
const batch = items.slice(i, i + CONCURRENT_LIMIT);
const batchPromises = batch.map((item, batchIndex) =>
processItem(item, i + batchIndex)
);
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return results;`,
onError: 'continueRegularOutput',
retryOnFail: true,
maxTries: 2
},
userMustProvide: [],
notes: [
'Processes 5 items concurrently',
'Prevents overwhelming external services',
'Each item processed independently',
'Errors don\'t affect other items'
]
},
'python_data_analysis': {
task: 'python_data_analysis',
description: 'Analyze data using Python with statistics',
nodeType: 'nodes-base.code',
configuration: {
language: 'python',
pythonCode: `# Python data analysis - use underscore prefix for built-in variables
import json
from datetime import datetime
import statistics
# Collect data for analysis
values = []
categories = {}
dates = []
# Use _input.all() to get items in Python
for item in _input.all():
# Convert JsProxy to Python dict for safe access
item_data = item.json.to_py()
# Extract numeric values
if 'value' in item_data or 'amount' in item_data:
value = item_data.get('value', item_data.get('amount', 0))
if isinstance(value, (int, float)):
values.append(value)
# Count categories
category = item_data.get('category', 'uncategorized')
categories[category] = categories.get(category, 0) + 1
# Collect dates
if 'date' in item_data:
dates.append(item_data['date'])
# Calculate statistics
result = {
'itemCount': len(_input.all()),
'values': {
'count': len(values),
'sum': sum(values) if values else 0,
'mean': statistics.mean(values) if values else 0,
'median': statistics.median(values) if values else 0,
'min': min(values) if values else 0,
'max': max(values) if values else 0,
'stdev': statistics.stdev(values) if len(values) > 1 else 0
},
'categories': categories,
'dateRange': {
'earliest': min(dates) if dates else None,
'latest': max(dates) if dates else None,
'count': len(dates)
},
'analysis': {
'hasNumericData': len(values) > 0,
'hasCategoricalData': len(categories) > 0,
'hasTemporalData': len(dates) > 0,
'dataQuality': 'good' if len(values) > len(items) * 0.8 else 'partial'
},
'processedAt': datetime.now().isoformat()
}
# Return single summary item
return [{'json': result}]`,
onError: 'continueRegularOutput'
},
userMustProvide: [],
notes: [
'Uses Python statistics module',
'Analyzes numeric, categorical, and date data',
'Returns comprehensive summary',
'Handles missing data gracefully'
]
}
};
@@ -926,10 +1487,10 @@ return results;`,
static getTaskCategories(): Record<string, string[]> {
return {
'HTTP/API': ['get_api_data', 'post_json_request', 'call_api_with_auth', 'api_call_with_retry'],
'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling'],
'Webhooks': ['receive_webhook', 'webhook_with_response', 'webhook_with_error_handling', 'process_webhook_data'],
'Database': ['query_postgres', 'insert_postgres_data', 'database_transaction_safety'],
'AI/LangChain': ['chat_with_ai', 'ai_agent_workflow', 'multi_tool_ai_agent', 'ai_rate_limit_handling'],
'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing'],
'Data Processing': ['transform_data', 'filter_data', 'fault_tolerant_processing', 'process_webhook_data'],
'Communication': ['send_slack_message', 'send_email'],
'AI Tool Usage': ['use_google_sheets_as_tool', 'use_slack_as_tool', 'multi_tool_ai_agent'],
'Error Handling': ['modern_error_handling_patterns', 'api_call_with_retry', 'fault_tolerant_processing', 'webhook_with_error_handling', 'database_transaction_safety', 'ai_rate_limit_handling']