n8n Data Transformation: How to Reshape Any Data Structure
Your workflow just crashed because the API returned an array when you expected an object. Or maybe a single item arrived when you needed multiple. Perhaps the field names use snake_case but your destination requires camelCase. These data structure mismatches cause most n8n workflow failures.
Data transformation sits at the heart of every useful automation. You pull data from one system, reshape it to match another system’s requirements, and push it forward. This sounds simple until you realize that every API speaks its own dialect of JSON, and n8n has specific structural requirements of its own.
The Data Structure Problem
APIs don’t care about your workflow. Shopify returns orders with nested line item arrays. HubSpot expects flat contact records. Google Sheets outputs rows as individual objects. Stripe sends webhook payloads wrapped in event objects. None of these match each other, and none arrive in exactly the format your next node expects.
n8n requires a specific data structure: an array of objects, each containing a json property. When incoming data doesn’t match this format, you get cryptic errors like “Cannot read properties of undefined” or silent failures where data simply disappears between nodes.
Most n8n users spend more time wrestling with data transformation than any other aspect of workflow building. The n8n community forums overflow with questions about restructuring JSON, splitting arrays, and handling nested objects.
What You’ll Learn
This guide covers everything you need to transform data confidently in n8n:
- n8n’s data structure requirements and why they matter
- Built-in transformation nodes (Set, Split Out, Aggregate, Merge) and when to use each
- 80+ data transformation functions built into n8n expressions
- Code node patterns for complex transformations that visual nodes can’t handle
- Real-world scenarios from API integrations, e-commerce, and data cleanup
- Troubleshooting techniques for common transformation errors
By the end, you’ll know exactly which tool to reach for when data arrives in the wrong shape.
Understanding n8n’s Data Structure
Before transforming data, you need to understand what n8n expects. This foundation prevents 90% of transformation headaches.
Items, JSON, and the Required Format
n8n workflows process items. Each item is an object containing at minimum a json property with your actual data. When you see output from any node, you’re looking at an array of items.
The required format:
[
{
json: {
name: "John",
email: "john@example.com"
}
},
{
json: {
name: "Jane",
email: "jane@example.com"
}
}
]
Notice the outer array, the individual objects, and the json wrapper around each record. This structure isn’t optional. Nodes that produce data in other formats cause downstream failures.
How Data Flows Between Nodes
Each node receives items from the previous node, processes them, and outputs new items to the next node. The transformation happens at each step.
| Input | Node | Output |
|---|---|---|
| 1 item with array | Split Out | Multiple items (one per array element) |
| Multiple items | Aggregate | 1 item with combined data |
| Multiple items | Set | Same number of items, modified fields |
| 2 input streams | Merge | Combined items based on mode |
Understanding this flow helps you choose the right transformation approach.
Common Structure Mistakes
Mistake 1: Returning raw data from Code nodes
// Wrong - returns raw object
return { name: "John" };
// Correct - returns array of items
return [{ json: { name: "John" } }];
Mistake 2: Accessing arrays without splitting
If a field contains an array, you can’t directly use each element in subsequent nodes. You need to split the array into separate items first using the Split Out node.
Mistake 3: Ignoring nested structures
{{ $json.user.address.city }} fails silently if user or address is undefined. Use optional chaining: {{ $json.user?.address?.city }}.
For expression-specific patterns, see our complete n8n expressions cheatsheet.
Built-in Transformation Nodes
n8n provides visual nodes for common transformation tasks. These should be your first choice before reaching for the Code node.
The Set Node (Edit Fields)
The Set node (also called Edit Fields) is the workhorse of data transformation. Use it to:
- Add new fields with static or dynamic values
- Rename existing fields
- Remove unwanted fields
- Transform values using expressions
Adding a calculated field:
Configure the Set node with a new field called fullName and expression {{ $json.firstName }} {{ $json.lastName }}.
Renaming fields:
Switch to “Manual Mapping” mode. Map first_name to firstName, last_name to lastName, and so on.
Removing fields:
Enable “Only Keep Fields Set in This Node” to output only the fields you explicitly define.
Tip: The Set node handles most simple transformations. Before using the Code node, ask yourself if Set node expressions can accomplish the same result.
Split Out Node
The Split Out node converts a single item containing an array into multiple separate items. This is essential when APIs return list data.
When to use:
- An API returns
{ orders: [{...}, {...}, {...}] }and you need to process each order - A webhook payload contains a
line_itemsarray - Any time you have one item with an array field that needs individual processing
Configuration:
- Set “Field to Split Out” to the array field name (e.g.,
ordersorline_items) - Optionally include parent fields in each output item
Example input:
{
"orderId": "12345",
"items": [
{ "product": "Widget", "qty": 2 },
{ "product": "Gadget", "qty": 1 }
]
}
After Split Out on items:
[
{ "product": "Widget", "qty": 2, "orderId": "12345" },
{ "product": "Gadget", "qty": 1, "orderId": "12345" }
]
Each array element becomes a separate item, optionally carrying parent field values.
Aggregate Node
The Aggregate node does the opposite of Split Out. It takes multiple items and combines them into fewer items (typically one).
When to use:
- You processed items individually and need to recombine them
- Building a summary from multiple records
- Creating a payload that requires an array of objects
Aggregation modes:
| Mode | What It Does | Result |
|---|---|---|
| Aggregate Individual Fields | Collects values from a specific field across all items | Single item with array field |
| Aggregate All Item Data | Combines all items into an array | Single item with all data as array |
Example: After processing order line items separately, use Aggregate to reconstruct the order with the modified items:
// Input: 3 separate items (one per line item)
// Output: 1 item with { lineItems: [...all three items...] }
Other Transformation Nodes
Merge Node
Combines data from two input connections. Multiple modes handle different scenarios:
- Append: Add items from both inputs together
- Combine: Match items by position or field value
- Multiplex: Create all possible combinations
Use Merge when you need to enrich data from one source with data from another.
Sort Node
Reorders items based on field values. Supports ascending, descending, and custom sort expressions.
Limit Node
Restricts output to a maximum number of items. Useful for testing with large datasets or implementing pagination.
Compare Datasets Node
Identifies differences between two data streams. Outputs items that are new, updated, unchanged, or removed.
Filter Node vs IF Node
- Filter: Removes items that don’t match criteria (reduce item count)
- IF: Routes ALL items to different branches based on criteria (item count unchanged)
For workflow architecture guidance, check our n8n workflow best practices guide.
Data Transformation Functions
n8n includes over 80 built-in functions for transforming data within expressions. These work anywhere you use the expression editor.
String Transformation Functions
| Function | Description | Example |
|---|---|---|
.extractEmail() | Extract email from text | {{ "Contact me at john@test.com".extractEmail() }} |
.extractUrl() | Extract URL from text | {{ "Visit https://example.com today".extractUrl() }} |
.extractDomain() | Get domain from URL | {{ "https://sub.example.com/path".extractDomain() }} |
.isEmail() | Check if valid email | {{ "test@example.com".isEmail() }} returns true |
.isEmpty() | Check if empty/null | {{ "".isEmpty() }} returns true |
.base64Encode() | Encode as base64 | {{ "hello".base64Encode() }} |
.base64Decode() | Decode from base64 | {{ "aGVsbG8=".base64Decode() }} |
.toTitleCase() | Title Case Text | {{ "hello world".toTitleCase() }} returns Hello World |
.toSnakeCase() | snake_case_text | {{ "Hello World".toSnakeCase() }} returns hello_world |
.hash('sha256') | Generate hash | {{ "text".hash("sha256") }} |
Practical example - clean and validate email:
{{ $json.userInput.trim().toLowerCase().isEmail() ? $json.userInput.trim().toLowerCase() : null }}
Array Transformation Functions
| Function | Description | Example |
|---|---|---|
.removeDuplicates() | Remove duplicate values | {{ $json.tags.removeDuplicates() }} |
.removeDuplicates('email') | Remove duplicates by key | {{ $json.users.removeDuplicates("email") }} |
.pluck('name') | Extract single property | {{ $json.users.pluck("name") }} returns array of names |
.sort() | Sort array | {{ $json.numbers.sort() }} |
.sort('lastName') | Sort by property | {{ $json.users.sort("lastName") }} |
.first() | Get first element | {{ $json.items.first() }} |
.last() | Get last element | {{ $json.items.last() }} |
.unique() | Get unique values | {{ $json.categories.unique() }} |
Practical example - extract all email domains:
{{ $json.contacts.pluck("email").map(e => e.extractDomain()) }}
Object Transformation Functions
| Function | Description | Example |
|---|---|---|
.isEmpty() | Check if object is empty | {{ $json.metadata.isEmpty() }} |
.keys() | Get object keys | {{ Object.keys($json.data) }} |
.values() | Get object values | {{ Object.values($json.data) }} |
.merge(obj) | Merge two objects | {{ $json.defaults.merge($json.overrides) }} |
Using JMESPath for Complex Queries
When standard expressions become unwieldy, JMESPath provides powerful JSON querying.
Basic syntax:
{{ $jmespath($json, "query") }}
Examples:
// Get all order totals
{{ $jmespath($json, "orders[*].total") }}
// Filter orders over $100
{{ $jmespath($json, "orders[?total > `100`]") }}
// Get first and last names as pairs
{{ $jmespath($json.people, "[].[firstName, lastName]") }}
// Nested extraction
{{ $jmespath($json, "response.data.users[*].{name: name, email: email}") }}
JMESPath shines when you need to filter, project, or reshape deeply nested structures without Code nodes.
Code Node Patterns for Data Transformation
Sometimes visual nodes and expressions can’t handle your transformation. The Code node provides full JavaScript capabilities for these cases.
When to use the Code node: If your transformation requires multiple steps, loops with conditions, or complex restructuring that would need 5+ visual nodes chained together.
For comprehensive Code node recipes, see our n8n Code Node JavaScript guide.
Restructuring Nested JSON
Problem: API returns deeply nested data you need to flatten.
const items = $input.all();
return items.map(item => {
const data = item.json;
return {
json: {
// Flatten nested user data
userId: data.user?.id,
userName: `${data.user?.profile?.firstName ?? ''} ${data.user?.profile?.lastName ?? ''}`.trim(),
userEmail: data.user?.contact?.email?.toLowerCase(),
userCity: data.user?.contact?.address?.city,
// Keep some original fields
createdAt: data.createdAt,
status: data.status
}
};
});
Key patterns:
- Optional chaining (
?.) prevents undefined errors - Nullish coalescing (
??) provides fallbacks - Template literals combine fields
Splitting Arrays into Items
Problem: Single item with array needs to become multiple items.
const items = $input.all();
const sourceArray = items[0].json.records || [];
// Convert each array element to an n8n item
return sourceArray.map(record => ({
json: {
...record,
processedAt: new Date().toISOString()
}
}));
Alternative with parent data preserved:
const items = $input.all();
const parent = items[0].json;
const children = parent.lineItems || [];
return children.map(child => ({
json: {
orderId: parent.orderId,
orderDate: parent.orderDate,
...child
}
}));
Aggregating Items into Single Output
Problem: Multiple items need to become one combined item.
const items = $input.all();
// Collect all records into one array
const allRecords = items.map(item => item.json);
// Calculate summary
const totalValue = allRecords.reduce((sum, r) => sum + (r.amount || 0), 0);
return [{
json: {
records: allRecords,
count: allRecords.length,
totalValue: totalValue.toFixed(2),
processedAt: new Date().toISOString()
}
}];
Handling API Response Variations
Problem: API sometimes returns array, sometimes single object.
const items = $input.all();
const response = items[0].json;
// Normalize to always be an array
let records = response.data;
if (!Array.isArray(records)) {
records = records ? [records] : [];
}
// Now safely process as array
return records.map(record => ({
json: {
...record,
_normalized: true
}
}));
For error handling patterns, see our timeout troubleshooting guide.
Real-World Transformation Scenarios
Theory is useful, but real-world examples show how these techniques combine.
API Response to CRM-Ready Format
Scenario: Transform webhook data for HubSpot or Salesforce import.
Input from form submission:
{
"submission_id": "12345",
"form_data": {
"first_name": "John",
"last_name": "Smith",
"work_email": "john@acme.com",
"company_name": "Acme Corp",
"phone_number": "+1-555-0123"
},
"submitted_at": "2025-01-15T10:30:00Z"
}
Transformation using Set node:
| Field | Expression |
|---|---|
firstname | {{ $json.form_data.first_name }} |
lastname | {{ $json.form_data.last_name }} |
email | {{ $json.form_data.work_email.toLowerCase() }} |
company | {{ $json.form_data.company_name }} |
phone | {{ $json.form_data.phone_number.replace(/-/g, '') }} |
lead_source | Website Form |
created_date | {{ $json.submitted_at }} |
E-commerce Order Processing
Scenario: Shopify order needs processing per line item, then reconstitution.
Workflow pattern:
- Webhook receives order with line items array
- Split Out on
line_itemsfield - creates item per product - HTTP Request to inventory API for each item
- Set calculates fulfillment data
- Aggregate recombines into single order
- HTTP Request sends to fulfillment system
Split Out configuration:
- Field to Split Out:
line_items - Include Other Fields: Enable (keeps
order_id,customer, etc.)
Aggregate configuration:
- Aggregate: Individual Fields
- Field Name:
line_items - Output Field Name:
processed_items
Spreadsheet Data Cleanup
Scenario: Google Sheets data has inconsistent formatting.
const items = $input.all();
return items
// Remove empty rows
.filter(item => {
const data = item.json;
return data.email && data.email.trim().length > 0;
})
// Clean and normalize each row
.map(item => {
const data = item.json;
return {
json: {
email: data.email?.trim().toLowerCase(),
name: (data.name || data.Name || '').trim(),
phone: (data.phone || data.Phone || '')
.replace(/[^\d+]/g, ''), // Keep only digits and +
status: (data.status || 'unknown').toLowerCase(),
// Convert empty strings to null
notes: data.notes?.trim() || null
}
};
});
Multi-API Data Merging
Scenario: Combine customer data from CRM with order data from e-commerce.
Workflow pattern:
- HTTP Request to CRM API - get customers
- HTTP Request to Shopify API - get orders (parallel)
- Merge node in Combine mode:
- Merge Mode: Combine
- Combine by: Fields
- Field:
email(from both sources)
Post-merge Set node:
// Expression for enriched customer record
{
email: {{ $json.email }},
name: {{ $json.crmData?.name ?? $json.shopifyData?.customer_name }},
totalOrders: {{ $json.shopifyData?.order_count ?? 0 }},
lifetimeValue: {{ $json.shopifyData?.total_spent ?? 0 }},
segment: {{ $json.crmData?.segment ?? 'Unknown' }}
}
Troubleshooting Data Transformation Issues
When transformations fail, these techniques help identify and fix the problem.
Common Errors and Solutions
| Error | Cause | Fix |
|---|---|---|
| ”Cannot read properties of undefined” | Accessing property on null/undefined value | Use optional chaining: $json.user?.email |
| ”items.map is not a function” | Expected array but received object | Normalize to array: Array.isArray(data) ? data : [data] |
| ”[object Object]” in output | Expression returns object instead of string | Use JSON.stringify() or access specific property |
| ”The JSON Output contains invalid JSON” | Malformed JSON in Set node | Use JSON fixer tool |
| Empty output from Split Out | Wrong field name or field is empty | Check exact field name, verify array exists |
| Duplicate items after Merge | Incorrect merge mode or field | Review merge configuration and input data |
Debugging Data Flow
Step 1: Pin data for testing
Click on any node’s output and select “Pin Data” to freeze it. This lets you test downstream nodes with consistent input.
Step 2: Check actual structure
In the Code node, log the full structure:
const items = $input.all();
console.log('Items count:', items.length);
console.log('First item:', JSON.stringify(items[0], null, 2));
return items;
Check browser developer console (F12) for output.
Step 3: Trace the transformation
Add temporary Set nodes between steps to see exactly what data looks like at each stage. Remove them once debugging is complete.
Step 4: Use the debugger tool
Our free n8n workflow debugger analyzes error messages and provides specific fixes.
Performance Considerations
Large datasets:
- Use Split In Batches to process in chunks
- Avoid loading entire datasets into Code node variables
- Consider pagination for API requests
Memory optimization:
- Filter early in the workflow to reduce item count
- Don’t carry unnecessary fields through multiple nodes
- Use streaming for file processing when available
Code node efficiency:
// Less efficient - creates intermediate arrays
const result = items
.filter(i => i.json.active)
.map(i => i.json)
.filter(j => j.value > 100);
// More efficient - single pass
const result = [];
for (const item of items) {
if (item.json.active && item.json.value > 100) {
result.push({ json: item.json });
}
}
return result;
For scaling challenges, our queue mode guide covers distributed processing.
Frequently Asked Questions
How do I access nested data in n8n expressions?
Use dot notation for known structures and optional chaining for uncertain ones.
Dot notation:
{{ $json.user.address.city }}
Bracket notation (for special characters):
{{ $json['user-data']['street-address'] }}
Optional chaining (safe access):
{{ $json.user?.address?.city ?? 'Unknown' }}
The optional chaining operator (?.) prevents errors when intermediate properties are undefined. The nullish coalescing operator (??) provides a fallback value.
For arrays, use index access:
{{ $json.orders[0].total }} // First order
{{ $json.orders?.at(-1)?.total }} // Last order (safe)
What’s the difference between Split Out and the Code node for splitting arrays?
Split Out node:
- Visual, no code required
- Handles standard array splitting
- Automatically includes parent fields if configured
- Best for simple array-to-items conversion
Code node:
- Full control over output structure
- Can apply transformations while splitting
- Better for conditional splitting or complex restructuring
- Required when you need to calculate values during split
Decision framework:
| Scenario | Use Split Out | Use Code |
|---|---|---|
| Simple array to items | Yes | No |
| Array with conditional filtering | No | Yes |
| Need to transform each element | Sometimes | Yes |
| Complex parent-child data merging | No | Yes |
| Quick prototyping | Yes | No |
Start with Split Out. Move to Code node when you need capabilities Split Out doesn’t offer.
Why does my Set node output show “[object Object]”?
This happens when an expression returns an object instead of a string, and n8n converts it to text for display.
Common causes:
- Accessing an object, not a property:
// Returns object - shows [object Object]
{{ $json.user }}
// Returns string - shows actual value
{{ $json.user.name }}
- Array access without index:
// Returns array object
{{ $json.tags }}
// Returns specific value
{{ $json.tags[0] }}
// Returns comma-separated string
{{ $json.tags.join(', ') }}
Fixes:
- Access the specific property you need
- Use
JSON.stringify()if you need the full object as text - Use
.join()for arrays - Use our expression validator to test expressions
How do I handle API responses that sometimes return an array and sometimes a single object?
Normalize the data at the start of your workflow:
In Set node (using expression):
{{ Array.isArray($json.data) ? $json.data : [$json.data] }}
In Code node (more control):
const items = $input.all();
const response = items[0].json;
let records = response.data;
// Normalize to array
if (!records) {
records = [];
} else if (!Array.isArray(records)) {
records = [records];
}
return records.map(record => ({ json: record }));
This pattern ensures downstream nodes always receive consistent input regardless of API behavior.
Can I transform data without using the Code node?
Yes. For most transformations, visual nodes and expressions are sufficient.
Transformations possible without Code node:
| Task | Solution |
|---|---|
| Rename fields | Set node with field mapping |
| Add calculated fields | Set node with expressions |
| Filter items | Filter node or IF node |
| Split array to items | Split Out node |
| Combine items | Aggregate node |
| Merge data streams | Merge node |
| Sort items | Sort node |
| Remove duplicates | Expression with .removeDuplicates() |
| Extract nested values | Expression with dot notation |
| Date formatting | Expression with Luxon methods |
| String manipulation | Expression with transformation functions |
When Code node becomes necessary:
- Complex conditional logic across multiple fields
- Custom aggregations (group by, complex sums)
- Restructuring that requires loops
- When 5+ visual nodes would be needed for the same task
The n8n documentation provides additional examples of visual transformation techniques.
Choosing Your Transformation Approach
Not every situation requires the same tool. Use this decision framework:
Start with expressions when:
- Accessing or formatting single values
- Simple calculations
- Quick field transformations
Use Set node when:
- Adding, renaming, or removing fields
- Multiple field transformations on same item
- Need visual clarity in workflow
Use Split Out/Aggregate when:
- Converting between array and items structure
- Standard split or combine operations
Use Code node when:
- Complex conditional logic
- Custom aggregations or grouping
- Performance critical with large datasets
- Visual approach would require 5+ nodes
For credentials handling across transformations, see our credential management guide.
Next Steps
You now have the knowledge to handle virtually any data transformation in n8n. Bookmark this page for reference when you encounter new transformation challenges.
Continue learning:
- n8n Expressions Cheatsheet for quick expression syntax reference
- Code Node JavaScript Guide for 25 copy-paste recipes
- Workflow Best Practices for production patterns
- Workflow Debugger Tool for troubleshooting errors
If your workflows need professional optimization or you’re stuck on complex transformations, our n8n consulting services can help you build reliable, maintainable automations.