Hello n8n community!
I have searched the documentation but did not find a successful answer to my problem.
The use case
I’m processing a list of customer records. Each customer has a contacts array containing items of different types (e.g. phone, email). My goal is to:
- Split the
contacts array into individual items
- Apply a different mapping depending on the contact type (e.g. phone numbers need to be parsed and reformatted, emails pass through directly)
- Re-aggregate all mapped contacts back into a single array
- Merge that array back into the original customer item to build the final payload
The workflow
The Merge (append) node works inside a loop when the Aggregate is the last node. However, adding a Merge (combine by position) after the Aggregate — which is necessary to reassemble the contacts back into the customer item — breaks the loop entirely.
Test data:
{
"data": [{
"firstName": "John",
"lastName": "Doe",
"contacts": [
{
"type": "phone",
"value": "+33652252525"
}
]
},
{
"firstName": "Mary",
"lastName": "Doe",
"contacts": [
{
"type": "email",
"value": "marydoe@example.com"
}
]
},
{
"firstName": "John",
"lastName": "Smith",
"contacts": [
{
"type": "email",
"value": "johnsmith@example.com"
},
{
"type": "phone",
"value": "+33652252526"
}
]
}]
}
Workflow
Information on your n8n setup
- n8n version: 2.20.9
- Database (default: SQLite): PostgrSQL
- Running n8n via (Docker, npm, n8n cloud, desktop app): Docker
Welcome @thomaslc to our community! I’m Jay and I am a n8n verified creator.
The Merge (combine by position) node inside a SplitInBatches loop breaks because it expects two inputs to arrive simultaneously - but inside a loop, only one branch is active per iteration. It’s not designed for that pattern.
The cleanest fix here is to skip the inner loop + Merge entirely and handle the full contacts transformation in a single Code node per customer. Something like this:
const contacts = $json.contacts;
const mapped = contacts.map(c => {
if (c.type === 'phone') {
return { type: 'phone', value: c.value.replace(/\s/g, '') };
}
return c;
});
return [{ json: { ...$json, contacts: mapped } }];
This runs per item in your outer loop, transforms the contacts array in place, and feeds straight into your final payload - no inner loop or Merge needed.
Interesting limitation. You could simplify the workflow using a Code node (as answered previously above) or even an Edit Fields node with some JS. Another option is to pass the loop functionality into a sub-workflow and merge the sub-workflow output.
The cleanest way to handle this is a single Code node that does all four steps in one shot, skipping the loop entirely:
return items.map(item => {
const mapped = item.json.contacts.map(c => {
if (c.type === 'phone') return { ...c, value: c.value.replace(/\D/g, '') };
return c; // email passes through
});
return { json: { ...item.json, contacts: mapped } };
});
This avoids the loop-merge conflict because you’re transforming the array inline. The Merge (combine by position) breaking the loop is expected - n8n loops don’t handle mid-loop aggregation well. If you need to keep the node-based approach, pass each processed customer into a sub-workflow via Execute Workflow and aggregate the results after all executions complete.
Hello @nguyenthieutoan, @njogued
Thanks for your feedback! The reason I’m using a loop is that I use custom nodes like a phone checker node. A code node could’nt replace it.
What I ended up doing was to make a subworkflow that will be called using the “Run once for each item” option.
hi @thomaslc , welcome!
i looked at the documentation regarding these items, and from my understanding it has to do with the way n8n maintains the context of the item. when you use Split Out, Loop Over Items, Merge and Aggregate, it needs to preserve or reconstruct the link between each aggregated contact and the original customer. The Merge combine by position can break this logic if the quantity or order of the items is not exactly the same.
here are the docs that will help you: