diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8a83f5f..bd70c62 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "n8n-mcp-skills", "source": "./", - "description": "Complete bundle: 5 expert skills for building flawless n8n workflows using n8n-mcp MCP server. Includes skills for expression syntax, MCP tools usage, workflow patterns, validation, and node configuration.", + "description": "Complete bundle: 7 expert skills for building flawless n8n workflows using n8n-mcp MCP server. Includes skills for expression syntax, MCP tools usage, workflow patterns, validation, node configuration, JavaScript code, and Python code.", "version": "1.0.0", "author": { "name": "Romuald CzΕ‚onkowski", @@ -23,6 +23,9 @@ "automation", "validation", "expressions", + "code", + "javascript", + "python", "skills" ], "homepage": "https://github.com/czlonkowski/n8n-skills", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3c8f866..b093aff 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -13,7 +13,10 @@ "automation", "mcp", "validation", - "expressions" + "expressions", + "code", + "javascript", + "python" ], "repository": "https://github.com/czlonkowski/n8n-skills", "homepage": "https://github.com/czlonkowski/n8n-skills" diff --git a/README.md b/README.md index 69f35a4..dae2f45 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ These skills solve these problems by teaching Claude: --- -## πŸ“š The 5 Skills +## πŸ“š The 7 Skills ### 1. **n8n Expression Syntax** Teaches correct n8n expression syntax and common patterns. @@ -90,6 +90,32 @@ Operation-aware node configuration guidance. - AI connection types (8 types for AI Agent workflows) - Common configuration patterns +### 6. **n8n Code JavaScript** +Write effective JavaScript code in n8n Code nodes. + +**Activates when**: Writing JavaScript in Code nodes, troubleshooting Code node errors, making HTTP requests with $helpers, working with dates. + +**Key Features**: +- Data access patterns ($input.all(), $input.first(), $input.item) +- **Critical gotcha**: Webhook data under `$json.body` +- Correct return format: `[{json: {...}}]` +- Built-in functions ($helpers.httpRequest(), DateTime, $jmespath()) +- Top 5 error patterns with solutions (covering 62%+ of failures) +- 10 production-tested patterns + +### 7. **n8n Code Python** +Write Python code in n8n Code nodes with proper limitations awareness. + +**Activates when**: Writing Python in Code nodes, need to know Python limitations, working with standard library. + +**Key Features**: +- **Important**: Use JavaScript for 95% of use cases +- Python data access (_input, _json, _node) +- **Critical limitation**: No external libraries (requests, pandas, numpy) +- Standard library reference (json, datetime, re, etc.) +- Workarounds for missing libraries +- Common Python patterns for n8n + --- ## πŸš€ Installation @@ -161,6 +187,12 @@ Skills activate **automatically** when relevant queries are detected: "How do I configure the HTTP Request node?" β†’ Activates: n8n Node Configuration + +"How do I access webhook data in a Code node?" +β†’ Activates: n8n Code JavaScript + +"Can I use pandas in Python Code node?" +β†’ Activates: n8n Code Python ``` ### Skills Work Together @@ -170,10 +202,11 @@ When you ask: **"Build and validate a webhook to Slack workflow"** 1. **n8n Workflow Patterns** identifies webhook processing pattern 2. **n8n MCP Tools Expert** searches for webhook and Slack nodes 3. **n8n Node Configuration** guides node setup -4. **n8n Expression Syntax** helps with data mapping -5. **n8n Validation Expert** validates the final workflow +4. **n8n Code JavaScript** helps process webhook data with proper .body access +5. **n8n Expression Syntax** helps with data mapping in other nodes +6. **n8n Validation Expert** validates the final workflow -All 5 skills compose seamlessly! +All skills compose seamlessly! --- @@ -238,9 +271,10 @@ MIT License - see [LICENSE](LICENSE) file for details. ## πŸ“Š What's Included -- **5** complementary skills that work together +- **7** complementary skills that work together - **525+** n8n nodes supported - **2,653+** workflow templates for examples +- **10** production-tested Code node patterns - **Comprehensive** error catalogs and troubleshooting guides --- diff --git a/evaluations/code-javascript/eval-001-webhook-body-gotcha.json b/evaluations/code-javascript/eval-001-webhook-body-gotcha.json new file mode 100644 index 0000000..0ecadfa --- /dev/null +++ b/evaluations/code-javascript/eval-001-webhook-body-gotcha.json @@ -0,0 +1,21 @@ +{ + "id": "code-js-001", + "skills": ["n8n-code-javascript"], + "query": "I'm getting undefined when trying to access data from my webhook in a Code node. My code is: const name = $json.name; const email = $json.email; Why isn't this working?", + "expected_behavior": [ + "Activate n8n-code-javascript skill", + "Explain that webhook data is nested under the .body property", + "Show correct syntax: $json.body.name and $json.body.email", + "Reference DATA_ACCESS.md for webhook structure details", + "Mention this is the #1 most common mistake", + "Optionally show alternative: const webhookData = $json.body; then access webhookData.name" + ], + "expected_content": [ + "webhook data", + ".body", + "$json.body.name", + "DATA_ACCESS.md" + ], + "priority": "high", + "notes": "This is the MOST common mistake. Skill must clearly explain .body nesting." +} diff --git a/evaluations/code-javascript/eval-002-return-format-error.json b/evaluations/code-javascript/eval-002-return-format-error.json new file mode 100644 index 0000000..8993152 --- /dev/null +++ b/evaluations/code-javascript/eval-002-return-format-error.json @@ -0,0 +1,22 @@ +{ + "id": "code-js-002", + "skills": ["n8n-code-javascript"], + "query": "My Code node is showing an error: 'Return value must be an array of objects'. My code returns: return {json: {result: 'success', data: processedData}}; What's wrong?", + "expected_behavior": [ + "Activate n8n-code-javascript skill", + "Explain the return format must be an ARRAY of objects", + "Show correct syntax: return [{json: {result: 'success', data: processedData}}];", + "Note the square brackets [] wrapping the object", + "Reference ERROR_PATTERNS.md #3 for detailed explanation", + "Show multiple items format as well", + "Emphasize this is error #3 in top 5 errors (5% of failures)" + ], + "expected_content": [ + "array", + "[{json:", + "ERROR_PATTERNS.md", + "square brackets" + ], + "priority": "high", + "notes": "Return format error is 5% of all failures. Must clearly show array wrapper requirement." +} diff --git a/evaluations/code-javascript/eval-003-http-request.json b/evaluations/code-javascript/eval-003-http-request.json new file mode 100644 index 0000000..5e334aa --- /dev/null +++ b/evaluations/code-javascript/eval-003-http-request.json @@ -0,0 +1,25 @@ +{ + "id": "code-js-003", + "skills": ["n8n-code-javascript"], + "query": "How do I make an HTTP API call from inside a JavaScript Code node in n8n? I need to call an external API with authentication.", + "expected_behavior": [ + "Activate n8n-code-javascript skill", + "Explain $helpers.httpRequest() built-in function", + "Show complete example with method, url, headers", + "Include authentication example (Bearer token or API key)", + "Show try-catch error handling pattern", + "Reference BUILTIN_FUNCTIONS.md for complete API reference", + "Mention this is async, needs await", + "Show both GET and POST examples if appropriate" + ], + "expected_content": [ + "$helpers.httpRequest", + "await", + "headers", + "Authorization", + "BUILTIN_FUNCTIONS.md", + "try-catch" + ], + "priority": "medium", + "notes": "$helpers.httpRequest() is a key built-in function that many users need to learn." +} diff --git a/evaluations/code-javascript/eval-004-aggregation-pattern.json b/evaluations/code-javascript/eval-004-aggregation-pattern.json new file mode 100644 index 0000000..12c5ff7 --- /dev/null +++ b/evaluations/code-javascript/eval-004-aggregation-pattern.json @@ -0,0 +1,25 @@ +{ + "id": "code-js-004", + "skills": ["n8n-code-javascript"], + "query": "I need to sum up all the 'amount' values from multiple items in my Code node. How do I access all items and calculate a total?", + "expected_behavior": [ + "Activate n8n-code-javascript skill", + "Show $input.all() to get all items", + "Demonstrate reduce() function for summing", + "Include null handling (item.json.amount || 0)", + "Show complete return format with result", + "Reference COMMON_PATTERNS.md for aggregation patterns", + "Reference DATA_ACCESS.md for $input.all() details", + "Emphasize 'Run Once for All Items' mode", + "Optionally show additional aggregations (count, average)" + ], + "expected_content": [ + "$input.all()", + "reduce", + "COMMON_PATTERNS.md", + "DATA_ACCESS.md", + "All Items" + ], + "priority": "high", + "notes": "Aggregation is a very common use case. Tests understanding of $input.all() and array methods." +} diff --git a/evaluations/code-javascript/eval-005-expression-syntax-confusion.json b/evaluations/code-javascript/eval-005-expression-syntax-confusion.json new file mode 100644 index 0000000..9c733ef --- /dev/null +++ b/evaluations/code-javascript/eval-005-expression-syntax-confusion.json @@ -0,0 +1,26 @@ +{ + "id": "code-js-005", + "skills": ["n8n-code-javascript"], + "query": "I'm trying to access a field in my Code node but it's not working. My code is: const userName = '{{ $json.name }}'; I'm getting the literal string instead of the value. What am I doing wrong?", + "expected_behavior": [ + "Activate n8n-code-javascript skill", + "Explain that {{ }} expression syntax does NOT work in Code nodes", + "Clarify distinction: {{ }} is for OTHER nodes (Set, IF, HTTP Request)", + "Show correct JavaScript syntax: const userName = $json.name;", + "Show JavaScript template literals for string interpolation: `Hello ${$json.name}`", + "Reference ERROR_PATTERNS.md #2 for detailed explanation", + "Emphasize this is error #2 in top 5 errors (8% of failures)", + "Provide comparison table of when to use expressions vs JavaScript" + ], + "expected_content": [ + "{{ }}", + "expression syntax", + "JavaScript", + "$json.name", + "template literals", + "ERROR_PATTERNS.md", + "backticks" + ], + "priority": "high", + "notes": "Expression syntax confusion is 8% of failures. Critical to understand Code nodes use JavaScript, not expressions." +} diff --git a/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md b/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md new file mode 100644 index 0000000..71c32b7 --- /dev/null +++ b/skills/n8n-code-javascript/BUILTIN_FUNCTIONS.md @@ -0,0 +1,764 @@ +# Built-in Functions - JavaScript Code Node + +Complete reference for n8n's built-in JavaScript functions and helpers. + +--- + +## Overview + +n8n Code nodes provide powerful built-in functions beyond standard JavaScript. This guide covers: + +1. **$helpers.httpRequest()** - Make HTTP requests +2. **DateTime (Luxon)** - Advanced date/time operations +3. **$jmespath()** - Query JSON structures +4. **$getWorkflowStaticData()** - Persistent storage +5. **Standard JavaScript Globals** - Math, JSON, console, etc. +6. **Available Node.js Modules** - crypto, Buffer, URL + +--- + +## 1. $helpers.httpRequest() - HTTP Requests + +Make HTTP requests directly from Code nodes without using HTTP Request node. + +### Basic Usage + +```javascript +const response = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com/users' +}); + +return [{json: {data: response}}]; +``` + +### Complete Options + +```javascript +const response = await $helpers.httpRequest({ + method: 'POST', // GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS + url: 'https://api.example.com/users', + headers: { + 'Authorization': 'Bearer token123', + 'Content-Type': 'application/json', + 'User-Agent': 'n8n-workflow' + }, + body: { + name: 'John Doe', + email: 'john@example.com' + }, + qs: { // Query string parameters + page: 1, + limit: 10 + }, + timeout: 10000, // Milliseconds (default: no timeout) + json: true, // Auto-parse JSON response (default: true) + simple: false, // Don't throw on HTTP errors (default: true) + resolveWithFullResponse: false // Return only body (default: false) +}); +``` + +### GET Request + +```javascript +// Simple GET +const users = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com/users' +}); + +return [{json: {users}}]; +``` + +```javascript +// GET with query parameters +const results = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com/search', + qs: { + q: 'javascript', + page: 1, + per_page: 50 + } +}); + +return [{json: results}]; +``` + +### POST Request + +```javascript +// POST with JSON body +const newUser = await $helpers.httpRequest({ + method: 'POST', + url: 'https://api.example.com/users', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + $env.API_KEY + }, + body: { + name: $json.body.name, + email: $json.body.email, + role: 'user' + } +}); + +return [{json: newUser}]; +``` + +### PUT/PATCH Request + +```javascript +// Update resource +const updated = await $helpers.httpRequest({ + method: 'PATCH', + url: `https://api.example.com/users/${userId}`, + body: { + name: 'Updated Name', + status: 'active' + } +}); + +return [{json: updated}]; +``` + +### DELETE Request + +```javascript +// Delete resource +await $helpers.httpRequest({ + method: 'DELETE', + url: `https://api.example.com/users/${userId}`, + headers: { + 'Authorization': 'Bearer ' + $env.API_KEY + } +}); + +return [{json: {deleted: true, userId}}]; +``` + +### Authentication Patterns + +```javascript +// Bearer Token +const response = await $helpers.httpRequest({ + url: 'https://api.example.com/data', + headers: { + 'Authorization': `Bearer ${$env.API_TOKEN}` + } +}); +``` + +```javascript +// API Key in Header +const response = await $helpers.httpRequest({ + url: 'https://api.example.com/data', + headers: { + 'X-API-Key': $env.API_KEY + } +}); +``` + +```javascript +// Basic Auth (manual) +const credentials = Buffer.from(`${username}:${password}`).toString('base64'); + +const response = await $helpers.httpRequest({ + url: 'https://api.example.com/data', + headers: { + 'Authorization': `Basic ${credentials}` + } +}); +``` + +### Error Handling + +```javascript +// Handle HTTP errors gracefully +try { + const response = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com/users', + simple: false // Don't throw on 4xx/5xx + }); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return [{json: {success: true, data: response.body}}]; + } else { + return [{ + json: { + success: false, + status: response.statusCode, + error: response.body + } + }]; + } +} catch (error) { + return [{ + json: { + success: false, + error: error.message + } + }]; +} +``` + +### Full Response Access + +```javascript +// Get full response including headers and status +const response = await $helpers.httpRequest({ + url: 'https://api.example.com/data', + resolveWithFullResponse: true +}); + +return [{ + json: { + statusCode: response.statusCode, + headers: response.headers, + body: response.body, + rateLimit: response.headers['x-ratelimit-remaining'] + } +}]; +``` + +--- + +## 2. DateTime (Luxon) - Date & Time Operations + +n8n includes Luxon for powerful date/time handling. Access via `DateTime` global. + +### Current Date/Time + +```javascript +// Current time +const now = DateTime.now(); + +// Current time in specific timezone +const nowTokyo = DateTime.now().setZone('Asia/Tokyo'); + +// Today at midnight +const today = DateTime.now().startOf('day'); + +return [{ + json: { + iso: now.toISO(), // "2025-01-20T15:30:00.000Z" + formatted: now.toFormat('yyyy-MM-dd HH:mm:ss'), // "2025-01-20 15:30:00" + unix: now.toSeconds(), // Unix timestamp + millis: now.toMillis() // Milliseconds since epoch + } +}]; +``` + +### Formatting Dates + +```javascript +const now = DateTime.now(); + +return [{ + json: { + isoFormat: now.toISO(), // ISO 8601: "2025-01-20T15:30:00.000Z" + sqlFormat: now.toSQL(), // SQL: "2025-01-20 15:30:00.000" + httpFormat: now.toHTTP(), // HTTP: "Mon, 20 Jan 2025 15:30:00 GMT" + + // Custom formats + dateOnly: now.toFormat('yyyy-MM-dd'), // "2025-01-20" + timeOnly: now.toFormat('HH:mm:ss'), // "15:30:00" + readable: now.toFormat('MMMM dd, yyyy'), // "January 20, 2025" + compact: now.toFormat('yyyyMMdd'), // "20250120" + withDay: now.toFormat('EEEE, MMMM dd, yyyy'), // "Monday, January 20, 2025" + custom: now.toFormat('dd/MM/yy HH:mm') // "20/01/25 15:30" + } +}]; +``` + +### Parsing Dates + +```javascript +// From ISO string +const dt1 = DateTime.fromISO('2025-01-20T15:30:00'); + +// From specific format +const dt2 = DateTime.fromFormat('01/20/2025', 'MM/dd/yyyy'); + +// From SQL +const dt3 = DateTime.fromSQL('2025-01-20 15:30:00'); + +// From Unix timestamp +const dt4 = DateTime.fromSeconds(1737384600); + +// From milliseconds +const dt5 = DateTime.fromMillis(1737384600000); + +return [{json: {parsed: dt1.toISO()}}]; +``` + +### Date Arithmetic + +```javascript +const now = DateTime.now(); + +return [{ + json: { + // Adding time + tomorrow: now.plus({days: 1}).toISO(), + nextWeek: now.plus({weeks: 1}).toISO(), + nextMonth: now.plus({months: 1}).toISO(), + inTwoHours: now.plus({hours: 2}).toISO(), + + // Subtracting time + yesterday: now.minus({days: 1}).toISO(), + lastWeek: now.minus({weeks: 1}).toISO(), + lastMonth: now.minus({months: 1}).toISO(), + twoHoursAgo: now.minus({hours: 2}).toISO(), + + // Complex operations + in90Days: now.plus({days: 90}).toFormat('yyyy-MM-dd'), + in6Months: now.plus({months: 6}).toFormat('yyyy-MM-dd') + } +}]; +``` + +### Time Comparisons + +```javascript +const now = DateTime.now(); +const targetDate = DateTime.fromISO('2025-12-31'); + +return [{ + json: { + // Comparisons + isFuture: targetDate > now, + isPast: targetDate < now, + isEqual: targetDate.equals(now), + + // Differences + daysUntil: targetDate.diff(now, 'days').days, + hoursUntil: targetDate.diff(now, 'hours').hours, + monthsUntil: targetDate.diff(now, 'months').months, + + // Detailed difference + detailedDiff: targetDate.diff(now, ['months', 'days', 'hours']).toObject() + } +}]; +``` + +### Timezone Operations + +```javascript +const now = DateTime.now(); + +return [{ + json: { + // Current timezone + local: now.toISO(), + + // Convert to different timezone + tokyo: now.setZone('Asia/Tokyo').toISO(), + newYork: now.setZone('America/New_York').toISO(), + london: now.setZone('Europe/London').toISO(), + utc: now.toUTC().toISO(), + + // Get timezone info + timezone: now.zoneName, // "America/Los_Angeles" + offset: now.offset, // Offset in minutes + offsetFormatted: now.toFormat('ZZ') // "+08:00" + } +}]; +``` + +### Start/End of Period + +```javascript +const now = DateTime.now(); + +return [{ + json: { + startOfDay: now.startOf('day').toISO(), + endOfDay: now.endOf('day').toISO(), + startOfWeek: now.startOf('week').toISO(), + endOfWeek: now.endOf('week').toISO(), + startOfMonth: now.startOf('month').toISO(), + endOfMonth: now.endOf('month').toISO(), + startOfYear: now.startOf('year').toISO(), + endOfYear: now.endOf('year').toISO() + } +}]; +``` + +### Weekday & Month Info + +```javascript +const now = DateTime.now(); + +return [{ + json: { + // Day info + weekday: now.weekday, // 1 = Monday, 7 = Sunday + weekdayShort: now.weekdayShort, // "Mon" + weekdayLong: now.weekdayLong, // "Monday" + isWeekend: now.weekday > 5, // Saturday or Sunday + + // Month info + month: now.month, // 1-12 + monthShort: now.monthShort, // "Jan" + monthLong: now.monthLong, // "January" + + // Year info + year: now.year, // 2025 + quarter: now.quarter, // 1-4 + daysInMonth: now.daysInMonth // 28-31 + } +}]; +``` + +--- + +## 3. $jmespath() - JSON Querying + +Query and transform JSON structures using JMESPath syntax. + +### Basic Queries + +```javascript +const data = $input.first().json; + +// Extract specific field +const names = $jmespath(data, 'users[*].name'); + +// Filter array +const adults = $jmespath(data, 'users[?age >= `18`]'); + +// Get specific index +const firstUser = $jmespath(data, 'users[0]'); + +return [{json: {names, adults, firstUser}}]; +``` + +### Advanced Queries + +```javascript +const data = $input.first().json; + +// Sort and slice +const top5 = $jmespath(data, 'users | sort_by(@, &score) | reverse(@) | [0:5]'); + +// Extract nested fields +const emails = $jmespath(data, 'users[*].contact.email'); + +// Multi-field extraction +const simplified = $jmespath(data, 'users[*].{name: name, email: contact.email}'); + +// Conditional filtering +const premium = $jmespath(data, 'users[?subscription.tier == `premium`]'); + +return [{json: {top5, emails, simplified, premium}}]; +``` + +### Common Patterns + +```javascript +// Pattern 1: Filter and project +const query1 = $jmespath(data, 'products[?price > `100`].{name: name, price: price}'); + +// Pattern 2: Aggregate functions +const query2 = $jmespath(data, 'sum(products[*].price)'); +const query3 = $jmespath(data, 'max(products[*].price)'); +const query4 = $jmespath(data, 'length(products)'); + +// Pattern 3: Nested filtering +const query5 = $jmespath(data, 'categories[*].products[?inStock == `true`]'); + +return [{json: {query1, query2, query3, query4, query5}}]; +``` + +--- + +## 4. $getWorkflowStaticData() - Persistent Storage + +Store data that persists across workflow executions. + +### Basic Usage + +```javascript +// Get static data storage +const staticData = $getWorkflowStaticData(); + +// Initialize counter if doesn't exist +if (!staticData.counter) { + staticData.counter = 0; +} + +// Increment counter +staticData.counter++; + +return [{ + json: { + executionCount: staticData.counter + } +}]; +``` + +### Use Cases + +```javascript +// Use Case 1: Rate limiting +const staticData = $getWorkflowStaticData(); +const now = Date.now(); + +if (!staticData.lastRun) { + staticData.lastRun = now; + staticData.runCount = 1; +} else { + const timeSinceLastRun = now - staticData.lastRun; + + if (timeSinceLastRun < 60000) { // Less than 1 minute + return [{json: {error: 'Rate limit: wait 1 minute between runs'}}]; + } + + staticData.lastRun = now; + staticData.runCount++; +} + +return [{json: {allowed: true, totalRuns: staticData.runCount}}]; +``` + +```javascript +// Use Case 2: Tracking last processed ID +const staticData = $getWorkflowStaticData(); +const currentItems = $input.all(); + +// Get last processed ID +const lastId = staticData.lastProcessedId || 0; + +// Filter only new items +const newItems = currentItems.filter(item => item.json.id > lastId); + +// Update last processed ID +if (newItems.length > 0) { + staticData.lastProcessedId = Math.max(...newItems.map(item => item.json.id)); +} + +return newItems; +``` + +```javascript +// Use Case 3: Accumulating results +const staticData = $getWorkflowStaticData(); + +if (!staticData.accumulated) { + staticData.accumulated = []; +} + +// Add current items to accumulated list +const currentData = $input.all().map(item => item.json); +staticData.accumulated.push(...currentData); + +return [{ + json: { + currentBatch: currentData.length, + totalAccumulated: staticData.accumulated.length, + allData: staticData.accumulated + } +}]; +``` + +--- + +## 5. Standard JavaScript Globals + +### Math Object + +```javascript +return [{ + json: { + // Rounding + rounded: Math.round(3.7), // 4 + floor: Math.floor(3.7), // 3 + ceil: Math.ceil(3.2), // 4 + + // Min/Max + max: Math.max(1, 5, 3, 9, 2), // 9 + min: Math.min(1, 5, 3, 9, 2), // 1 + + // Random + random: Math.random(), // 0-1 + randomInt: Math.floor(Math.random() * 100), // 0-99 + + // Other + abs: Math.abs(-5), // 5 + sqrt: Math.sqrt(16), // 4 + pow: Math.pow(2, 3) // 8 + } +}]; +``` + +### JSON Object + +```javascript +// Parse JSON string +const jsonString = '{"name": "John", "age": 30}'; +const parsed = JSON.parse(jsonString); + +// Stringify object +const obj = {name: "John", age: 30}; +const stringified = JSON.stringify(obj); + +// Pretty print +const pretty = JSON.stringify(obj, null, 2); + +return [{json: {parsed, stringified, pretty}}]; +``` + +### console Object + +```javascript +// Debug logging (appears in browser console, press F12) +console.log('Processing items:', $input.all().length); +console.log('First item:', $input.first().json); + +// Other console methods +console.error('Error message'); +console.warn('Warning message'); +console.info('Info message'); + +// Continues to return data +return [{json: {processed: true}}]; +``` + +### Object Methods + +```javascript +const obj = {name: "John", age: 30, city: "NYC"}; + +return [{ + json: { + keys: Object.keys(obj), // ["name", "age", "city"] + values: Object.values(obj), // ["John", 30, "NYC"] + entries: Object.entries(obj), // [["name", "John"], ...] + + // Check property + hasName: 'name' in obj, // true + + // Merge objects + merged: Object.assign({}, obj, {country: "USA"}) + } +}]; +``` + +### Array Methods + +```javascript +const arr = [1, 2, 3, 4, 5]; + +return [{ + json: { + mapped: arr.map(x => x * 2), // [2, 4, 6, 8, 10] + filtered: arr.filter(x => x > 2), // [3, 4, 5] + reduced: arr.reduce((sum, x) => sum + x, 0), // 15 + some: arr.some(x => x > 3), // true + every: arr.every(x => x > 0), // true + find: arr.find(x => x > 3), // 4 + includes: arr.includes(3), // true + joined: arr.join(', ') // "1, 2, 3, 4, 5" + } +}]; +``` + +--- + +## 6. Available Node.js Modules + +### crypto Module + +```javascript +const crypto = require('crypto'); + +// Hash functions +const hash = crypto.createHash('sha256') + .update('my secret text') + .digest('hex'); + +// MD5 hash +const md5 = crypto.createHash('md5') + .update('my text') + .digest('hex'); + +// Random values +const randomBytes = crypto.randomBytes(16).toString('hex'); + +return [{json: {hash, md5, randomBytes}}]; +``` + +### Buffer (built-in) + +```javascript +// Base64 encoding +const encoded = Buffer.from('Hello World').toString('base64'); + +// Base64 decoding +const decoded = Buffer.from(encoded, 'base64').toString(); + +// Hex encoding +const hex = Buffer.from('Hello').toString('hex'); + +return [{json: {encoded, decoded, hex}}]; +``` + +### URL / URLSearchParams + +```javascript +// Parse URL +const url = new URL('https://example.com/path?param1=value1¶m2=value2'); + +// Build query string +const params = new URLSearchParams({ + search: 'query', + page: 1, + limit: 10 +}); + +return [{ + json: { + host: url.host, + pathname: url.pathname, + search: url.search, + queryString: params.toString() // "search=query&page=1&limit=10" + } +}]; +``` + +--- + +## What's NOT Available + +**External npm packages are NOT available:** +- ❌ axios +- ❌ lodash +- ❌ moment (use DateTime/Luxon instead) +- ❌ request +- ❌ Any other npm package + +**Workaround**: Use $helpers.httpRequest() for HTTP, or add data to workflow via HTTP Request node. + +--- + +## Summary + +**Most Useful Built-ins**: +1. **$helpers.httpRequest()** - API calls without HTTP Request node +2. **DateTime** - Professional date/time handling +3. **$jmespath()** - Complex JSON queries +4. **Math, JSON, Object, Array** - Standard JavaScript utilities + +**Common Patterns**: +- API calls: Use $helpers.httpRequest() +- Date operations: Use DateTime (Luxon) +- Data filtering: Use $jmespath() or JavaScript .filter() +- Persistent data: Use $getWorkflowStaticData() +- Hashing: Use crypto module + +**See Also**: +- [SKILL.md](SKILL.md) - Overview +- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - Real usage examples +- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Error prevention diff --git a/skills/n8n-code-javascript/COMMON_PATTERNS.md b/skills/n8n-code-javascript/COMMON_PATTERNS.md new file mode 100644 index 0000000..15aa06d --- /dev/null +++ b/skills/n8n-code-javascript/COMMON_PATTERNS.md @@ -0,0 +1,1110 @@ +# Common Patterns - JavaScript Code Node + +Production-tested patterns for n8n Code nodes. These patterns are proven in real workflows. + +--- + +## Overview + +This guide covers the 10 most useful Code node patterns for n8n workflows. Each pattern includes: +- **Use Case**: When to use this pattern +- **Key Techniques**: Important coding techniques demonstrated +- **Complete Example**: Working code you can adapt +- **Variations**: Common modifications + +**Pattern Categories:** +- Data Aggregation (Patterns 1, 5, 10) +- Content Processing (Patterns 2, 3) +- Data Validation & Comparison (Patterns 4) +- Data Transformation (Patterns 5, 6, 7) +- Output Formatting (Pattern 8) +- Filtering & Ranking (Pattern 9) + +--- + +## Pattern 1: Multi-Source Data Aggregation + +**Use Case**: Combining data from multiple APIs, RSS feeds, webhooks, or databases + +**When to use:** +- Collecting data from multiple services +- Normalizing different API response formats +- Merging data sources into unified structure +- Building aggregated reports + +**Key Techniques**: Loop iteration, conditional parsing, data normalization + +### Complete Example + +```javascript +// Process and structure data collected from multiple sources +const allItems = $input.all(); +let processedArticles = []; + +// Handle different source formats +for (const item of allItems) { + const sourceName = item.json.name || 'Unknown'; + const sourceData = item.json; + + // Parse source-specific structure - Hacker News format + if (sourceName === 'Hacker News' && sourceData.hits) { + for (const hit of sourceData.hits) { + processedArticles.push({ + title: hit.title, + url: hit.url, + summary: hit.story_text || 'No summary', + source: 'Hacker News', + score: hit.points || 0, + fetchedAt: new Date().toISOString() + }); + } + } + + // Parse source-specific structure - Reddit format + else if (sourceName === 'Reddit' && sourceData.data?.children) { + for (const post of sourceData.data.children) { + processedArticles.push({ + title: post.data.title, + url: post.data.url, + summary: post.data.selftext || 'No summary', + source: 'Reddit', + score: post.data.score || 0, + fetchedAt: new Date().toISOString() + }); + } + } + + // Parse source-specific structure - RSS feed format + else if (sourceName === 'RSS' && sourceData.items) { + for (const rssItem of sourceData.items) { + processedArticles.push({ + title: rssItem.title, + url: rssItem.link, + summary: rssItem.description || 'No summary', + source: 'RSS Feed', + score: 0, + fetchedAt: new Date().toISOString() + }); + } + } +} + +// Sort by score (highest first) +processedArticles.sort((a, b) => b.score - a.score); + +return processedArticles.map(article => ({json: article})); +``` + +### Variations + +```javascript +// Variation 1: Add source weighting +for (const article of processedArticles) { + const weights = { + 'Hacker News': 1.5, + 'Reddit': 1.0, + 'RSS Feed': 0.8 + }; + + article.weightedScore = article.score * (weights[article.source] || 1.0); +} + +// Variation 2: Filter by minimum score +processedArticles = processedArticles.filter(article => article.score >= 10); + +// Variation 3: Deduplicate by URL +const seen = new Set(); +processedArticles = processedArticles.filter(article => { + if (seen.has(article.url)) { + return false; + } + seen.add(article.url); + return true; +}); +``` + +--- + +## Pattern 2: Regex Filtering & Pattern Matching + +**Use Case**: Content analysis, keyword extraction, mention tracking, text parsing + +**When to use:** +- Extracting mentions or tags from text +- Finding patterns in unstructured data +- Counting keyword occurrences +- Validating formats (emails, phone numbers) + +**Key Techniques**: Regex matching, object aggregation, sorting/ranking + +### Complete Example + +```javascript +// Extract and track mentions using regex patterns +const etfPattern = /\b([A-Z]{2,5})\b/g; +const knownETFs = ['VOO', 'VTI', 'VT', 'SCHD', 'QYLD', 'VXUS', 'SPY', 'QQQ']; + +const etfMentions = {}; + +for (const item of $input.all()) { + const data = item.json.data; + + // Skip if no data or children + if (!data?.children) continue; + + for (const post of data.children) { + // Combine title and body text + const title = post.data.title || ''; + const body = post.data.selftext || ''; + const combinedText = (title + ' ' + body).toUpperCase(); + + // Find all matches + const matches = combinedText.match(etfPattern); + + if (matches) { + for (const match of matches) { + // Only count known ETFs + if (knownETFs.includes(match)) { + if (!etfMentions[match]) { + etfMentions[match] = { + count: 0, + totalScore: 0, + posts: [] + }; + } + + etfMentions[match].count++; + etfMentions[match].totalScore += post.data.score || 0; + etfMentions[match].posts.push({ + title: post.data.title, + url: post.data.url, + score: post.data.score + }); + } + } + } + } +} + +// Convert to array and sort by mention count +return Object.entries(etfMentions) + .map(([etf, data]) => ({ + json: { + etf, + mentions: data.count, + totalScore: data.totalScore, + averageScore: data.totalScore / data.count, + topPosts: data.posts + .sort((a, b) => b.score - a.score) + .slice(0, 3) + } + })) + .sort((a, b) => b.json.mentions - a.json.mentions); +``` + +### Variations + +```javascript +// Variation 1: Email extraction +const emailPattern = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; +const emails = text.match(emailPattern) || []; + +// Variation 2: Phone number extraction +const phonePattern = /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g; +const phones = text.match(phonePattern) || []; + +// Variation 3: Hashtag extraction +const hashtagPattern = /#(\w+)/g; +const hashtags = []; +let match; +while ((match = hashtagPattern.exec(text)) !== null) { + hashtags.push(match[1]); +} + +// Variation 4: URL extraction +const urlPattern = /https?:\/\/[^\s]+/g; +const urls = text.match(urlPattern) || []; +``` + +--- + +## Pattern 3: Markdown Parsing & Structured Data Extraction + +**Use Case**: Parsing formatted text, extracting structured fields, content transformation + +**When to use:** +- Parsing markdown or HTML +- Extracting data from structured text +- Converting formatted content to JSON +- Processing documentation or articles + +**Key Techniques**: Regex grouping, helper functions, data normalization, while loops for iteration + +### Complete Example + +```javascript +// Parse markdown and extract structured information +const markdown = $input.first().json.data.markdown; +const adRegex = /##\s*(.*?)\n(.*?)(?=\n##|\n---|$)/gs; + +const ads = []; +let match; + +// Helper function to parse time strings to minutes +function parseTimeToMinutes(timeStr) { + if (!timeStr) return 999999; // Sort unparseable times last + + const hourMatch = timeStr.match(/(\d+)\s*hour/); + const dayMatch = timeStr.match(/(\d+)\s*day/); + const minMatch = timeStr.match(/(\d+)\s*min/); + + let totalMinutes = 0; + if (dayMatch) totalMinutes += parseInt(dayMatch[1]) * 1440; // 24 * 60 + if (hourMatch) totalMinutes += parseInt(hourMatch[1]) * 60; + if (minMatch) totalMinutes += parseInt(minMatch[1]); + + return totalMinutes; +} + +// Extract all job postings from markdown +while ((match = adRegex.exec(markdown)) !== null) { + const title = match[1]?.trim() || 'No title'; + const content = match[2]?.trim() || ''; + + // Extract structured fields from content + const districtMatch = content.match(/\*\*District:\*\*\s*(.*?)(?:\n|$)/); + const salaryMatch = content.match(/\*\*Salary:\*\*\s*(.*?)(?:\n|$)/); + const timeMatch = content.match(/Posted:\s*(.*?)\*/); + + ads.push({ + title: title, + district: districtMatch?.[1].trim() || 'Unknown', + salary: salaryMatch?.[1].trim() || 'Not specified', + postedTimeAgo: timeMatch?.[1] || 'Unknown', + timeInMinutes: parseTimeToMinutes(timeMatch?.[1]), + fullContent: content, + extractedAt: new Date().toISOString() + }); +} + +// Sort by recency (posted time) +ads.sort((a, b) => a.timeInMinutes - b.timeInMinutes); + +return ads.map(ad => ({json: ad})); +``` + +### Variations + +```javascript +// Variation 1: Parse HTML table to JSON +const tableRegex = /(.*?)<\/tr>/gs; +const cellRegex = /(.*?)<\/td>/g; + +const rows = []; +let tableMatch; + +while ((tableMatch = tableRegex.exec(htmlTable)) !== null) { + const cells = []; + let cellMatch; + + while ((cellMatch = cellRegex.exec(tableMatch[1])) !== null) { + cells.push(cellMatch[1].trim()); + } + + if (cells.length > 0) { + rows.push(cells); + } +} + +// Variation 2: Extract code blocks from markdown +const codeBlockRegex = /```(\w+)?\n(.*?)```/gs; +const codeBlocks = []; + +while ((match = codeBlockRegex.exec(markdown)) !== null) { + codeBlocks.push({ + language: match[1] || 'plain', + code: match[2].trim() + }); +} + +// Variation 3: Parse YAML frontmatter +const frontmatterRegex = /^---\n(.*?)\n---/s; +const frontmatterMatch = content.match(frontmatterRegex); + +if (frontmatterMatch) { + const yamlLines = frontmatterMatch[1].split('\n'); + const metadata = {}; + + for (const line of yamlLines) { + const [key, ...valueParts] = line.split(':'); + if (key && valueParts.length > 0) { + metadata[key.trim()] = valueParts.join(':').trim(); + } + } +} +``` + +--- + +## Pattern 4: JSON Comparison & Validation + +**Use Case**: Workflow versioning, configuration validation, change detection, data integrity + +**When to use:** +- Comparing two versions of data +- Detecting changes in configurations +- Validating data consistency +- Checking for differences + +**Key Techniques**: JSON ordering, base64 decoding, deep comparison, object manipulation + +### Complete Example + +```javascript +// Compare and validate JSON objects from different sources +const orderJsonKeys = (jsonObj) => { + const ordered = {}; + Object.keys(jsonObj).sort().forEach(key => { + ordered[key] = jsonObj[key]; + }); + return ordered; +}; + +const allItems = $input.all(); + +// Assume first item is base64-encoded original, second is current +const origWorkflow = JSON.parse( + Buffer.from(allItems[0].json.content, 'base64').toString() +); +const currentWorkflow = allItems[1].json; + +// Order keys for consistent comparison +const orderedOriginal = orderJsonKeys(origWorkflow); +const orderedCurrent = orderJsonKeys(currentWorkflow); + +// Deep comparison +const isSame = JSON.stringify(orderedOriginal) === JSON.stringify(orderedCurrent); + +// Find differences +const differences = []; +for (const key of Object.keys(orderedOriginal)) { + if (JSON.stringify(orderedOriginal[key]) !== JSON.stringify(orderedCurrent[key])) { + differences.push({ + field: key, + original: orderedOriginal[key], + current: orderedCurrent[key] + }); + } +} + +// Check for new keys +for (const key of Object.keys(orderedCurrent)) { + if (!(key in orderedOriginal)) { + differences.push({ + field: key, + original: null, + current: orderedCurrent[key], + status: 'new' + }); + } +} + +return [{ + json: { + identical: isSame, + differenceCount: differences.length, + differences: differences, + original: orderedOriginal, + current: orderedCurrent, + comparedAt: new Date().toISOString() + } +}]; +``` + +### Variations + +```javascript +// Variation 1: Simple equality check +const isEqual = JSON.stringify(obj1) === JSON.stringify(obj2); + +// Variation 2: Deep diff with detailed changes +function deepDiff(obj1, obj2, path = '') { + const changes = []; + + for (const key in obj1) { + const currentPath = path ? `${path}.${key}` : key; + + if (!(key in obj2)) { + changes.push({type: 'removed', path: currentPath, value: obj1[key]}); + } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { + changes.push(...deepDiff(obj1[key], obj2[key], currentPath)); + } else if (obj1[key] !== obj2[key]) { + changes.push({ + type: 'modified', + path: currentPath, + from: obj1[key], + to: obj2[key] + }); + } + } + + for (const key in obj2) { + if (!(key in obj1)) { + const currentPath = path ? `${path}.${key}` : key; + changes.push({type: 'added', path: currentPath, value: obj2[key]}); + } + } + + return changes; +} + +// Variation 3: Schema validation +function validateSchema(data, schema) { + const errors = []; + + for (const field of schema.required || []) { + if (!(field in data)) { + errors.push(`Missing required field: ${field}`); + } + } + + for (const [field, type] of Object.entries(schema.types || {})) { + if (field in data && typeof data[field] !== type) { + errors.push(`Field ${field} should be ${type}, got ${typeof data[field]}`); + } + } + + return { + valid: errors.length === 0, + errors + }; +} +``` + +--- + +## Pattern 5: CRM Data Transformation + +**Use Case**: Lead enrichment, data normalization, API preparation, form data processing + +**When to use:** +- Processing form submissions +- Preparing data for CRM APIs +- Normalizing contact information +- Enriching lead data + +**Key Techniques**: Object destructuring, data mapping, format conversion, field splitting + +### Complete Example + +```javascript +// Transform form data into CRM-compatible format +const item = $input.all()[0]; +const { + name, + email, + phone, + company, + course_interest, + message, + timestamp +} = item.json; + +// Split name into first and last +const nameParts = name.split(' '); +const firstName = nameParts[0] || ''; +const lastName = nameParts.slice(1).join(' ') || 'Unknown'; + +// Format phone number +const cleanPhone = phone.replace(/[^\d]/g, ''); // Remove non-digits + +// Build CRM data structure +const crmData = { + data: { + type: 'Contact', + attributes: { + first_name: firstName, + last_name: lastName, + email1: email, + phone_work: cleanPhone, + account_name: company, + description: `Course Interest: ${course_interest}\n\nMessage: ${message}\n\nSubmitted: ${timestamp}`, + lead_source: 'Website Form', + status: 'New' + } + }, + metadata: { + original_submission: timestamp, + processed_at: new Date().toISOString() + } +}; + +return [{ + json: { + ...item.json, + crmData, + processed: true + } +}]; +``` + +### Variations + +```javascript +// Variation 1: Multiple contact processing +const contacts = $input.all(); + +return contacts.map(item => { + const data = item.json; + const [firstName, ...lastNameParts] = data.name.split(' '); + + return { + json: { + firstName, + lastName: lastNameParts.join(' ') || 'Unknown', + email: data.email.toLowerCase(), + phone: data.phone.replace(/[^\d]/g, ''), + tags: [data.source, data.interest_level].filter(Boolean) + } + }; +}); + +// Variation 2: Field validation and normalization +function normalizePContact(raw) { + return { + first_name: raw.firstName?.trim() || '', + last_name: raw.lastName?.trim() || 'Unknown', + email: raw.email?.toLowerCase().trim() || '', + phone: raw.phone?.replace(/[^\d]/g, '') || '', + company: raw.company?.trim() || 'Unknown', + title: raw.title?.trim() || '', + valid: Boolean(raw.email && raw.firstName) + }; +} + +// Variation 3: Lead scoring +function calculateLeadScore(data) { + let score = 0; + + if (data.email) score += 10; + if (data.phone) score += 10; + if (data.company) score += 15; + if (data.title?.toLowerCase().includes('director')) score += 20; + if (data.title?.toLowerCase().includes('manager')) score += 15; + if (data.message?.length > 100) score += 10; + + return score; +} +``` + +--- + +## Pattern 6: Release Information Processing + +**Use Case**: Version management, changelog parsing, release notes generation, GitHub API processing + +**When to use:** +- Processing GitHub releases +- Filtering stable versions +- Generating changelog summaries +- Extracting version information + +**Key Techniques**: Array filtering, conditional field extraction, date formatting, string manipulation + +### Complete Example + +```javascript +// Extract and filter stable releases from GitHub API +const allReleases = $input.first().json; + +const stableReleases = allReleases + .filter(release => !release.prerelease && !release.draft) + .slice(0, 10) + .map(release => { + // Extract highlights section from changelog + const body = release.body || ''; + let highlights = 'No highlights available'; + + if (body.includes('## Highlights:')) { + highlights = body.split('## Highlights:')[1]?.split('##')[0]?.trim(); + } else { + // Fallback to first 500 chars + highlights = body.substring(0, 500) + '...'; + } + + return { + tag: release.tag_name, + name: release.name, + published: release.published_at, + publishedDate: new Date(release.published_at).toLocaleDateString(), + author: release.author.login, + url: release.html_url, + changelog: body, + highlights: highlights, + assetCount: release.assets.length, + assets: release.assets.map(asset => ({ + name: asset.name, + size: asset.size, + downloadCount: asset.download_count, + downloadUrl: asset.browser_download_url + })) + }; + }); + +return stableReleases.map(release => ({json: release})); +``` + +### Variations + +```javascript +// Variation 1: Version comparison +function compareVersions(v1, v2) { + const parts1 = v1.replace('v', '').split('.').map(Number); + const parts2 = v2.replace('v', '').split('.').map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const num1 = parts1[i] || 0; + const num2 = parts2[i] || 0; + + if (num1 > num2) return 1; + if (num1 < num2) return -1; + } + + return 0; +} + +// Variation 2: Breaking change detection +function hasBreakingChanges(changelog) { + const breakingKeywords = [ + 'BREAKING CHANGE', + 'breaking change', + 'BC:', + 'πŸ’₯' + ]; + + return breakingKeywords.some(keyword => changelog.includes(keyword)); +} + +// Variation 3: Extract version numbers +const versionPattern = /v?(\d+)\.(\d+)\.(\d+)/; +const match = tagName.match(versionPattern); + +if (match) { + const [_, major, minor, patch] = match; + const version = {major: parseInt(major), minor: parseInt(minor), patch: parseInt(patch)}; +} +``` + +--- + +## Pattern 7: Array Transformation with Context + +**Use Case**: Quick data transformation, field mapping, adding computed fields + +**When to use:** +- Transforming arrays with additional context +- Adding calculated fields +- Simplifying complex objects +- Pluralization logic + +**Key Techniques**: Array methods chaining, ternary operators, computed properties + +### Complete Example + +```javascript +// Transform releases with contextual information +const releases = $input.first().json + .filter(release => !release.prerelease && !release.draft) + .slice(0, 10) + .map(release => ({ + version: release.tag_name, + assetCount: release.assets.length, + assetsCountText: release.assets.length === 1 ? 'file' : 'files', + downloadUrl: release.html_url, + isRecent: new Date(release.published_at) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), + age: Math.floor((Date.now() - new Date(release.published_at)) / (24 * 60 * 60 * 1000)), + ageText: `${Math.floor((Date.now() - new Date(release.published_at)) / (24 * 60 * 60 * 1000))} days ago` + })); + +return releases.map(release => ({json: release})); +``` + +### Variations + +```javascript +// Variation 1: Add ranking +const items = $input.all() + .sort((a, b) => b.json.score - a.json.score) + .map((item, index) => ({ + json: { + ...item.json, + rank: index + 1, + medal: index < 3 ? ['πŸ₯‡', 'πŸ₯ˆ', 'πŸ₯‰'][index] : '' + } + })); + +// Variation 2: Add percentage calculations +const total = $input.all().reduce((sum, item) => sum + item.json.value, 0); + +const itemsWithPercentage = $input.all().map(item => ({ + json: { + ...item.json, + percentage: ((item.json.value / total) * 100).toFixed(2) + '%' + } +})); + +// Variation 3: Add category labels +const categorize = (value) => { + if (value > 100) return 'High'; + if (value > 50) return 'Medium'; + return 'Low'; +}; + +const categorized = $input.all().map(item => ({ + json: { + ...item.json, + category: categorize(item.json.value) + } +})); +``` + +--- + +## Pattern 8: Slack Block Kit Formatting + +**Use Case**: Chat notifications, rich message formatting, interactive messages + +**When to use:** +- Sending formatted Slack messages +- Creating interactive notifications +- Building rich content for chat platforms +- Status reports and alerts + +**Key Techniques**: Template literals, nested objects, Block Kit syntax, date formatting + +### Complete Example + +```javascript +// Create Slack-formatted message with structured blocks +const date = new Date().toISOString().split('T')[0]; +const data = $input.first().json; + +return [{ + json: { + text: `Daily Report - ${date}`, // Fallback text + blocks: [ + { + type: "header", + text: { + type: "plain_text", + text: `πŸ“Š Daily Security Report - ${date}` + } + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Status:* ${data.status === 'ok' ? 'βœ… All Clear' : '⚠️ Issues Detected'}\n*Alerts:* ${data.alertCount || 0}\n*Updated:* ${new Date().toLocaleString()}` + } + }, + { + type: "divider" + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: `*Failed Logins:*\n${data.failedLogins || 0}` + }, + { + type: "mrkdwn", + text: `*API Errors:*\n${data.apiErrors || 0}` + }, + { + type: "mrkdwn", + text: `*Uptime:*\n${data.uptime || '100%'}` + }, + { + type: "mrkdwn", + text: `*Response Time:*\n${data.avgResponseTime || 'N/A'}ms` + } + ] + }, + { + type: "context", + elements: [{ + type: "mrkdwn", + text: `Report generated automatically by n8n workflow` + }] + } + ] + } +}]; +``` + +### Variations + +```javascript +// Variation 1: Interactive buttons +const blocksWithButtons = [ + { + type: "section", + text: { + type: "mrkdwn", + text: "Would you like to approve this request?" + }, + accessory: { + type: "button", + text: { + type: "plain_text", + text: "Approve" + }, + style: "primary", + value: "approve", + action_id: "approve_button" + } + } +]; + +// Variation 2: List formatting +const items = ['Item 1', 'Item 2', 'Item 3']; +const formattedList = items.map((item, i) => `${i + 1}. ${item}`).join('\n'); + +// Variation 3: Status indicators +function getStatusEmoji(status) { + const statusMap = { + 'success': 'βœ…', + 'warning': '⚠️', + 'error': '❌', + 'info': 'ℹ️' + }; + + return statusMap[status] || 'β€’'; +} + +// Variation 4: Truncate long messages +function truncate(text, maxLength = 3000) { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength - 3) + '...'; +} +``` + +--- + +## Pattern 9: Top N Filtering & Ranking + +**Use Case**: RAG pipelines, ranking algorithms, result filtering, leaderboards + +**When to use:** +- Getting top results by score +- Filtering best/worst performers +- Building leaderboards +- Relevance ranking + +**Key Techniques**: Sorting, slicing, null coalescing, score calculations + +### Complete Example + +```javascript +// Filter and rank by similarity score, return top results +const ragResponse = $input.item.json; +const chunks = ragResponse.chunks || []; + +// Sort by similarity (highest first) +const topChunks = chunks + .sort((a, b) => (b.similarity || 0) - (a.similarity || 0)) + .slice(0, 6); + +return [{ + json: { + query: ragResponse.query, + topChunks: topChunks, + count: topChunks.length, + maxSimilarity: topChunks[0]?.similarity || 0, + minSimilarity: topChunks[topChunks.length - 1]?.similarity || 0, + averageSimilarity: topChunks.reduce((sum, chunk) => sum + (chunk.similarity || 0), 0) / topChunks.length + } +}]; +``` + +### Variations + +```javascript +// Variation 1: Top N with minimum threshold +const threshold = 0.7; +const topItems = $input.all() + .filter(item => item.json.score >= threshold) + .sort((a, b) => b.json.score - a.json.score) + .slice(0, 10); + +// Variation 2: Bottom N (worst performers) +const bottomItems = $input.all() + .sort((a, b) => a.json.score - b.json.score) // Ascending + .slice(0, 5); + +// Variation 3: Top N by multiple criteria +const ranked = $input.all() + .map(item => ({ + ...item, + compositeScore: (item.json.relevance * 0.6) + (item.json.recency * 0.4) + })) + .sort((a, b) => b.compositeScore - a.compositeScore) + .slice(0, 10); + +// Variation 4: Percentile filtering +const allScores = $input.all().map(item => item.json.score).sort((a, b) => b - a); +const percentile95 = allScores[Math.floor(allScores.length * 0.05)]; + +const topPercentile = $input.all().filter(item => item.json.score >= percentile95); +``` + +--- + +## Pattern 10: String Aggregation & Reporting + +**Use Case**: Report generation, log aggregation, content concatenation, summary creation + +**When to use:** +- Combining multiple text outputs +- Generating reports from data +- Aggregating logs or messages +- Creating formatted summaries + +**Key Techniques**: Array joining, string concatenation, template literals, timestamp handling + +### Complete Example + +```javascript +// Aggregate multiple text inputs into formatted report +const allItems = $input.all(); + +// Collect all messages +const messages = allItems.map(item => item.json.message); + +// Build report +const header = `🎯 **Daily Summary Report**\nπŸ“… ${new Date().toLocaleString()}\nπŸ“Š Total Items: ${messages.length}\n\n`; +const divider = '\n\n---\n\n'; +const footer = `\n\n---\n\nβœ… Report generated at ${new Date().toISOString()}`; + +const finalReport = header + messages.join(divider) + footer; + +return [{ + json: { + report: finalReport, + messageCount: messages.length, + generatedAt: new Date().toISOString(), + reportLength: finalReport.length + } +}]; +``` + +### Variations + +```javascript +// Variation 1: Numbered list +const numberedReport = allItems + .map((item, index) => `${index + 1}. ${item.json.title}\n ${item.json.description}`) + .join('\n\n'); + +// Variation 2: Markdown table +const headers = '| Name | Status | Score |\n|------|--------|-------|\n'; +const rows = allItems + .map(item => `| ${item.json.name} | ${item.json.status} | ${item.json.score} |`) + .join('\n'); + +const table = headers + rows; + +// Variation 3: HTML report +const htmlReport = ` + + +Report + +

Report - ${new Date().toLocaleDateString()}

+ + + +`; + +// Variation 4: JSON summary +const summary = { + generated: new Date().toISOString(), + totalItems: allItems.length, + items: allItems.map(item => item.json), + statistics: { + total: allItems.reduce((sum, item) => sum + (item.json.value || 0), 0), + average: allItems.reduce((sum, item) => sum + (item.json.value || 0), 0) / allItems.length, + max: Math.max(...allItems.map(item => item.json.value || 0)), + min: Math.min(...allItems.map(item => item.json.value || 0)) + } +}; +``` + +--- + +## Choosing the Right Pattern + +### Pattern Selection Guide + +| Your Goal | Use Pattern | +|-----------|-------------| +| Combine multiple API responses | Pattern 1 (Multi-source Aggregation) | +| Extract mentions or keywords | Pattern 2 (Regex Filtering) | +| Parse formatted text | Pattern 3 (Markdown Parsing) | +| Detect changes in data | Pattern 4 (JSON Comparison) | +| Prepare form data for CRM | Pattern 5 (CRM Transformation) | +| Process GitHub releases | Pattern 6 (Release Processing) | +| Add computed fields | Pattern 7 (Array Transformation) | +| Format Slack messages | Pattern 8 (Block Kit Formatting) | +| Get top results | Pattern 9 (Top N Filtering) | +| Create text reports | Pattern 10 (String Aggregation) | + +### Combining Patterns + +Many real workflows combine multiple patterns: + +```javascript +// Example: Multi-source aggregation + Top N filtering +const allItems = $input.all(); +const aggregated = []; + +// Pattern 1: Aggregate from different sources +for (const item of allItems) { + // ... aggregation logic + aggregated.push(normalizedItem); +} + +// Pattern 9: Get top 10 by score +const top10 = aggregated + .sort((a, b) => b.score - a.score) + .slice(0, 10); + +// Pattern 10: Generate report +const report = `Top 10 Items:\n\n${top10.map((item, i) => `${i + 1}. ${item.title} (${item.score})`).join('\n')}`; + +return [{json: {report, items: top10}}]; +``` + +--- + +## Summary + +**Most Useful Patterns**: +1. Multi-source Aggregation - Combining data from APIs, databases +2. Top N Filtering - Rankings, leaderboards, best results +3. Data Transformation - CRM data, field mapping, enrichment + +**Key Techniques Across Patterns**: +- Array methods (map, filter, reduce, sort, slice) +- Regex for pattern matching +- Object manipulation and destructuring +- Error handling with optional chaining +- Template literals for formatting + +**See Also**: +- [DATA_ACCESS.md](DATA_ACCESS.md) - Data access methods +- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Avoid common mistakes +- [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) - Built-in helpers diff --git a/skills/n8n-code-javascript/DATA_ACCESS.md b/skills/n8n-code-javascript/DATA_ACCESS.md new file mode 100644 index 0000000..50a8d20 --- /dev/null +++ b/skills/n8n-code-javascript/DATA_ACCESS.md @@ -0,0 +1,782 @@ +# Data Access Patterns - JavaScript Code Node + +Comprehensive guide to accessing data in n8n Code nodes using JavaScript. + +--- + +## Overview + +In n8n Code nodes, you access data from previous nodes using built-in variables and methods. Understanding which method to use is critical for correct workflow execution. + +**Data Access Priority** (by common usage): +1. **`$input.all()`** - Most common - Batch operations, aggregations +2. **`$input.first()`** - Very common - Single item operations +3. **`$input.item`** - Common - Each Item mode only +4. **`$node["NodeName"].json`** - Specific node references +5. **`$json`** - Direct current item (legacy, use `$input` instead) + +--- + +## Pattern 1: $input.all() - Process All Items + +**Usage**: Most common pattern for batch processing + +**When to use:** +- Processing multiple records +- Aggregating data (sum, count, average) +- Filtering arrays +- Transforming datasets +- Comparing items +- Sorting or ranking + +### Basic Usage + +```javascript +// Get all items from previous node +const allItems = $input.all(); + +// allItems is an array of objects like: +// [ +// {json: {id: 1, name: "Alice"}}, +// {json: {id: 2, name: "Bob"}} +// ] + +console.log(`Received ${allItems.length} items`); + +return allItems; +``` + +### Example 1: Filter Active Items + +```javascript +const allItems = $input.all(); + +// Filter only active items +const activeItems = allItems.filter(item => item.json.status === 'active'); + +return activeItems; +``` + +### Example 2: Transform All Items + +```javascript +const allItems = $input.all(); + +// Map to new structure +const transformed = allItems.map(item => ({ + json: { + id: item.json.id, + fullName: `${item.json.firstName} ${item.json.lastName}`, + email: item.json.email, + processedAt: new Date().toISOString() + } +})); + +return transformed; +``` + +### Example 3: Aggregate Data + +```javascript +const allItems = $input.all(); + +// Calculate total +const total = allItems.reduce((sum, item) => { + return sum + (item.json.amount || 0); +}, 0); + +return [{ + json: { + total, + count: allItems.length, + average: total / allItems.length + } +}]; +``` + +### Example 4: Sort and Limit + +```javascript +const allItems = $input.all(); + +// Get top 5 by score +const topFive = allItems + .sort((a, b) => (b.json.score || 0) - (a.json.score || 0)) + .slice(0, 5); + +return topFive.map(item => ({json: item.json})); +``` + +### Example 5: Group By Category + +```javascript +const allItems = $input.all(); + +// Group items by category +const grouped = {}; + +for (const item of allItems) { + const category = item.json.category || 'Uncategorized'; + + if (!grouped[category]) { + grouped[category] = []; + } + + grouped[category].push(item.json); +} + +// Convert to array format +return Object.entries(grouped).map(([category, items]) => ({ + json: { + category, + items, + count: items.length + } +})); +``` + +### Example 6: Deduplicate by ID + +```javascript +const allItems = $input.all(); + +// Remove duplicates by ID +const seen = new Set(); +const unique = []; + +for (const item of allItems) { + const id = item.json.id; + + if (!seen.has(id)) { + seen.add(id); + unique.push(item); + } +} + +return unique; +``` + +--- + +## Pattern 2: $input.first() - Get First Item + +**Usage**: Very common for single-item operations + +**When to use:** +- Previous node returns single object +- Working with API responses +- Getting initial/first data point +- Configuration or metadata access + +### Basic Usage + +```javascript +// Get first item from previous node +const firstItem = $input.first(); + +// Access the JSON data +const data = firstItem.json; + +console.log('First item:', data); + +return [{json: data}]; +``` + +### Example 1: Process Single API Response + +```javascript +// Get API response (typically single object) +const response = $input.first().json; + +// Extract what you need +return [{ + json: { + userId: response.data.user.id, + userName: response.data.user.name, + status: response.status, + fetchedAt: new Date().toISOString() + } +}]; +``` + +### Example 2: Transform Single Object + +```javascript +const data = $input.first().json; + +// Transform structure +return [{ + json: { + id: data.id, + contact: { + email: data.email, + phone: data.phone + }, + address: { + street: data.street, + city: data.city, + zip: data.zip + } + } +}]; +``` + +### Example 3: Validate Single Item + +```javascript +const item = $input.first().json; + +// Validation logic +const isValid = item.email && item.email.includes('@'); + +return [{ + json: { + ...item, + valid: isValid, + validatedAt: new Date().toISOString() + } +}]; +``` + +### Example 4: Extract Nested Data + +```javascript +const response = $input.first().json; + +// Navigate nested structure +const users = response.data?.users || []; + +return users.map(user => ({ + json: { + id: user.id, + name: user.profile?.name || 'Unknown', + email: user.contact?.email || 'no-email' + } +})); +``` + +### Example 5: Combine with Other Methods + +```javascript +// Get first item's data +const firstData = $input.first().json; + +// Use it to filter all items +const allItems = $input.all(); +const matching = allItems.filter(item => + item.json.category === firstData.targetCategory +); + +return matching; +``` + +--- + +## Pattern 3: $input.item - Current Item (Each Item Mode) + +**Usage**: Common in "Run Once for Each Item" mode + +**When to use:** +- Mode is set to "Run Once for Each Item" +- Need to process items independently +- Per-item API calls or validations +- Item-specific error handling + +**IMPORTANT**: Only use in "Each Item" mode. Will be undefined in "All Items" mode. + +### Basic Usage + +```javascript +// In "Run Once for Each Item" mode +const currentItem = $input.item; +const data = currentItem.json; + +console.log('Processing item:', data.id); + +return [{ + json: { + ...data, + processed: true + } +}]; +``` + +### Example 1: Add Processing Metadata + +```javascript +const item = $input.item; + +return [{ + json: { + ...item.json, + processed: true, + processedAt: new Date().toISOString(), + processingDuration: Math.random() * 1000 // Simulated duration + } +}]; +``` + +### Example 2: Per-Item Validation + +```javascript +const item = $input.item; +const data = item.json; + +// Validate this specific item +const errors = []; + +if (!data.email) errors.push('Email required'); +if (!data.name) errors.push('Name required'); +if (data.age && data.age < 18) errors.push('Must be 18+'); + +return [{ + json: { + ...data, + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined + } +}]; +``` + +### Example 3: Item-Specific API Call + +```javascript +const item = $input.item; +const userId = item.json.userId; + +// Make API call specific to this item +const response = await $helpers.httpRequest({ + method: 'GET', + url: `https://api.example.com/users/${userId}/details` +}); + +return [{ + json: { + ...item.json, + details: response + } +}]; +``` + +### Example 4: Conditional Processing + +```javascript +const item = $input.item; +const data = item.json; + +// Process based on item type +if (data.type === 'premium') { + return [{ + json: { + ...data, + discount: 0.20, + tier: 'premium' + } + }]; +} else { + return [{ + json: { + ...data, + discount: 0.05, + tier: 'standard' + } + }]; +} +``` + +--- + +## Pattern 4: $node - Reference Other Nodes + +**Usage**: Less common, but powerful for specific scenarios + +**When to use:** +- Need data from specific named node +- Combining data from multiple nodes +- Accessing metadata about workflow execution + +### Basic Usage + +```javascript +// Get output from specific node +const webhookData = $node["Webhook"].json; +const apiData = $node["HTTP Request"].json; + +return [{ + json: { + fromWebhook: webhookData, + fromAPI: apiData + } +}]; +``` + +### Example 1: Combine Multiple Sources + +```javascript +// Reference multiple nodes +const webhook = $node["Webhook"].json; +const database = $node["Postgres"].json; +const api = $node["HTTP Request"].json; + +return [{ + json: { + combined: { + webhook: webhook.body, + dbRecords: database.length, + apiResponse: api.status + }, + processedAt: new Date().toISOString() + } +}]; +``` + +### Example 2: Compare Across Nodes + +```javascript +const oldData = $node["Get Old Data"].json; +const newData = $node["Get New Data"].json; + +// Compare +const changes = { + added: newData.filter(n => !oldData.find(o => o.id === n.id)), + removed: oldData.filter(o => !newData.find(n => n.id === o.id)), + modified: newData.filter(n => { + const old = oldData.find(o => o.id === n.id); + return old && JSON.stringify(old) !== JSON.stringify(n); + }) +}; + +return [{ + json: { + changes, + summary: { + added: changes.added.length, + removed: changes.removed.length, + modified: changes.modified.length + } + } +}]; +``` + +### Example 3: Access Node Metadata + +```javascript +// Get data from specific execution path +const ifTrueBranch = $node["IF True"].json; +const ifFalseBranch = $node["IF False"].json; + +// Use whichever branch executed +const result = ifTrueBranch || ifFalseBranch || {}; + +return [{json: result}]; +``` + +--- + +## Critical: Webhook Data Structure + +**MOST COMMON MISTAKE**: Forgetting webhook data is nested under `.body` + +### The Problem + +Webhook node wraps all incoming data under a `body` property. This catches many developers by surprise. + +### Structure + +```javascript +// Webhook node output structure: +{ + "headers": { + "content-type": "application/json", + "user-agent": "...", + // ... other headers + }, + "params": {}, + "query": {}, + "body": { + // ← YOUR DATA IS HERE + "name": "Alice", + "email": "alice@example.com", + "message": "Hello!" + } +} +``` + +### Wrong vs Right + +```javascript +// ❌ WRONG: Trying to access directly +const name = $json.name; // undefined +const email = $json.email; // undefined + +// βœ… CORRECT: Access via .body +const name = $json.body.name; // "Alice" +const email = $json.body.email; // "alice@example.com" + +// βœ… CORRECT: Extract body first +const webhookData = $json.body; +const name = webhookData.name; // "Alice" +const email = webhookData.email; // "alice@example.com" +``` + +### Example: Full Webhook Processing + +```javascript +// Get webhook data from previous node +const webhookOutput = $input.first().json; + +// Access the actual payload +const payload = webhookOutput.body; + +// Access headers if needed +const contentType = webhookOutput.headers['content-type']; + +// Access query parameters if needed +const apiKey = webhookOutput.query.api_key; + +// Process the actual data +return [{ + json: { + // Data from webhook body + userName: payload.name, + userEmail: payload.email, + message: payload.message, + + // Metadata + receivedAt: new Date().toISOString(), + contentType: contentType, + authenticated: !!apiKey + } +}]; +``` + +### POST Data, Query Params, and Headers + +```javascript +const webhook = $input.first().json; + +return [{ + json: { + // POST body data + formData: webhook.body, + + // Query parameters (?key=value) + queryParams: webhook.query, + + // HTTP headers + userAgent: webhook.headers['user-agent'], + contentType: webhook.headers['content-type'], + + // Request metadata + method: webhook.method, // POST, GET, etc. + url: webhook.url + } +}]; +``` + +### Common Webhook Scenarios + +```javascript +// Scenario 1: Form submission +const formData = $json.body; +const name = formData.name; +const email = formData.email; + +// Scenario 2: JSON API webhook +const apiPayload = $json.body; +const eventType = apiPayload.event; +const data = apiPayload.data; + +// Scenario 3: Query parameters +const apiKey = $json.query.api_key; +const userId = $json.query.user_id; + +// Scenario 4: Headers +const authorization = $json.headers['authorization']; +const signature = $json.headers['x-signature']; +``` + +--- + +## Choosing the Right Pattern + +### Decision Tree + +``` +Do you need ALL items from previous node? +β”œβ”€ YES β†’ Use $input.all() +β”‚ +└─ NO β†’ Do you need just the FIRST item? + β”œβ”€ YES β†’ Use $input.first() + β”‚ + └─ NO β†’ Are you in "Each Item" mode? + β”œβ”€ YES β†’ Use $input.item + β”‚ + └─ NO β†’ Do you need specific node data? + β”œβ”€ YES β†’ Use $node["NodeName"] + └─ NO β†’ Use $input.first() (default) +``` + +### Quick Reference Table + +| Scenario | Use This | Example | +|----------|----------|---------| +| Sum all amounts | `$input.all()` | `allItems.reduce((sum, i) => sum + i.json.amount, 0)` | +| Get API response | `$input.first()` | `$input.first().json.data` | +| Process each independently | `$input.item` | `$input.item.json` (Each Item mode) | +| Combine two nodes | `$node["Name"]` | `$node["API"].json` | +| Filter array | `$input.all()` | `allItems.filter(i => i.json.active)` | +| Transform single object | `$input.first()` | `{...input.first().json, new: true}` | +| Webhook data | `$input.first()` | `$input.first().json.body` | + +--- + +## Common Mistakes + +### Mistake 1: Using $json Without Context + +```javascript +// ❌ WRONG: $json is ambiguous +const value = $json.field; + +// βœ… CORRECT: Be explicit +const value = $input.first().json.field; +``` + +### Mistake 2: Forgetting .json Property + +```javascript +// ❌ WRONG: Trying to access fields on item object +const items = $input.all(); +const names = items.map(item => item.name); // undefined + +// βœ… CORRECT: Access via .json +const names = items.map(item => item.json.name); +``` + +### Mistake 3: Using $input.item in All Items Mode + +```javascript +// ❌ WRONG: $input.item is undefined in "All Items" mode +const data = $input.item.json; // Error! + +// βœ… CORRECT: Use appropriate method +const data = $input.first().json; // Or $input.all() +``` + +### Mistake 4: Not Handling Empty Arrays + +```javascript +// ❌ WRONG: Crashes if no items +const first = $input.all()[0].json; + +// βœ… CORRECT: Check length first +const items = $input.all(); +if (items.length === 0) { + return []; +} +const first = items[0].json; + +// βœ… ALSO CORRECT: Use $input.first() +const first = $input.first().json; // Built-in safety +``` + +### Mistake 5: Modifying Original Data + +```javascript +// ❌ RISKY: Mutating original +const items = $input.all(); +items[0].json.modified = true; // Modifies original +return items; + +// βœ… SAFE: Create new objects +const items = $input.all(); +return items.map(item => ({ + json: { + ...item.json, + modified: true + } +})); +``` + +--- + +## Advanced Patterns + +### Pattern: Pagination Handling + +```javascript +const currentPage = $input.all(); +const pageNumber = $node["Set Page"].json.page || 1; + +// Combine with previous pages +const allPreviousPages = $node["Accumulator"]?.json.accumulated || []; + +return [{ + json: { + accumulated: [...allPreviousPages, ...currentPage], + currentPage: pageNumber, + totalItems: allPreviousPages.length + currentPage.length + } +}]; +``` + +### Pattern: Conditional Node Reference + +```javascript +// Access different nodes based on condition +const condition = $input.first().json.type; + +let data; +if (condition === 'api') { + data = $node["API Response"].json; +} else if (condition === 'database') { + data = $node["Database"].json; +} else { + data = $node["Default"].json; +} + +return [{json: data}]; +``` + +### Pattern: Multi-Node Aggregation + +```javascript +// Collect data from multiple named nodes +const sources = ['Source1', 'Source2', 'Source3']; +const allData = []; + +for (const source of sources) { + const nodeData = $node[source]?.json; + if (nodeData) { + allData.push({ + source, + data: nodeData + }); + } +} + +return allData.map(item => ({json: item})); +``` + +--- + +## Summary + +**Most Common Patterns**: +1. `$input.all()` - Process multiple items, batch operations +2. `$input.first()` - Single item, API responses +3. `$input.item` - Each Item mode processing + +**Critical Rule**: +- Webhook data is under `.body` property + +**Best Practice**: +- Be explicit: Use `$input.first().json.field` instead of `$json.field` +- Always check for null/undefined +- Use appropriate method for your mode (All Items vs Each Item) + +**See Also**: +- [SKILL.md](SKILL.md) - Overview and quick start +- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - Production patterns +- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Avoid common mistakes diff --git a/skills/n8n-code-javascript/ERROR_PATTERNS.md b/skills/n8n-code-javascript/ERROR_PATTERNS.md new file mode 100644 index 0000000..d2ae482 --- /dev/null +++ b/skills/n8n-code-javascript/ERROR_PATTERNS.md @@ -0,0 +1,763 @@ +# Error Patterns - JavaScript Code Node + +Complete guide to avoiding the most common Code node errors. + +--- + +## Overview + +This guide covers the **top 5 error patterns** encountered in n8n Code nodes. Understanding and avoiding these errors will save you significant debugging time. + +**Error Frequency**: +1. Empty Code / Missing Return - **38% of failures** +2. Expression Syntax Confusion - **8% of failures** +3. Incorrect Return Wrapper - **5% of failures** +4. Unmatched Expression Brackets - **6% of failures** +5. Missing Null Checks - **Common runtime error** + +--- + +## Error #1: Empty Code or Missing Return Statement + +**Frequency**: Most common error (38% of all validation failures) + +**What Happens**: +- Workflow execution fails +- Next nodes receive no data +- Error: "Code cannot be empty" or "Code must return data" + +### The Problem + +```javascript +// ❌ ERROR: No code at all +// (Empty code field) +``` + +```javascript +// ❌ ERROR: Code executes but doesn't return anything +const items = $input.all(); + +// Process items +for (const item of items) { + console.log(item.json.name); +} + +// Forgot to return! +``` + +```javascript +// ❌ ERROR: Early return path exists, but not all paths return +const items = $input.all(); + +if (items.length === 0) { + return []; // βœ… This path returns +} + +// Process items +const processed = items.map(item => ({json: item.json})); + +// ❌ Forgot to return processed! +``` + +### The Solution + +```javascript +// βœ… CORRECT: Always return data +const items = $input.all(); + +// Process items +const processed = items.map(item => ({ + json: { + ...item.json, + processed: true + } +})); + +return processed; // βœ… Return statement present +``` + +```javascript +// βœ… CORRECT: Return empty array if no items +const items = $input.all(); + +if (items.length === 0) { + return []; // Valid: empty array when no data +} + +// Process and return +return items.map(item => ({json: item.json})); +``` + +```javascript +// βœ… CORRECT: All code paths return +const items = $input.all(); + +if (items.length === 0) { + return []; +} else if (items.length === 1) { + return [{json: {single: true, data: items[0].json}}]; +} else { + return items.map(item => ({json: item.json})); +} + +// All paths covered +``` + +### Checklist + +- [ ] Code field is not empty +- [ ] Return statement exists +- [ ] ALL code paths return data (if/else branches) +- [ ] Return format is correct (`[{json: {...}}]`) +- [ ] Return happens even on errors (use try-catch) + +--- + +## Error #2: Expression Syntax Confusion + +**Frequency**: 8% of validation failures + +**What Happens**: +- Syntax error in code execution +- Error: "Unexpected token" or "Expression syntax is not valid in Code nodes" +- Template variables not evaluated + +### The Problem + +n8n has TWO distinct syntaxes: +1. **Expression syntax** `{{ }}` - Used in OTHER nodes (Set, IF, HTTP Request) +2. **JavaScript** - Used in CODE nodes (no `{{ }}`) + +Many developers mistakenly use expression syntax inside Code nodes. + +```javascript +// ❌ WRONG: Using n8n expression syntax in Code node +const userName = "{{ $json.name }}"; +const userEmail = "{{ $json.body.email }}"; + +return [{ + json: { + name: userName, + email: userEmail + } +}]; + +// Result: Literal string "{{ $json.name }}", NOT the value! +``` + +```javascript +// ❌ WRONG: Trying to evaluate expressions +const value = "{{ $now.toFormat('yyyy-MM-dd') }}"; +``` + +### The Solution + +```javascript +// βœ… CORRECT: Use JavaScript directly (no {{ }}) +const userName = $json.name; +const userEmail = $json.body.email; + +return [{ + json: { + name: userName, + email: userEmail + } +}]; +``` + +```javascript +// βœ… CORRECT: JavaScript template literals (use backticks) +const message = `Hello, ${$json.name}! Your email is ${$json.email}`; + +return [{ + json: { + greeting: message + } +}]; +``` + +```javascript +// βœ… CORRECT: Direct variable access +const item = $input.first().json; + +return [{ + json: { + name: item.name, + email: item.email, + timestamp: new Date().toISOString() // JavaScript Date, not {{ }} + } +}]; +``` + +### Comparison Table + +| Context | Syntax | Example | +|---------|--------|---------| +| Set node | `{{ }}` expressions | `{{ $json.name }}` | +| IF node | `{{ }}` expressions | `{{ $json.age > 18 }}` | +| HTTP Request URL | `{{ }}` expressions | `{{ $json.userId }}` | +| **Code node** | **JavaScript** | `$json.name` | +| **Code node strings** | **Template literals** | `` `Hello ${$json.name}` `` | + +### Quick Fix Guide + +```javascript +// WRONG β†’ RIGHT conversions + +// ❌ "{{ $json.field }}" +// βœ… $json.field + +// ❌ "{{ $now }}" +// βœ… new Date().toISOString() + +// ❌ "{{ $node['HTTP Request'].json.data }}" +// βœ… $node["HTTP Request"].json.data + +// ❌ `{{ $json.firstName }} {{ $json.lastName }}` +// βœ… `${$json.firstName} ${$json.lastName}` +``` + +--- + +## Error #3: Incorrect Return Wrapper Format + +**Frequency**: 5% of validation failures + +**What Happens**: +- Error: "Return value must be an array of objects" +- Error: "Each item must have a json property" +- Next nodes receive malformed data + +### The Problem + +Code nodes MUST return: +- **Array** of objects +- Each object MUST have a **`json` property** + +```javascript +// ❌ WRONG: Returning object instead of array +return { + json: { + result: 'success' + } +}; +// Missing array wrapper [] +``` + +```javascript +// ❌ WRONG: Returning array without json wrapper +return [ + {id: 1, name: 'Alice'}, + {id: 2, name: 'Bob'} +]; +// Missing json property +``` + +```javascript +// ❌ WRONG: Returning plain value +return "processed"; +``` + +```javascript +// ❌ WRONG: Returning items without mapping +return $input.all(); +// Works if items already have json property, but not guaranteed +``` + +```javascript +// ❌ WRONG: Incomplete structure +return [{data: {result: 'success'}}]; +// Should be {json: {...}}, not {data: {...}} +``` + +### The Solution + +```javascript +// βœ… CORRECT: Single result +return [{ + json: { + result: 'success', + timestamp: new Date().toISOString() + } +}]; +``` + +```javascript +// βœ… CORRECT: Multiple results +return [ + {json: {id: 1, name: 'Alice'}}, + {json: {id: 2, name: 'Bob'}}, + {json: {id: 3, name: 'Carol'}} +]; +``` + +```javascript +// βœ… CORRECT: Transforming array +const items = $input.all(); + +return items.map(item => ({ + json: { + id: item.json.id, + name: item.json.name, + processed: true + } +})); +``` + +```javascript +// βœ… CORRECT: Empty result +return []; +// Valid when no data to return +``` + +```javascript +// βœ… CORRECT: Conditional returns +if (shouldProcess) { + return [{json: {result: 'processed'}}]; +} else { + return []; +} +``` + +### Return Format Checklist + +- [ ] Return value is an **array** `[...]` +- [ ] Each array element has **`json` property** +- [ ] Structure is `[{json: {...}}]` or `[{json: {...}}, {json: {...}}]` +- [ ] NOT `{json: {...}}` (missing array wrapper) +- [ ] NOT `[{...}]` (missing json property) + +### Common Scenarios + +```javascript +// Scenario 1: Single object from API +const response = $input.first().json; + +// βœ… CORRECT +return [{json: response}]; + +// ❌ WRONG +return {json: response}; + + +// Scenario 2: Array of objects +const users = $input.all(); + +// βœ… CORRECT +return users.map(user => ({json: user.json})); + +// ❌ WRONG +return users; // Risky - depends on existing structure + + +// Scenario 3: Computed result +const total = $input.all().reduce((sum, item) => sum + item.json.amount, 0); + +// βœ… CORRECT +return [{json: {total}}]; + +// ❌ WRONG +return {total}; + + +// Scenario 4: No results +// βœ… CORRECT +return []; + +// ❌ WRONG +return null; +``` + +--- + +## Error #4: Unmatched Expression Brackets + +**Frequency**: 6% of validation failures + +**What Happens**: +- Parsing error during save +- Error: "Unmatched expression brackets" +- Code appears correct but fails validation + +### The Problem + +This error typically occurs when: +1. Strings contain unbalanced quotes +2. Multi-line strings with special characters +3. Template literals with nested brackets + +```javascript +// ❌ WRONG: Unescaped quote in string +const message = "It's a nice day"; +// Single quote breaks string +``` + +```javascript +// ❌ WRONG: Unbalanced brackets in regex +const pattern = /\{(\w+)\}/; // JSON storage issue +``` + +```javascript +// ❌ WRONG: Multi-line string with quotes +const html = " +
+

Hello

+
+"; +// Quote balance issues +``` + +### The Solution + +```javascript +// βœ… CORRECT: Escape quotes +const message = "It\\'s a nice day"; +// Or use different quotes +const message = "It's a nice day"; // Double quotes work +``` + +```javascript +// βœ… CORRECT: Escape regex properly +const pattern = /\\{(\\w+)\\}/; +``` + +```javascript +// βœ… CORRECT: Template literals for multi-line +const html = ` +
+

Hello

+
+`; +// Backticks handle multi-line and quotes +``` + +```javascript +// βœ… CORRECT: Escape backslashes +const path = "C:\\\\Users\\\\Documents\\\\file.txt"; +``` + +### Escaping Guide + +| Character | Escape As | Example | +|-----------|-----------|---------| +| Single quote in single-quoted string | `\\'` | `'It\\'s working'` | +| Double quote in double-quoted string | `\\"` | `"She said \\"hello\\""` | +| Backslash | `\\\\` | `"C:\\\\path"` | +| Newline | `\\n` | `"Line 1\\nLine 2"` | +| Tab | `\\t` | `"Column1\\tColumn2"` | + +### Best Practices + +```javascript +// βœ… BEST: Use template literals for complex strings +const message = `User ${name} said: "Hello!"`; + +// βœ… BEST: Use template literals for HTML +const html = ` +
+

${title}

+

${content}

+
+`; + +// βœ… BEST: Use template literals for JSON +const jsonString = `{ + "name": "${name}", + "email": "${email}" +}`; +``` + +--- + +## Error #5: Missing Null Checks / Undefined Access + +**Frequency**: Very common runtime error + +**What Happens**: +- Workflow execution stops +- Error: "Cannot read property 'X' of undefined" +- Error: "Cannot read property 'X' of null" +- Crashes on missing data + +### The Problem + +```javascript +// ❌ WRONG: No null check - crashes if user doesn't exist +const email = item.json.user.email; +``` + +```javascript +// ❌ WRONG: Assumes array has items +const firstItem = $input.all()[0].json; +``` + +```javascript +// ❌ WRONG: Assumes nested property exists +const city = $json.address.city; +``` + +```javascript +// ❌ WRONG: No validation before array operations +const names = $json.users.map(user => user.name); +``` + +### The Solution + +```javascript +// βœ… CORRECT: Optional chaining +const email = item.json?.user?.email || 'no-email@example.com'; +``` + +```javascript +// βœ… CORRECT: Check array length +const items = $input.all(); + +if (items.length === 0) { + return []; +} + +const firstItem = items[0].json; +``` + +```javascript +// βœ… CORRECT: Guard clauses +const data = $input.first().json; + +if (!data.address) { + return [{json: {error: 'No address provided'}}]; +} + +const city = data.address.city; +``` + +```javascript +// βœ… CORRECT: Default values +const users = $json.users || []; +const names = users.map(user => user.name || 'Unknown'); +``` + +```javascript +// βœ… CORRECT: Try-catch for risky operations +try { + const email = item.json.user.email.toLowerCase(); + return [{json: {email}}]; +} catch (error) { + return [{ + json: { + error: 'Invalid user data', + details: error.message + } + }]; +} +``` + +### Safe Access Patterns + +```javascript +// Pattern 1: Optional chaining (modern, recommended) +const value = data?.nested?.property?.value; + +// Pattern 2: Logical OR with default +const value = data.property || 'default'; + +// Pattern 3: Ternary check +const value = data.property ? data.property : 'default'; + +// Pattern 4: Guard clause +if (!data.property) { + return []; +} +const value = data.property; + +// Pattern 5: Try-catch +try { + const value = data.nested.property.value; +} catch (error) { + const value = 'default'; +} +``` + +### Webhook Data Safety + +```javascript +// Webhook data requires extra safety + +// ❌ RISKY: Assumes all fields exist +const name = $json.body.user.name; +const email = $json.body.user.email; + +// βœ… SAFE: Check each level +const body = $json.body || {}; +const user = body.user || {}; +const name = user.name || 'Unknown'; +const email = user.email || 'no-email'; + +// βœ… BETTER: Optional chaining +const name = $json.body?.user?.name || 'Unknown'; +const email = $json.body?.user?.email || 'no-email'; +``` + +### Array Safety + +```javascript +// ❌ RISKY: No length check +const items = $input.all(); +const firstId = items[0].json.id; + +// βœ… SAFE: Check length +const items = $input.all(); + +if (items.length > 0) { + const firstId = items[0].json.id; +} else { + // Handle empty case + return []; +} + +// βœ… BETTER: Use $input.first() +const firstItem = $input.first(); +const firstId = firstItem.json.id; // Built-in safety +``` + +### Object Property Safety + +```javascript +// ❌ RISKY: Direct access +const config = $json.settings.advanced.timeout; + +// βœ… SAFE: Step by step with defaults +const settings = $json.settings || {}; +const advanced = settings.advanced || {}; +const timeout = advanced.timeout || 30000; + +// βœ… BETTER: Optional chaining +const timeout = $json.settings?.advanced?.timeout ?? 30000; +// Note: ?? (nullish coalescing) vs || (logical OR) +``` + +--- + +## Error Prevention Checklist + +Use this checklist before deploying Code nodes: + +### Code Structure +- [ ] Code field is not empty +- [ ] Return statement exists +- [ ] All code paths return data + +### Return Format +- [ ] Returns array: `[...]` +- [ ] Each item has `json` property: `{json: {...}}` +- [ ] Format is `[{json: {...}}]` + +### Syntax +- [ ] No `{{ }}` expression syntax (use JavaScript) +- [ ] Template literals use backticks: `` `${variable}` `` +- [ ] All quotes and brackets balanced +- [ ] Strings properly escaped + +### Data Safety +- [ ] Null checks for optional properties +- [ ] Array length checks before access +- [ ] Webhook data accessed via `.body` +- [ ] Try-catch for risky operations +- [ ] Default values for missing data + +### Testing +- [ ] Test with empty input +- [ ] Test with missing fields +- [ ] Test with unexpected data types +- [ ] Check browser console for errors + +--- + +## Quick Error Reference + +| Error Message | Likely Cause | Fix | +|---------------|--------------|-----| +| "Code cannot be empty" | Empty code field | Add meaningful code | +| "Code must return data" | Missing return statement | Add `return [...]` | +| "Return value must be an array" | Returning object instead of array | Wrap in `[...]` | +| "Each item must have json property" | Missing `json` wrapper | Use `{json: {...}}` | +| "Unexpected token" | Expression syntax `{{ }}` in code | Remove `{{ }}`, use JavaScript | +| "Cannot read property X of undefined" | Missing null check | Use optional chaining `?.` | +| "Cannot read property X of null" | Null value access | Add guard clause or default | +| "Unmatched expression brackets" | Quote/bracket imbalance | Check string escaping | + +--- + +## Debugging Tips + +### 1. Use console.log() + +```javascript +const items = $input.all(); +console.log('Items count:', items.length); +console.log('First item:', items[0]); + +// Check browser console (F12) for output +``` + +### 2. Return Intermediate Results + +```javascript +// Debug by returning current state +const items = $input.all(); +const processed = items.map(item => ({json: item.json})); + +// Return to see what you have +return processed; +``` + +### 3. Try-Catch for Troubleshooting + +```javascript +try { + // Your code here + const result = riskyOperation(); + return [{json: {result}}]; +} catch (error) { + // See what failed + return [{ + json: { + error: error.message, + stack: error.stack + } + }]; +} +``` + +### 4. Validate Input Structure + +```javascript +const items = $input.all(); + +// Check what you received +console.log('Input structure:', JSON.stringify(items[0], null, 2)); + +// Then process +``` + +--- + +## Summary + +**Top 5 Errors to Avoid**: +1. **Empty code / missing return** (38%) - Always return data +2. **Expression syntax `{{ }}`** (8%) - Use JavaScript, not expressions +3. **Wrong return format** (5%) - Always `[{json: {...}}]` +4. **Unmatched brackets** (6%) - Escape strings properly +5. **Missing null checks** - Use optional chaining `?.` + +**Quick Prevention**: +- Return `[{json: {...}}]` format +- Use JavaScript, NOT `{{ }}` expressions +- Check for null/undefined before accessing +- Test with empty and invalid data +- Use browser console for debugging + +**See Also**: +- [SKILL.md](SKILL.md) - Overview and best practices +- [DATA_ACCESS.md](DATA_ACCESS.md) - Safe data access patterns +- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - Working examples diff --git a/skills/n8n-code-javascript/README.md b/skills/n8n-code-javascript/README.md new file mode 100644 index 0000000..df3464f --- /dev/null +++ b/skills/n8n-code-javascript/README.md @@ -0,0 +1,350 @@ +# n8n Code JavaScript + +Expert guidance for writing JavaScript code in n8n Code nodes. + +--- + +## Purpose + +Teaches how to write effective JavaScript in n8n Code nodes, avoid common errors, and use built-in functions effectively. + +--- + +## Activates On + +**Trigger keywords**: +- "javascript code node" +- "write javascript in n8n" +- "code node javascript" +- "$input syntax" +- "$json syntax" +- "$helpers.httpRequest" +- "DateTime luxon" +- "code node error" +- "webhook data code" +- "return format code node" + +**Common scenarios**: +- Writing JavaScript code in Code nodes +- Troubleshooting Code node errors +- Making HTTP requests from code +- Working with dates and times +- Accessing webhook data +- Choosing between All Items and Each Item mode + +--- + +## What You'll Learn + +### Quick Start +- Mode selection (All Items vs Each Item) +- Data access patterns ($input.all(), $input.first(), $input.item) +- Correct return format: `[{json: {...}}]` +- Webhook data structure (.body nesting) +- Built-in functions overview + +### Data Access Mastery +- $input.all() - Batch operations (most common) +- $input.first() - Single item operations +- $input.item - Each Item mode processing +- $node - Reference other workflow nodes +- **Critical gotcha**: Webhook data under `.body` + +### Common Patterns (Production-Tested) +1. Multi-source Data Aggregation +2. Regex Filtering & Pattern Matching +3. Markdown Parsing & Structured Extraction +4. JSON Comparison & Validation +5. CRM Data Transformation +6. Release Information Processing +7. Array Transformation with Context +8. Slack Block Kit Formatting +9. Top N Filtering & Ranking +10. String Aggregation & Reporting + +### Error Prevention +Top 5 errors to avoid: +1. **Empty code / missing return** (38% of failures) +2. **Expression syntax confusion** (using `{{}}` in code) +3. **Incorrect return format** (missing array wrapper or json property) +4. **Unmatched brackets** (string escaping issues) +5. **Missing null checks** (crashes on undefined) + +### Built-in Functions +- **$helpers.httpRequest()** - Make HTTP requests +- **DateTime (Luxon)** - Advanced date/time operations +- **$jmespath()** - Query JSON structures +- **$getWorkflowStaticData()** - Persistent storage +- Standard JavaScript globals (Math, JSON, console) +- Available Node.js modules (crypto, Buffer, URL) + +--- + +## File Structure + +``` +n8n-code-javascript/ +β”œβ”€β”€ SKILL.md (500 lines) +β”‚ Overview, quick start, mode selection, best practices +β”‚ - Mode selection guide (All Items vs Each Item) +β”‚ - Data access patterns overview +β”‚ - Return format requirements +β”‚ - Critical webhook gotcha +β”‚ - Error prevention overview +β”‚ - Quick reference checklist +β”‚ +β”œβ”€β”€ DATA_ACCESS.md (400 lines) +β”‚ Complete data access patterns +β”‚ - $input.all() - Most common (26% usage) +β”‚ - $input.first() - Very common (25% usage) +β”‚ - $input.item - Each Item mode (19% usage) +β”‚ - $node - Reference other nodes +β”‚ - Webhook data structure (.body nesting) +β”‚ - Choosing the right pattern +β”‚ - Common mistakes to avoid +β”‚ +β”œβ”€β”€ COMMON_PATTERNS.md (600 lines) +β”‚ 10 production-tested patterns +β”‚ - Pattern 1: Multi-source Aggregation +β”‚ - Pattern 2: Regex Filtering +β”‚ - Pattern 3: Markdown Parsing +β”‚ - Pattern 4: JSON Comparison +β”‚ - Pattern 5: CRM Transformation +β”‚ - Pattern 6: Release Processing +β”‚ - Pattern 7: Array Transformation +β”‚ - Pattern 8: Slack Block Kit +β”‚ - Pattern 9: Top N Filtering +β”‚ - Pattern 10: String Aggregation +β”‚ - Pattern selection guide +β”‚ +β”œβ”€β”€ ERROR_PATTERNS.md (450 lines) +β”‚ Top 5 errors with solutions +β”‚ - Error #1: Empty Code / Missing Return (38%) +β”‚ - Error #2: Expression Syntax Confusion (8%) +β”‚ - Error #3: Incorrect Return Wrapper (5%) +β”‚ - Error #4: Unmatched Brackets (6%) +β”‚ - Error #5: Missing Null Checks +β”‚ - Error prevention checklist +β”‚ - Quick error reference +β”‚ - Debugging tips +β”‚ +β”œβ”€β”€ BUILTIN_FUNCTIONS.md (450 lines) +β”‚ Complete built-in function reference +β”‚ - $helpers.httpRequest() API reference +β”‚ - DateTime (Luxon) complete guide +β”‚ - $jmespath() JSON querying +β”‚ - $getWorkflowStaticData() persistent storage +β”‚ - Standard JavaScript globals +β”‚ - Available Node.js modules +β”‚ - What's NOT available +β”‚ +└── README.md (this file) + Skill metadata and overview +``` + +**Total**: ~2,400 lines across 6 files + +--- + +## Coverage + +### Mode Selection +- **Run Once for All Items** - Recommended for 95% of use cases +- **Run Once for Each Item** - Specialized cases only +- Decision guide and performance implications + +### Data Access +- Most common patterns with usage statistics +- Webhook data structure (critical .body gotcha) +- Safe access patterns with null checks +- When to use which pattern + +### Error Prevention +- Top 5 errors covering 62%+ of all failures +- Clear wrong vs right examples +- Error prevention checklist +- Debugging tips and console.log usage + +### Production Patterns +- 10 patterns from real workflows +- Complete working examples +- Use cases and key techniques +- Pattern selection guide + +### Built-in Functions +- Complete $helpers.httpRequest() reference +- DateTime/Luxon operations (formatting, parsing, arithmetic) +- $jmespath() for JSON queries +- Persistent storage with $getWorkflowStaticData() +- Standard JavaScript and Node.js modules + +--- + +## Critical Gotchas Highlighted + +### #1: Webhook Data Structure +**MOST COMMON MISTAKE**: Webhook data is under `.body` + +```javascript +// ❌ WRONG +const name = $json.name; + +// βœ… CORRECT +const name = $json.body.name; +``` + +### #2: Return Format +**CRITICAL**: Must return array with json property + +```javascript +// ❌ WRONG +return {json: {result: 'success'}}; + +// βœ… CORRECT +return [{json: {result: 'success'}}]; +``` + +### #3: Expression Syntax +**Don't use `{{}}` in Code nodes** + +```javascript +// ❌ WRONG +const value = "{{ $json.field }}"; + +// βœ… CORRECT +const value = $json.field; +``` + +--- + +## Integration with Other Skills + +### n8n Expression Syntax +- **Distinction**: Expressions use `{{}}` in OTHER nodes +- **Code nodes**: Use JavaScript directly (no `{{}}`) +- **When to use each**: Code vs expressions decision guide + +### n8n MCP Tools Expert +- Find Code node: `search_nodes({query: "code"})` +- Get configuration: `get_node_essentials("nodes-base.code")` +- Validate code: `validate_node_operation()` + +### n8n Node Configuration +- Mode selection (All Items vs Each Item) +- Language selection (JavaScript vs Python) +- Understanding property dependencies + +### n8n Workflow Patterns +- Code nodes in transformation step +- Webhook β†’ Code β†’ API pattern +- Error handling in workflows + +### n8n Validation Expert +- Validate Code node configuration +- Handle validation errors +- Auto-fix common issues + +--- + +## When to Use Code Node + +**Use Code node when:** +- βœ… Complex transformations requiring multiple steps +- βœ… Custom calculations or business logic +- βœ… Recursive operations +- βœ… API response parsing with complex structure +- βœ… Multi-step conditionals +- βœ… Data aggregation across items + +**Consider other nodes when:** +- ❌ Simple field mapping β†’ Use **Set** node +- ❌ Basic filtering β†’ Use **Filter** node +- ❌ Simple conditionals β†’ Use **IF** or **Switch** node +- ❌ HTTP requests only β†’ Use **HTTP Request** node + +**Code node excels at**: Complex logic that would require chaining many simple nodes + +--- + +## Success Metrics + +**Before this skill**: +- Users confused by mode selection +- Frequent return format errors +- Expression syntax mistakes +- Webhook data access failures +- Missing null check crashes + +**After this skill**: +- Clear mode selection guidance +- Understanding of return format +- JavaScript vs expression distinction +- Correct webhook data access +- Safe null-handling patterns +- Production-ready code patterns + +--- + +## Quick Reference + +### Essential Rules +1. Choose "All Items" mode (recommended) +2. Access data: `$input.all()`, `$input.first()`, `$input.item` +3. **MUST return**: `[{json: {...}}]` format +4. **Webhook data**: Under `.body` property +5. **No `{{}}` syntax**: Use JavaScript directly + +### Most Common Patterns +- Batch processing β†’ $input.all() + map/filter +- Single item β†’ $input.first() +- Aggregation β†’ reduce() +- HTTP requests β†’ $helpers.httpRequest() +- Date handling β†’ DateTime (Luxon) + +### Error Prevention +- Always return data +- Check for null/undefined +- Use try-catch for risky operations +- Test with empty input +- Use console.log() for debugging + +--- + +## Related Documentation + +- **n8n Code Node Guide**: https://docs.n8n.io/code/code-node/ +- **Built-in Methods Reference**: https://docs.n8n.io/code-examples/methods-variables-reference/ +- **Luxon Documentation**: https://moment.github.io/luxon/ + +--- + +## Evaluations + +**5 test scenarios** covering: +1. Webhook body gotcha (most common mistake) +2. Return format error (missing array wrapper) +3. HTTP request with $helpers.httpRequest() +4. Aggregation pattern with $input.all() +5. Expression syntax confusion (using `{{}}`) + +Each evaluation tests skill activation, correct guidance, and reference to appropriate documentation files. + +--- + +## Version History + +- **v1.0** (2025-01-20): Initial implementation + - SKILL.md with comprehensive overview + - DATA_ACCESS.md covering all access patterns + - COMMON_PATTERNS.md with 10 production patterns + - ERROR_PATTERNS.md covering top 5 errors + - BUILTIN_FUNCTIONS.md complete reference + - 5 evaluation scenarios + +--- + +## Author + +Conceived by Romuald CzΕ‚onkowski - [www.aiadvisors.pl/en](https://www.aiadvisors.pl/en) + +Part of the n8n-skills collection. diff --git a/skills/n8n-code-javascript/SKILL.md b/skills/n8n-code-javascript/SKILL.md new file mode 100644 index 0000000..449a7b6 --- /dev/null +++ b/skills/n8n-code-javascript/SKILL.md @@ -0,0 +1,699 @@ +--- +name: n8n-code-javascript +description: Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with $helpers, working with dates using DateTime, troubleshooting Code node errors, or choosing between Code node modes. +--- + +# JavaScript Code Node + +Expert guidance for writing JavaScript code in n8n Code nodes. + +--- + +## Quick Start + +```javascript +// Basic template for Code nodes +const items = $input.all(); + +// Process data +const processed = items.map(item => ({ + json: { + ...item.json, + processed: true, + timestamp: new Date().toISOString() + } +})); + +return processed; +``` + +### Essential Rules + +1. **Choose "Run Once for All Items" mode** (recommended for most use cases) +2. **Access data**: `$input.all()`, `$input.first()`, or `$input.item` +3. **CRITICAL**: Must return `[{json: {...}}]` format +4. **CRITICAL**: Webhook data is under `$json.body` (not `$json` directly) +5. **Built-ins available**: $helpers.httpRequest(), DateTime (Luxon), $jmespath() + +--- + +## Mode Selection Guide + +The Code node offers two execution modes. Choose based on your use case: + +### Run Once for All Items (Recommended - Default) + +**Use this mode for:** 95% of use cases + +- **How it works**: Code executes **once** regardless of input count +- **Data access**: `$input.all()` or `items` array +- **Best for**: Aggregation, filtering, batch processing, transformations, API calls with all data +- **Performance**: Faster for multiple items (single execution) + +```javascript +// Example: Calculate total from all items +const allItems = $input.all(); +const total = allItems.reduce((sum, item) => sum + (item.json.amount || 0), 0); + +return [{ + json: { + total, + count: allItems.length, + average: total / allItems.length + } +}]; +``` + +**When to use:** +- βœ… Comparing items across the dataset +- βœ… Calculating totals, averages, or statistics +- βœ… Sorting or ranking items +- βœ… Deduplication +- βœ… Building aggregated reports +- βœ… Combining data from multiple items + +### Run Once for Each Item + +**Use this mode for:** Specialized cases only + +- **How it works**: Code executes **separately** for each input item +- **Data access**: `$input.item` or `$item` +- **Best for**: Item-specific logic, independent operations, per-item validation +- **Performance**: Slower for large datasets (multiple executions) + +```javascript +// Example: Add processing timestamp to each item +const item = $input.item; + +return [{ + json: { + ...item.json, + processed: true, + processedAt: new Date().toISOString() + } +}]; +``` + +**When to use:** +- βœ… Each item needs independent API call +- βœ… Per-item validation with different error handling +- βœ… Item-specific transformations based on item properties +- βœ… When items must be processed separately for business logic + +**Decision Shortcut:** +- **Need to look at multiple items?** β†’ Use "All Items" mode +- **Each item completely independent?** β†’ Use "Each Item" mode +- **Not sure?** β†’ Use "All Items" mode (you can always loop inside) + +--- + +## Data Access Patterns + +### Pattern 1: $input.all() - Most Common + +**Use when**: Processing arrays, batch operations, aggregations + +```javascript +// Get all items from previous node +const allItems = $input.all(); + +// Filter, map, reduce as needed +const valid = allItems.filter(item => item.json.status === 'active'); +const mapped = valid.map(item => ({ + json: { + id: item.json.id, + name: item.json.name + } +})); + +return mapped; +``` + +### Pattern 2: $input.first() - Very Common + +**Use when**: Working with single objects, API responses, first-in-first-out + +```javascript +// Get first item only +const firstItem = $input.first(); +const data = firstItem.json; + +return [{ + json: { + result: processData(data), + processedAt: new Date().toISOString() + } +}]; +``` + +### Pattern 3: $input.item - Each Item Mode Only + +**Use when**: In "Run Once for Each Item" mode + +```javascript +// Current item in loop (Each Item mode only) +const currentItem = $input.item; + +return [{ + json: { + ...currentItem.json, + itemProcessed: true + } +}]; +``` + +### Pattern 4: $node - Reference Other Nodes + +**Use when**: Need data from specific nodes in workflow + +```javascript +// Get output from specific node +const webhookData = $node["Webhook"].json; +const httpData = $node["HTTP Request"].json; + +return [{ + json: { + combined: { + webhook: webhookData, + api: httpData + } + } +}]; +``` + +**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for comprehensive guide + +--- + +## Critical: Webhook Data Structure + +**MOST COMMON MISTAKE**: Webhook data is nested under `.body` + +```javascript +// ❌ WRONG - Will return undefined +const name = $json.name; +const email = $json.email; + +// βœ… CORRECT - Webhook data is under .body +const name = $json.body.name; +const email = $json.body.email; + +// Or with $input +const webhookData = $input.first().json.body; +const name = webhookData.name; +``` + +**Why**: Webhook node wraps all request data under `body` property. This includes POST data, query parameters, and JSON payloads. + +**See**: [DATA_ACCESS.md](DATA_ACCESS.md) for full webhook structure details + +--- + +## Return Format Requirements + +**CRITICAL RULE**: Always return array of objects with `json` property + +### Correct Return Formats + +```javascript +// βœ… Single result +return [{ + json: { + field1: value1, + field2: value2 + } +}]; + +// βœ… Multiple results +return [ + {json: {id: 1, data: 'first'}}, + {json: {id: 2, data: 'second'}} +]; + +// βœ… Transformed array +const transformed = $input.all() + .filter(item => item.json.valid) + .map(item => ({ + json: { + id: item.json.id, + processed: true + } + })); +return transformed; + +// βœ… Empty result (when no data to return) +return []; + +// βœ… Conditional return +if (shouldProcess) { + return [{json: processedData}]; +} else { + return []; +} +``` + +### Incorrect Return Formats + +```javascript +// ❌ WRONG: Object without array wrapper +return { + json: {field: value} +}; + +// ❌ WRONG: Array without json wrapper +return [{field: value}]; + +// ❌ WRONG: Plain string +return "processed"; + +// ❌ WRONG: Raw data without mapping +return $input.all(); // Missing .map() + +// ❌ WRONG: Incomplete structure +return [{data: value}]; // Should be {json: value} +``` + +**Why it matters**: Next nodes expect array format. Incorrect format causes workflow execution to fail. + +**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) #3 for detailed error solutions + +--- + +## Common Patterns Overview + +Based on production workflows, here are the most useful patterns: + +### 1. Multi-Source Data Aggregation +Combine data from multiple APIs, webhooks, or nodes + +```javascript +const allItems = $input.all(); +const results = []; + +for (const item of allItems) { + const sourceName = item.json.name || 'Unknown'; + // Parse source-specific structure + if (sourceName === 'API1' && item.json.data) { + results.push({ + json: { + title: item.json.data.title, + source: 'API1' + } + }); + } +} + +return results; +``` + +### 2. Filtering with Regex +Extract patterns, mentions, or keywords from text + +```javascript +const pattern = /\b([A-Z]{2,5})\b/g; +const matches = {}; + +for (const item of $input.all()) { + const text = item.json.text; + const found = text.match(pattern); + + if (found) { + found.forEach(match => { + matches[match] = (matches[match] || 0) + 1; + }); + } +} + +return [{json: {matches}}]; +``` + +### 3. Data Transformation & Enrichment +Map fields, normalize formats, add computed fields + +```javascript +const items = $input.all(); + +return items.map(item => { + const data = item.json; + const nameParts = data.name.split(' '); + + return { + json: { + first_name: nameParts[0], + last_name: nameParts.slice(1).join(' '), + email: data.email, + created_at: new Date().toISOString() + } + }; +}); +``` + +### 4. Top N Filtering & Ranking +Sort and limit results + +```javascript +const items = $input.all(); + +const topItems = items + .sort((a, b) => (b.json.score || 0) - (a.json.score || 0)) + .slice(0, 10); + +return topItems.map(item => ({json: item.json})); +``` + +### 5. Aggregation & Reporting +Sum, count, group data + +```javascript +const items = $input.all(); +const total = items.reduce((sum, item) => sum + (item.json.amount || 0), 0); + +return [{ + json: { + total, + count: items.length, + average: total / items.length, + timestamp: new Date().toISOString() + } +}]; +``` + +**See**: [COMMON_PATTERNS.md](COMMON_PATTERNS.md) for 10 detailed production patterns + +--- + +## Error Prevention - Top 5 Mistakes + +### #1: Empty Code or Missing Return (Most Common) + +```javascript +// ❌ WRONG: No return statement +const items = $input.all(); +// ... processing code ... +// Forgot to return! + +// βœ… CORRECT: Always return data +const items = $input.all(); +// ... processing ... +return items.map(item => ({json: item.json})); +``` + +### #2: Expression Syntax Confusion + +```javascript +// ❌ WRONG: Using n8n expression syntax in code +const value = "{{ $json.field }}"; + +// βœ… CORRECT: Use JavaScript template literals +const value = `${$json.field}`; + +// βœ… CORRECT: Direct access +const value = $input.first().json.field; +``` + +### #3: Incorrect Return Wrapper + +```javascript +// ❌ WRONG: Returning object instead of array +return {json: {result: 'success'}}; + +// βœ… CORRECT: Array wrapper required +return [{json: {result: 'success'}}]; +``` + +### #4: Missing Null Checks + +```javascript +// ❌ WRONG: Crashes if field doesn't exist +const value = item.json.user.email; + +// βœ… CORRECT: Safe access with optional chaining +const value = item.json?.user?.email || 'no-email@example.com'; + +// βœ… CORRECT: Guard clause +if (!item.json.user) { + return []; +} +const value = item.json.user.email; +``` + +### #5: Webhook Body Nesting + +```javascript +// ❌ WRONG: Direct access to webhook data +const email = $json.email; + +// βœ… CORRECT: Webhook data under .body +const email = $json.body.email; +``` + +**See**: [ERROR_PATTERNS.md](ERROR_PATTERNS.md) for comprehensive error guide + +--- + +## Built-in Functions & Helpers + +### $helpers.httpRequest() + +Make HTTP requests from within code: + +```javascript +const response = await $helpers.httpRequest({ + method: 'GET', + url: 'https://api.example.com/data', + headers: { + 'Authorization': 'Bearer token', + 'Content-Type': 'application/json' + } +}); + +return [{json: {data: response}}]; +``` + +### DateTime (Luxon) + +Date and time operations: + +```javascript +// Current time +const now = DateTime.now(); + +// Format dates +const formatted = now.toFormat('yyyy-MM-dd'); +const iso = now.toISO(); + +// Date arithmetic +const tomorrow = now.plus({days: 1}); +const lastWeek = now.minus({weeks: 1}); + +return [{ + json: { + today: formatted, + tomorrow: tomorrow.toFormat('yyyy-MM-dd') + } +}]; +``` + +### $jmespath() + +Query JSON structures: + +```javascript +const data = $input.first().json; + +// Filter array +const adults = $jmespath(data, 'users[?age >= `18`]'); + +// Extract fields +const names = $jmespath(data, 'users[*].name'); + +return [{json: {adults, names}}]; +``` + +**See**: [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) for complete reference + +--- + +## Best Practices + +### 1. Always Validate Input Data + +```javascript +const items = $input.all(); + +// Check if data exists +if (!items || items.length === 0) { + return []; +} + +// Validate structure +if (!items[0].json) { + return [{json: {error: 'Invalid input format'}}]; +} + +// Continue processing... +``` + +### 2. Use Try-Catch for Error Handling + +```javascript +try { + const response = await $helpers.httpRequest({ + url: 'https://api.example.com/data' + }); + + return [{json: {success: true, data: response}}]; +} catch (error) { + return [{ + json: { + success: false, + error: error.message + } + }]; +} +``` + +### 3. Prefer Array Methods Over Loops + +```javascript +// βœ… GOOD: Functional approach +const processed = $input.all() + .filter(item => item.json.valid) + .map(item => ({json: {id: item.json.id}})); + +// ❌ SLOWER: Manual loop +const processed = []; +for (const item of $input.all()) { + if (item.json.valid) { + processed.push({json: {id: item.json.id}}); + } +} +``` + +### 4. Filter Early, Process Late + +```javascript +// βœ… GOOD: Filter first to reduce processing +const processed = $input.all() + .filter(item => item.json.status === 'active') // Reduce dataset first + .map(item => expensiveTransformation(item)); // Then transform + +// ❌ WASTEFUL: Transform everything, then filter +const processed = $input.all() + .map(item => expensiveTransformation(item)) // Wastes CPU + .filter(item => item.json.status === 'active'); +``` + +### 5. Use Descriptive Variable Names + +```javascript +// βœ… GOOD: Clear intent +const activeUsers = $input.all().filter(item => item.json.active); +const totalRevenue = activeUsers.reduce((sum, user) => sum + user.json.revenue, 0); + +// ❌ BAD: Unclear purpose +const a = $input.all().filter(item => item.json.active); +const t = a.reduce((s, u) => s + u.json.revenue, 0); +``` + +### 6. Debug with console.log() + +```javascript +// Debug statements appear in browser console +const items = $input.all(); +console.log(`Processing ${items.length} items`); + +for (const item of items) { + console.log('Item data:', item.json); + // Process... +} + +return result; +``` + +--- + +## When to Use Code Node + +Use Code node when: +- βœ… Complex transformations requiring multiple steps +- βœ… Custom calculations or business logic +- βœ… Recursive operations +- βœ… API response parsing with complex structure +- βœ… Multi-step conditionals +- βœ… Data aggregation across items + +Consider other nodes when: +- ❌ Simple field mapping β†’ Use **Set** node +- ❌ Basic filtering β†’ Use **Filter** node +- ❌ Simple conditionals β†’ Use **IF** or **Switch** node +- ❌ HTTP requests only β†’ Use **HTTP Request** node + +**Code node excels at**: Complex logic that would require chaining many simple nodes + +--- + +## Integration with Other Skills + +### Works With: + +**n8n Expression Syntax**: +- Expressions use `{{ }}` syntax in other nodes +- Code nodes use JavaScript directly (no `{{ }}`) +- When to use expressions vs code + +**n8n MCP Tools Expert**: +- How to find Code node: `search_nodes({query: "code"})` +- Get configuration help: `get_node_essentials("nodes-base.code")` +- Validate code: `validate_node_operation()` + +**n8n Node Configuration**: +- Mode selection (All Items vs Each Item) +- Language selection (JavaScript vs Python) +- Understanding property dependencies + +**n8n Workflow Patterns**: +- Code nodes in transformation step +- Webhook β†’ Code β†’ API pattern +- Error handling in workflows + +**n8n Validation Expert**: +- Validate Code node configuration +- Handle validation errors +- Auto-fix common issues + +--- + +## Quick Reference Checklist + +Before deploying Code nodes, verify: + +- [ ] **Code is not empty** - Must have meaningful logic +- [ ] **Return statement exists** - Must return array of objects +- [ ] **Proper return format** - Each item: `{json: {...}}` +- [ ] **Data access correct** - Using `$input.all()`, `$input.first()`, or `$input.item` +- [ ] **No n8n expressions** - Use JavaScript template literals: `` `${value}` `` +- [ ] **Error handling** - Guard clauses for null/undefined inputs +- [ ] **Webhook data** - Access via `.body` if from webhook +- [ ] **Mode selection** - "All Items" for most cases +- [ ] **Performance** - Prefer map/filter over manual loops +- [ ] **Output consistent** - All code paths return same structure + +--- + +## Additional Resources + +### Related Files +- [DATA_ACCESS.md](DATA_ACCESS.md) - Comprehensive data access patterns +- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - 10 production-tested patterns +- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Top 5 errors and solutions +- [BUILTIN_FUNCTIONS.md](BUILTIN_FUNCTIONS.md) - Complete built-in reference + +### n8n Documentation +- Code Node Guide: https://docs.n8n.io/code/code-node/ +- Built-in Methods: https://docs.n8n.io/code-examples/methods-variables-reference/ +- Luxon Documentation: https://moment.github.io/luxon/ + +--- + +**Ready to write JavaScript in n8n Code nodes!** Start with simple transformations, use the error patterns guide to avoid common mistakes, and reference the pattern library for production-ready examples.