Ibrahim Harbutlu
Playbook

The n8n Bug Where a Node Silently Overwrites $json

4 min read

Editorial illustration with sage green blob shapes representing a playbook post about an n8n data-mapping bug

I was building an n8n workflow that watches a Gmail inbox, runs each new email through an AI agent, sorts it into one of five categories, and logs the result to a spreadsheet. For emails that need a reply, it also drafts one in Gmail — never sends it, just drafts it. The workflow ran. It created the draft. It logged a row to the sheet. Everything looked fine until I opened the sheet and checked the actual values.

The category, priority, and summary columns had the wrong data in them. Not blank, not an error. Wrong. The row had an id and a message object where category and priority should have been.

How an n8n node overwrites $json without warning

The node that creates the Gmail draft does its job, then hands off its own API response as the new $json for everything downstream — {id: "...", message: {...}}. It doesn’t merge that response with the data already flowing through the workflow. It replaces it.

Plenty of n8n nodes return their own response shape instead of passing data through, so that part isn’t unusual. The problem showed up one step later: the node writing to Google Sheets had been built using cross-node references specifically to dodge this — pulling values like $('Parse AI Output Data').item.json.category instead of trusting whatever $json happened to be sitting in front of it.

In “defineBelow” mapping mode, that reference didn’t hold. The Sheets node fell back to auto-mapping whatever $json it was currently next to, which by then was Gmail’s response, not the parsed AI output. The cross-node reference itself was written correctly. The node just didn’t honor it the way I expected.

The execution log showed no problem at all — green checkmarks the whole way down, a real draft sitting in Gmail, a new row in the sheet. I only caught it because I happened to open the sheet and read what had actually landed in each column instead of trusting the run history.

The fix for n8n nodes that overwrite shared data

I added a Code node between the Gmail draft node and the Sheets node. Its only job is rebuilding a clean object from two known sources: the original Gmail Trigger output, and the parsed AI output from the triage step, both referenced by node name. It doesn’t touch whatever $json happens to be sitting one step upstream — it goes and gets the seven fields the sheet needs, by name, every time.

If a node in your own n8n workflow is known to replace $json instead of extending it, don’t assume a cross-node reference will survive a mapping mode you haven’t tested by hand. I hadn’t tested “defineBelow” specifically, and that’s the mode that broke. A normalization node is one extra step on the canvas, and it’s cheaper than re-checking spreadsheet columns after every run.

A second bug, much easier to catch

While debugging the above, I also hit a configuration error in the AI agent’s system prompt: the Chat Messages field had "type": "SystemMessage" set, which isn’t valid in n8n — it only accepts "AI", "System", or "User". This one failed loudly with an “Invalid message type” error, so it took five minutes once I saw it. I’m including it because it landed in the same build as the silent bug above, and the two needed different instincts to catch — one threw an error message, the other ran clean and just put the wrong data in a cell.

What to check in your own n8n workflow

This workflow was packaged as Stop Sorting Support Emails Manually, since taken down. What I do have is the same root pattern showing up twice in two separate n8n builds: an upstream node silently changes the shape of $json, and the cross-node reference meant to protect against it doesn’t behave the way the docs or your intuition suggest it will. I ran into a related version of this — a chainLlm node’s JSON output landing somewhere other than $json.text — while building the free Launch Content Pack template, also since taken down. If something downstream in your own workflow is reading the wrong field, check whether a node in between replaced $json outright before you assume the bug is somewhere else. I wrote about a similar kind of bug — one that only shows up once you stop trusting that a feature “works” and start checking what it actually does — in a recent post on an Obsidian plugin licensing bug.