ust be serialized to JSON strings before Python can consume them, and vice versa.
Implementation Pattern:
// JavaScript Node: Serialize complex payload for downstream Python processing
const rawPayload = $input.getAll();
const formattedBatch = rawPayload.map(record => ({
transactionId: record.json.txn_ref,
amountCents: record.json.value,
currencyCode: record.json.currency
}));
return {
serialized_data: JSON.stringify(formattedBatch),
batch_size: formattedBatch.length
};
# Python Node: Deserialize and process
import json
raw_string = $input.all()[0]['json']['serialized_data']
transactions = json.loads(raw_string)
processed = []
for tx in transactions:
processed.append({
'id': tx['transactionId'],
'amount_usd': tx['amountCents'] / 100,
'category': 'verified'
})
return {'results': processed}
Architecture Rationale: Explicit serialization prevents implicit type coercion bugs. By structuring the payload as a flat array of primitives before crossing the language boundary, you eliminate nested object parsing overhead and ensure predictable deserialization in the target runtime.
Step 2: Iteration Control & Early Termination
n8n does not support native break statements in per-item execution contexts. Attempting to force early termination inside a loop requires architectural workarounds that align with n8n's data flow model.
Implementation Pattern: Accumulator with Threshold Exit
const sourceBatch = $input.getAll();
const threshold = 750;
let runningTotal = 0;
const approvedRecords = [];
for (const record of sourceBatch) {
const itemValue = record.json.order_total || 0;
runningTotal += itemValue;
approvedRecords.push({
...record.json,
cumulative_sum: runningTotal,
within_budget: runningTotal <= threshold
});
if (runningTotal > threshold) {
break; // Safe in runOnceForAllItems mode
}
}
return approvedRecords;
Architecture Rationale: Switching to runOnceForAllItems enables standard JavaScript control flow (for...of, break, continue). This approach processes data in a single execution context, avoids per-item function call overhead, and guarantees deterministic early exit. For even greater efficiency, push filtering logic upstream to the data source (SQL LIMIT, API pagination parameters) to eliminate unnecessary network and compute cycles.
Step 3: Resilient Execution & Fallback Routing
External API calls and third-party integrations require explicit error boundaries. Swallowing exceptions masks failures, while unhandled rejections crash the entire workflow branch.
Implementation Pattern: Graceful Degradation with Structured Fallbacks
const primaryEndpoint = 'https://api.vendor.com/v2/metrics';
const fallbackEndpoint = 'https://backup.vendor.com/v1/metrics';
async function fetchMetrics(targetUrl) {
const response = await fetch(targetUrl, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} from ${targetUrl}`);
}
return response.json();
}
try {
const primaryData = await fetchMetrics(primaryEndpoint);
return { source: 'primary', payload: primaryData };
} catch (primaryError) {
console.warn(`Primary endpoint failed: ${primaryError.message}`);
try {
const backupData = await fetchMetrics(fallbackEndpoint);
return { source: 'fallback', payload: backupData };
} catch (fallbackError) {
console.error(`Fallback also failed: ${fallbackError.message}`);
return { source: 'error', payload: null, details: fallbackError.message };
}
}
Architecture Rationale: This pattern isolates failure domains. The primary call attempts execution, and on failure, the catch block triggers a secondary attempt without halting the workflow. Returning a structured response object (source, payload, details) allows downstream nodes to branch logically based on success state. Note: For simple retry logic, prefer n8n's native node-level retry settings over in-code loops, as native retries are handled at the scheduler level and do not block worker threads.
The sandboxed V8 environment prohibits require() and npm module resolution. All HTTP requests must use the native fetch() API. When field names are determined at runtime, developers often reach for n8n.evaluateExpression(), but this introduces significant parsing overhead.
Implementation Pattern: Direct Access vs Dynamic Evaluation
// β Anti-pattern: Dynamic evaluation inside a loop
const dynamicKey = $input.item.json.config_field;
const value = n8n.evaluateExpression(`{{ $node["Source"]["json"]["${dynamicKey}"] }}`, $input.item);
// β
Production pattern: Direct property access with fallback
const sourceData = $input.item.json;
const dynamicKey = sourceData.config_field || 'default_metric';
const value = sourceData[dynamicKey] ?? null;
return { resolved_value: value };
Architecture Rationale: Direct bracket notation (obj[key]) executes in constant time with zero parsing overhead. evaluateExpression() compiles a template string, resolves node references, and evaluates the expression tree on every call. Reserve dynamic evaluation strictly for scenarios where the field path cannot be determined until runtime, and never invoke it inside iteration loops.
Pitfall Guide
| Pitfall Name | Explanation | Fix |
|---|
| Implicit State Leakage Across Languages | Assuming JavaScript objects flow directly into Python nodes without serialization. Python receives a stringified representation, causing TypeError when accessing properties. | Always serialize complex payloads with JSON.stringify() before crossing language boundaries. Deserialize explicitly in the target node. |
| Per-Item Execution for Aggregation | Using default per-item mode to calculate sums, averages, or groupings. This triggers separate function calls for each record, multiplying CPU overhead and preventing cross-item state sharing. | Switch to runOnceForAllItems mode. Use $input.getAll() to access the full dataset in a single execution context. |
Overusing evaluateExpression() | Calling dynamic expression resolution inside loops or for static field lookups. The template parser adds ~10x latency compared to direct property access. | Use direct bracket notation (item.json[key]) for runtime keys. Reserve evaluateExpression() only for complex cross-node path resolution that cannot be achieved natively. |
| Silent Error Swallowing | Catching exceptions and returning null or empty objects without logging or structuring the failure state. Downstream nodes receive ambiguous data, causing cascading logic errors. | Return structured error objects containing status, source, and message. Log warnings explicitly. Use downstream IF nodes to route failed items to error handlers. |
Assuming require() or npm Modules | Attempting to import third-party packages or Node.js built-ins. The sandbox blocks file system access and module resolution, throwing ReferenceError. | Use native fetch() for HTTP calls. For complex operations, pre-install packages in the n8n Docker environment or offload heavy computation to external services. |
| Ignoring Source-Level Pagination | Fetching entire datasets into the Code node and filtering downstream. This wastes bandwidth, increases memory pressure, and slows execution. | Apply LIMIT, OFFSET, or API query parameters at the source node. Push filtering logic upstream whenever possible. |
| Mixing Synchronous and Asynchronous Patterns | Awaiting promises inside a synchronous per-item execution context, or returning promises without async declaration. Causes unhandled rejections or stalled workflows. | Declare the function as async when using await. Ensure all promises resolve before returning. Use runOnceForAllItems for complex async orchestration. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple field mapping or renaming | Native SET node | Declarative, zero execution overhead, visual debugging | Negligible |
| Cross-item aggregation (sums, grouping) | Code node (runOnceForAllItems) | Shared memory space, single execution context, O(1) overhead | Low |
| Heavy numerical computation / ML scoring | Python Code node | Access to numpy/pandas, optimized C backends | Medium (+100ms cold start) |
| External API call with retry logic | Native HTTP Request node + Retry settings | Scheduler-level retries, non-blocking, built-in exponential backoff | Low |
| Dynamic field resolution at runtime | Direct bracket notation (obj[key]) | Constant-time execution, no template parsing | Negligible |
| Complex multi-step transformation | Code node (runOnceForAllItems) | Full JavaScript control flow, predictable state management | Medium |
Configuration Template
Copy this template into a new Code node configured for runOnceForAllItems mode. It demonstrates batch processing, threshold-based early exit, structured error handling, and performance-optimized field access.
const BATCH_THRESHOLD = 1000;
const sourceItems = $input.getAll();
const processedBatch = [];
const errorLog = [];
for (let i = 0; i < sourceItems.length; i++) {
const currentItem = sourceItems[i];
const rawData = currentItem.json;
try {
// Direct property access (no evaluateExpression)
const recordId = rawData.transaction_ref || `unknown_${i}`;
const amount = parseFloat(rawData.value) || 0;
const category = rawData.segment || 'uncategorized';
// Business logic validation
if (amount < 0) {
throw new Error(`Negative value detected for ${recordId}`);
}
processedBatch.push({
id: recordId,
normalized_amount: amount,
classification: category,
processed_at: new Date().toISOString()
});
// Early exit condition
if (processedBatch.length >= BATCH_THRESHOLD) {
console.log(`Batch threshold reached. Exiting early.`);
break;
}
} catch (err) {
errorLog.push({
index: i,
record_id: rawData.transaction_ref || 'N/A',
error: err.message
});
}
}
return {
successful_records: processedBatch,
failed_records: errorLog,
summary: {
total_attempted: sourceItems.length,
total_processed: processedBatch.length,
total_errors: errorLog.length
}
};
Quick Start Guide
- Create a new workflow and add a trigger node (Webhook, Schedule, or Cron).
- Add a Code node immediately after the trigger. In the node settings, switch execution mode to
Run Once For All Items.
- Paste the Configuration Template into the JavaScript editor. Replace
rawData.transaction_ref and rawData.value with field names matching your actual payload structure.
- Execute the workflow using the
Execute Node button. Inspect the output JSON to verify successful_records, failed_records, and summary metrics.
- Connect downstream nodes using the structured output. Route
successful_records to your destination service and pipe failed_records to an error handler or logging node.