Learn how to debug n8n workflows using built-in tools like the debugger, execution data viewer, breakpoints, Function nodes, logs, and error handling to efficiently identify and fix issues.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
To debug workflows in n8n, you can use built-in tools like the built-in debugger, execution data viewer, logs, and breakpoints. You can also inspect the data structure between nodes, use the "Function" node for custom debugging, test specific node outputs, and leverage console.log statements to identify and fix issues in your workflows.
Step 1: Understanding n8n's Built-in Debugging Tools
Before diving into specific debugging techniques, it's important to understand the debugging tools that n8n provides out of the box. These tools form the foundation of your debugging process:
Execution Data: n8n records the input and output data for each node in your workflow whenever the workflow is executed. This is available in the execution history.
Test Executions: You can run your workflow or individual nodes to test their behavior without triggering the entire workflow.
Console Output: n8n captures console logs from your workflow executions, which can be viewed in the execution details.
Breakpoints: You can set breakpoints to pause workflow execution at specific nodes for inspection.
Step 2: Using the Execution Data Viewer
The Execution Data Viewer is one of the most powerful debugging tools in n8n:
This feature is invaluable for understanding how data is transformed as it flows through your workflow.
Step 3: Testing Individual Nodes
To isolate and debug specific parts of your workflow:
This approach helps you identify which specific node might be causing issues in your workflow.
Step 4: Using the Function Node for Debugging
The Function node is extremely useful for debugging because it allows you to run custom JavaScript code:
// Log the entire input items
console.log(JSON.stringify(items, null, 2));
// Log specific properties
console.log('User ID:', items[0].json.userId);
// Return the items unmodified for the next node
return items;
You can also use the Function node to inspect and modify data:
// Add a debugging property to see what's happening
for (const item of items) {
item.json.\_debug = {
timestamp: new Date().toISOString(),
itemType: typeof item.json.data,
dataLength: item.json.data ? item.json.data.length : 0
};
}
return items;
Step 5: Using Breakpoints
Breakpoints allow you to pause execution and inspect data at specific points:
Breakpoints are particularly useful for complex workflows where you need to understand the data flow at specific critical points.
Step 6: Debugging Error Handling
For workflows with error handling branches:
// Log detailed error information
console.log('Error occurred:');
console.log('Error message:', items[0].json.error);
console.log('Error stack:', items[0].json.stack);
console.log('Node where error occurred:', items[0].json.node);
return items;
Step 7: Inspecting HTTP Requests and Responses
When debugging HTTP Request nodes:
// Log the status code and headers
console.log('Status Code:', items[0].json.statusCode);
console.log('Headers:', JSON.stringify(items[0].json.headers, null, 2));
// If there's an error, log more details
if (items[0].json.statusCode >= 400) {
console.log('Error Response Body:', items[0].json.body);
}
return items;
Step 8: Using the n8n Logs
n8n produces logs that can be helpful for debugging:
docker logs [container_name]
N8N_LOG_LEVEL=debug
before starting n8nprocess.env.N8N_LOG_LEVEL = 'debug';
to a Function node
Step 9: Troubleshooting Data Type Issues
Data type issues are common in n8n. To debug them:
// Check data types
for (const [index, item] of items.entries()) {
console.log(`Item ${index} types:`);
for (const key in item.json) {
console.log(` ${key}: ${typeof item.json[key]}`);
// For objects and arrays, log additional info
if (typeof item.json[key] === 'object' && item.json[key] !== null) {
console.log(` isArray: ${Array.isArray(item.json[key])}`);
console.log(` length: ${Array.isArray(item.json[key]) ? item.json[key].length : Object.keys(item.json[key]).length}`);
}
}
}
return items;
Look for cases where a number is stored as a string, or vice versa, which can cause issues with comparisons or calculations.
Use type conversion in Function nodes to fix data type issues:
// Convert string to number
items[0].json.amount = Number(items[0].json.amount);
// Convert number to string
items[0].json.id = String(items[0].json.id);
// Ensure boolean type
items[0].json.isActive = Boolean(items[0].json.isActive);
return items;
Step 10: Debugging Expressions
n8n expressions (code in {{ }}) can be tricky to debug:
{{ $json.items[0].data.user.profile.name.split(' ')[0] }}
{{ $json.items[0].data.user.profile.name }}
{{ $item(0).$json.name.split(' ') }}
{{ $item(0).$json.nameParts[0] }}
// Test the same logic as your expression
const item = items[0].json;
const userData = item.data.user.profile;
console.log('User data:', userData);
const fullName = userData.name;
console.log('Full name:', fullName);
const nameParts = fullName.split(' ');
console.log('Name parts:', nameParts);
const firstName = nameParts[0];
console.log('First name:', firstName);
// Add the result to the item
item.firstName = firstName;
return items;
Step 11: Handling JSON Parsing Issues
JSON parsing errors are common when working with APIs. To debug these:
// Safe JSON parsing with error logging
function safeJsonParse(str) {
try {
return JSON.parse(str);
} catch (error) {
console.log('JSON Parse Error:', error.message);
console.log('String that failed to parse:', str);
return null;
}
}
// Try to parse the JSON string
const jsonData = items[0].json.responseData;
items[0].json.parsedData = safeJsonParse(jsonData);
return items;
// Log the string with visible whitespace and special characters
console.log('JSON String representation:');
console.log(JSON.stringify(items[0].json.jsonString));
// Log the string character by character
const str = items[0].json.jsonString;
console.log('Character by character analysis:');
for (let i = 0; i < Math.min(str.length, 100); i++) {
console.log(`Position ${i}: "${str[i]}" (char code: ${str.charCodeAt(i)})`);
}
return items;
Step 12: Debugging Workflow Timing Issues
For workflows where timing matters:
// Add a timestamp to track execution time
if (!items[0].json.\_timestamps) {
items[0].json.\_timestamps = {};
}
const currentStep = 'afterApiCall';
items[0].json.\_timestamps[currentStep] = new Date().toISOString();
// Calculate time elapsed since workflow start
if (items[0].json.\_timestamps.workflowStart) {
const start = new Date(items[0].json.\_timestamps.workflowStart);
const now = new Date();
const elapsedMs = now - start;
console.log(`Time elapsed since workflow start: ${elapsedMs}ms`);
}
return items;
// Initialize timestamp tracking at workflow start
for (const item of items) {
if (!item.json.\_timestamps) {
item.json.\_timestamps = {};
}
item.json.\_timestamps.workflowStart = new Date().toISOString();
}
return items;
// Summarize timing information
const timestamps = items[0].json.\_timestamps;
console.log('Workflow timing summary:');
let previousTime = new Date(timestamps.workflowStart);
for (const [step, timeStr] of Object.entries(timestamps)) {
if (step === 'workflowStart') continue;
const currentTime = new Date(timeStr);
const elapsedMs = currentTime - previousTime;
console.log(`Step "${step}": ${elapsedMs}ms`);
previousTime = currentTime;
}
const totalTime = new Date() - new Date(timestamps.workflowStart);
console.log(`Total workflow execution time: ${totalTime}ms`);
return items;
Step 13: Debugging Parallel Executions
For workflows using the Split In Batches node or parallel operations:
// Add branch ID for tracking parallel execution
const branchId = `branch-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
for (const item of items) {
item.json.\_branchId = branchId;
}
console.log(`Starting execution of branch: ${branchId}`);
return items;
// Log completion of this branch
console.log(`Completed execution of branch: ${items[0].json._branchId}`);
return items;
// Summarize data from all branches
const branchIds = new Set();
for (const item of items) {
branchIds.add(item.json.\_branchId);
}
console.log(`Merged data from ${branchIds.size} branches`);
console.log('Branch IDs:', Array.from(branchIds));
return items;
Step 14: Creating a Dedicated Debug Workflow
For complex debugging scenarios, create a dedicated debug workflow:
// Set up test data for the target workflow
items = [
{
json: {
testCase: 'Case 1: Basic operation',
input: {
userId: 12345,
action: 'update',
data: { name: 'Test User', email: '[email protected]' }
}
}
},
{
json: {
testCase: 'Case 2: Edge case with missing data',
input: {
userId: 67890,
action: 'update',
data: { name: 'Another User' } // missing email
}
}
}
];
return items;
// Analyze results from target workflow
console.log('Debug Results:');
for (const [index, item] of items.entries()) {
console.log(`\nTest Case: ${item.json.testCase}`);
console.log('Input:', item.json.input);
console.log('Output:', item.json.output);
// Add specific test validations
if (item.json.testCase.includes('Basic operation')) {
console.log('Validation:', item.json.output.success ? 'PASSED' : 'FAILED');
} else if (item.json.testCase.includes('Edge case')) {
console.log('Validation:', item.json.output.error ? 'PASSED (expected error)' : 'FAILED (missing expected error)');
}
}
return items;
Step 15: Using External Tools for Debugging
Leverage external tools to enhance your debugging capabilities:
// Use a service like RequestBin, webhook.site, or Pipedream
const axios = require('axios');
// Send debugging information to a webhook service
await axios.post('https://your-debug-webhook-url.com', {
workflowId: $workflow.id,
timestamp: new Date().toISOString(),
nodeData: items[0].json,
environment: $env.ENVIRONMENT || 'unknown'
});
return items;
// Log to a service like Loggly, Papertrail, or your own logging API
const axios = require('axios');
async function logToExternalService(level, message, data) {
try {
await axios.post('https://your-logging-api.com/log', {
level,
message,
data,
source: 'n8n-workflow',
workflow: $workflow.id,
timestamp: new Date().toISOString()
});
} catch (error) {
console.log('Failed to send log to external service:', error.message);
}
}
// Usage
await logToExternalService('debug', 'Processing item', items[0].json);
return items;
Step 16: Debugging Credential Issues
Credential problems are common in n8n workflows:
// Use the same auth parameters as your problematic node
const options = {
url: 'https://api.example.com/auth/test',
method: 'GET',
headers: {
'Authorization': 'Bearer ' + items[0].json.apiKey
}
};
try {
const response = await $http.request(options);
console.log('Auth test response:', response);
items[0].json.authTestResult = {
success: true,
statusCode: response.statusCode,
data: response.data
};
} catch (error) {
console.log('Auth test error:', error.message);
items[0].json.authTestResult = {
success: false,
error: error.message,
statusCode: error.response ? error.response.statusCode : null
};
}
return items;
// Check if the OAuth token might be expired
function checkTokenExpiration(token) {
try {
// JWT tokens are in three parts separated by dots
const parts = token.split('.');
if (parts.length !== 3) return 'Not a JWT token';
// Decode the middle part (payload)
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
console.log('Token payload:', payload);
// Check expiration
if (payload.exp) {
const expirationDate = new Date(payload.exp \* 1000);
const now = new Date();
const timeUntilExpiration = expirationDate - now;
return {
expiresAt: expirationDate.toISOString(),
expired: timeUntilExpiration <= 0,
timeRemaining: `${Math.round(timeUntilExpiration / 1000 / 60)} minutes`
};
}
return 'No expiration found in token';
} catch (error) {
return `Error analyzing token: ${error.message}`;
}
}
// Use the function
const tokenInfo = checkTokenExpiration(items[0].json.accessToken);
console.log('Token information:', tokenInfo);
return items;
Step 17: Debugging Data Mapping Issues
Data mapping problems can be difficult to track down:
// Log the structure of the data
function describeStructure(data, path = '') {
if (data === null) return { type: 'null', path };
if (data === undefined) return { type: 'undefined', path };
const type = typeof data;
if (type === 'object') {
if (Array.isArray(data)) {
return {
type: 'array',
path,
length: data.length,
sample: data.length > 0 ? describeStructure(data[0], `${path}[0]`) : null
};
} else {
const keys = Object.keys(data);
return {
type: 'object',
path,
keys,
keyCount: keys.length,
keyDetails: keys.map(key => ({
key,
valueType: describeStructure(data[key], `${path}.${key}`)
}))
};
}
}
return { type, path, value: type === 'string' ? `"${data}"` : data };
}
// Log the structure of the first item
console.log('Data structure:');
console.log(JSON.stringify(describeStructure(items[0].json), null, 2));
return items;
// Create a modified copy of data to track transformations
function createTrackedCopy(data, prefix = '_original_') {
if (data === null || data === undefined || typeof data !== 'object') {
return data;
}
if (Array.isArray(data)) {
return data.map(item => createTrackedCopy(item, prefix));
}
const result = {};
for (const [key, value] of Object.entries(data)) {
// Store original values with a prefix
result[`${prefix}${key}`] = value;
// Copy the value (potentially recursively for objects)
result[key] = createTrackedCopy(value, prefix);
}
return result;
}
// Create tracked copies of all items
for (let i = 0; i < items.length; i++) {
items[i].json = createTrackedCopy(items[i].json);
}
return items;
// Compare original and current values
function compareWithOriginal(data, prefix = '_original_') {
if (data === null || data === undefined || typeof data !== 'object') {
return [];
}
let differences = [];
for (const [key, value] of Object.entries(data)) {
// Skip the keys that store original values
if (key.startsWith(prefix)) continue;
const originalKey = `${prefix}${key}`;
if (originalKey in data) {
const originalValue = data[originalKey];
// Compare current and original values
if (JSON.stringify(value) !== JSON.stringify(originalValue)) {
differences.push({
key,
original: originalValue,
current: value
});
}
}
// Recursively check nested objects
if (value !== null && typeof value === 'object') {
differences = differences.concat(
compareWithOriginal(value, prefix).map(diff => ({
...diff,
key: `${key}.${diff.key}`
}))
);
}
}
return differences;
}
// Log differences for the first item
const differences = compareWithOriginal(items[0].json);
console.log('Data transformations:');
console.log(JSON.stringify(differences, null, 2));
return items;
Step 18: Debugging Conditional Logic
Debugging complex conditional logic in IF nodes:
// Test the same conditions as the IF node
const item = items[0].json;
// Condition 1: User is active
const condition1 = Boolean(item.user.isActive);
console.log('Condition 1 (User is active):', condition1);
console.log(' Value being tested:', item.user.isActive);
console.log(' Type:', typeof item.user.isActive);
// Condition 2: Has required permissions
const condition2 = Array.isArray(item.user.permissions) &&
item.user.permissions.includes('admin');
console.log('Condition 2 (Has admin permission):', condition2);
console.log(' Permissions:', item.user.permissions);
console.log(' Is Array:', Array.isArray(item.user.permissions));
if (Array.isArray(item.user.permissions)) {
console.log(' Includes "admin":', item.user.permissions.includes('admin'));
}
// Combined condition (what the IF node would evaluate)
const combinedCondition = condition1 && condition2;
console.log('Combined condition (should match IF node):', combinedCondition);
// Add the results to the item for reference
item.\_conditionResults = {
condition1,
condition2,
combined: combinedCondition
};
return items;
// Add a branch identifier to track execution path
for (const item of items) {
item.json.\_executionPath = 'true-branch'; // or 'false-branch'
}
return items;
Step 19: Debugging with Temporary Storage
Use n8n's binary data capabilities for temporary storage of debug information:
// Store debug information as a binary file
function createDebugData() {
const debugData = {
timestamp: new Date().toISOString(),
workflowId: $workflow.id,
executionId: $execution.id,
items: JSON.parse(JSON.stringify(items)), // Deep copy items
environmentInfo: {
nodeVersion: process.version,
platform: process.platform,
n8nVersion: process.env.N8N\_VERSION || 'unknown'
}
};
// Convert to JSON string with formatting
const debugJSON = JSON.stringify(debugData, null, 2);
// Create binary property
const buffer = Buffer.from(debugJSON);
const binaryProperty = {
data: buffer.toString('base64'),
mimeType: 'application/json',
fileName: `debug-${Date.now()}.json`
};
return binaryProperty;
}
// Add binary debug data to the first item
if (!items[0].binary) items[0].binary = {};
items[0].binary.debugData = createDebugData();
return items;
Step 20: Implementing Comprehensive Error Handling
Robust error handling is essential for effective debugging:
// Comprehensive error handling
try {
// Your original function code here
const result = processData(items[0].json.data);
items[0].json.result = result;
} catch (error) {
console.log('Error in data processing:');
console.log('Error message:', error.message);
console.log('Error stack:', error.stack);
// Add error details to the item
items[0].json.error = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
data: items[0].json.data // Include the data that caused the error
};
// Optionally, continue execution rather than failing the workflow
items[0].json.result = null;
items[0].json.errorOccurred = true;
}
return items;
// Reusable error logging function
function logError(error, context = {}) {
const errorDetails = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
workflowId: $workflow.id,
executionId: $execution.id,
context
};
console.log('ERROR DETAILS:');
console.log(JSON.stringify(errorDetails, null, 2));
return errorDetails;
}
// Usage example
try {
// Your code here
} catch (error) {
const errorDetails = logError(error, {
operation: 'data transformation',
inputData: items[0].json.data
});
items[0].json.error = errorDetails;
}
return items;
// Retry logic for external API calls
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
console.log(`API request attempt ${attempt}/${maxRetries}`);
const response = await $http.request({
url,
...options
});
console.log(`Request succeeded on attempt ${attempt}`);
return response;
} catch (error) {
lastError = error;
const isTransientError = error.response &&
(error.response.statusCode === 429 ||
error.response.statusCode >= 500);
if (!isTransientError || attempt === maxRetries) {
console.log(`Request failed on attempt ${attempt} with non-transient error or max retries reached`);
break;
}
// Calculate backoff time (exponential backoff with jitter)
const baseDelay = 1000; // 1 second
const maxDelay = 10000; // 10 seconds
const exponentialDelay = Math.min(baseDelay \* Math.pow(2, attempt - 1), maxDelay);
const jitter = Math.random() _ 0.3 _ exponentialDelay; // 0-30% jitter
const delay = exponentialDelay + jitter;
console.log(`Transient error detected, retrying in ${Math.round(delay/1000)} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Usage example
try {
const response = await fetchWithRetry('https://api.example.com/data', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + items[0].json.token
}
});
items[0].json.apiResponse = response.data;
} catch (error) {
logError(error, { operation: 'API request' });
items[0].json.error = {
message: error.message,
statusCode: error.response ? error.response.statusCode : null
};
}
return items;
This comprehensive guide covers the most important aspects of debugging n8n workflows. By combining these techniques, you can efficiently identify and fix issues in your workflows, ensuring they run reliably in production environments.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.