data/skills/n8n-binary-and-data/MERGE_FOR_CONTEXT.md
A common, maddening bug: an item carries both json and binary, it runs through a JSON-only node (Edit Fields, Code, IF), the binary slot quietly disappears, and the email node three steps later has nothing to attach. No error, no validation warning — just a missing file.
The fix is to keep the binary on a branch that doesn't touch it, and recombine. This is the same Merge node covered in n8n-node-configuration's gotchas; here it's used specifically to re-attach binary.
Split the stream at the source: one branch does the JSON work, the other carries the original item (binary intact) untouched. Merge them back.
[Source with binary] ─┬─→ [Edit Fields: change JSON] ─┐
│ (binary stripped here) │
│ ├─→ [Merge: combineByPosition] ─→ [Email: attach]
│ │
└──────────────────────────────────┘
(bypass — binary passes through unchanged)
The merged item gets its JSON from the transform branch and its binary from the bypass branch.
The source already feeds the transform branch. You add the bypass connection and the Merge with n8n_update_partial_workflow:
{
"operations": [
{ "type": "addNode", "node": {
"name": "Merge",
"type": "n8n-nodes-base.merge",
"parameters": { "mode": "combine", "combineBy": "combineByPosition" }
}},
{ "type": "addConnection", "source": "Edit Fields", "target": "Merge", "targetInput": 0 },
{ "type": "addConnection", "source": "Source", "target": "Merge", "targetInput": 1 },
{ "type": "addConnection", "source": "Merge", "target": "Send Email" }
]
}
The exact parameter names (mode, combineBy, combineByPosition, and how numberOfInputs is expressed) have shifted across Merge node versions — confirm the current shape with get_node on nodes-base.merge for the user's version before committing the structure. The principle is stable; the field names move.
Two wiring details that bite (both detailed in n8n-node-configuration's Merge section):
targetInput: 1 (the second input).For re-attaching binary, you want position-based combination:
| Mode | What it does | Use for binary re-attach? |
|---|---|---|
combineByPosition | Pairs item N from input 1 with item N from input 2 | ✅ Yes |
combineBySql / combineByFields | Joins on a key | Only if the two branches share a join key |
combineAll | Cartesian product (N×M items) | ❌ No — explodes the item count |
append | Concatenates inputs end to end | ❌ No — doesn't pair items |
combineByPosition is the right default: it keeps the item count at N and pairs each transformed JSON item with its corresponding binary-bearing original. For this to work, both branches must emit items in the same order and count — which they do when they share a single source.
A Merge combines both json and binary from the items it pairs. When one input holds the JSON you want and the other holds the binary you want, the merged item carries both. The binary survives because it traveled on the branch that never touched it.
If the transforming node can preserve binary itself, do that instead — it's one node, not three:
includeOtherFields so the node carries unmentioned fields and the binary slot forward.binary: $input.item.binary explicitly in the returned item (see BINARY_BASICS.md).Reach for Merge only when the transforming node genuinely can't carry the binary, or when the JSON and binary come from genuinely different upstream nodes.
If the chain has many strip points, threading binary through all of them — and Merging at each one — becomes more work than it's worth. Two better routes:
BINARY_BASICS.md).$binary, so use the passthrough input mode if the sub-workflow must receive bytes directly.Past a couple of strip points, one of these is usually less work — and less fragile — than keeping every node in a long chain honest about binary.
A merged-but-missing binary won't show in validation. Confirm in the execution:
n8n_test_workflow, then pull the execution with n8n_executions.json from the transform branch and the binary from the bypass branch.| Mistake | Symptom | Fix |
|---|---|---|
| Noticing the strip too late | The original binary is already gone | Inspect the execution after each node during development |
| "Merging" a single-source chain with no bypass | Nothing to merge with; binary still missing | Split the stream at the source so binary rides a bypass branch |
combineAll where you meant combineByPosition | N×M items instead of N | Choose the mode deliberately |
| Bypass branch on the wrong input index | Wrong pairing, or the branch drops | Connections are 0-based; verify with n8n_get_workflow |
| Forgetting to raise the Merge input count past 2 | A third branch silently drops | Set the input count to match the wired branches |