8 errors, each read from n8n's source. Saved from https://workflowerrors.com — free, no signup.
You returned the right envelope — an item with a json key — but json points at
something that isn't a plain object. n8n's description:
In the returned data, every key named 'json' must point to an object.
The identical check runs on binary, and produces A 'binary' property isn't an object. Everything below applies to both.
This is stricter than JavaScript's typeof, and the exact test is four conditions:
typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)
So all four of these fail, and three of them surprise people:
| What you returned | Why it fails |
|---|---|
json: null |
typeof null === 'object', but n8n excludes it explicitly |
json: [ … ] |
arrays are objects to JavaScript; n8n excludes them |
json: new Date() |
a Date is an object; n8n excludes it |
json: 'some string', json: 42, json: undefined |
not objects at all |
The array case is the common one. { json: results } where results is an array of
rows reads perfectly and is wrong — each row needs to be its own item:
// throws
return [{ json: rows }];
// right
return rows.map(row => ({ json: row }));
The Date case is the sharp one, because a Date inside json is fine. n8n walks
the contents of json and stringifies anything that isn't a plain object — Dates,
RegExps and the like — at any depth. So { json: { created: new Date() } } works and
comes out as a string, while { json: new Date() } throws. The cleanup applies to what
is inside the envelope, never to the envelope itself.
{ json: undefined } throws, and so does an item where a branch of your code simply
never set json:
return $input.all().map(item => {
const out = {};
if (item.json.active) out.json = { id: item.json.id };
return out; // inactive items have no json at all
});
That's the same failure as json: undefined, and it's the one that shows up on item 40
of 50 rather than on the first item — because it's data-dependent. If this error names a
late item index, look for a conditional that doesn't assign on every path.
return null is a different problemReturning null as your whole result doesn't produce this message. It gets past the
earlier shape checks (typeof null === 'object') and then fails inside n8n's item
normalisation with a plain JavaScript type error rather than a validation message. If
you're seeing a type error with no helpful description, check whether some path returns
null instead of [].
A 'json' property isn't an object [item 4] — item 4 specifically… [line 12, for item 4] — when n8n knows the line as wellItem 0 failing usually means the shape is wrong for every item. A late index almost always means a data-dependent branch, as above.
The json object is what the rest of n8n operates on: expressions resolve against its
keys, the table view renders its columns, and downstream nodes address fields by name.
An array has indices rather than names, a Date has neither, and null has nothing at
all. None of them can carry the field access that every node after this one assumes.
Validating at the boundary is deliberate — the alternative is a workflow that runs another three nodes and then fails with an expression error pointing at code you didn't write.
Source: validateItem and isObject in
packages/nodes-base/nodes/Code/result-validation.ts and utils.ts, and
standardizeOutput in the same utils.ts for the stringify-inside-json behaviour.
Read directly from n8n's repository; the Code node documentation does not list these
error strings.
Verified 16 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).
Your Code node is in Run Once for All Items mode and returned something that isn't an array of objects. n8n checks the shape of the return value before it looks at any of your data, so this fires even when the data inside is perfect.
The description under it always reads: "Please return an array of objects, one for each item you would like to output."
Two conditions, and only two. Both come straight from n8n's validator:
The return value isn't an object at all. A string, a number, a boolean — or
nothing, because a path through your code hit the end of the function without a
return. That last one is the most common cause by a distance, and it's invisible
when you read the code, because the return you're looking at is inside an if.
You returned an array, but at least one element isn't an object.
return [1, 2, 3] and return ['a', 'b'] both fail here. So does an array with a
null hiding in it from a .map() that didn't cover every branch.
One consequence worth knowing: typeof null === 'object' in JavaScript, so
return null passes this check and fails further down with a different message.
If you're staring at this error, null is not what you returned.
The shape n8n wants is an array of objects, each with a json key:
return $input.all().map(item => ({
json: {
email: item.json.email,
domain: item.json.email.split('@')[1],
},
}));
Three checks that resolve almost every occurrence:
return [] as the last line of the function. An
empty array is a valid, explicit "no output"..forEach()? forEach returns undefined.
You want .map(), or push into an array and return that.await go missing? Returning a Promise fails this check. await it.n8n will wrap a bare object in json for you — but only under a specific condition,
and getting it half-right is what produces the confusing failures.
n8n reserves exactly five top-level item keys:
json binary pairedItem error index
The rule:
json for you. So return [{ name: 'Ada' }] works fine and becomes
{ json: { name: 'Ada' } }.json.So this is fine:
return [{ name: 'Ada', role: 'admin' }]; // wrapped for you
and this throws — not with the error at the top of this page, but with Invalid output format, described as "An output item contains the reserved key json. To
get around this, please wrap each item in an object, under a key called json.":
return [{ json: { name: 'Ada' }, role: 'admin' }]; // mixed. refused.
The fix is to put the stray key where it belongs:
return [{ json: { name: 'Ada', role: 'admin' } }];
If no reserved key is present but an unknown one is flagged, the message is
Unknown top-level item key: <yourKey>, described as "Access the properties of an
item under .json, e.g. item.json" — same mistake, caught on a different path.
If you're in Run Once for Each Item mode, you get different wording, and the wording tells you which mistake you made:
| Message | You did this |
|---|---|
Code doesn't return an object |
returned a string/number/undefined from per-item mode |
Code doesn't return a single object |
returned an array from per-item mode |
The second one's description says it outright: "If you need to output multiple items,
please use the 'Run Once for All Items' mode instead." In per-item mode you return one
bare object — return { json: { … } } — not an array of one.
So: if you're reading "Code doesn't return items properly", you are definitely in Run Once for All Items mode. That's already one thing narrowed down.
n8n appends location information to these messages when it has it:
… [item 4] — it knows which item failed… [line 12] — it knows which line… [line 12, for item 4] — bothSo A 'json' property isn't an object [item 4] means item 4 specifically. Items 0–3
were fine, which usually means the failure is data-dependent — a field that's missing
on some records, not a mistake in the shape of your code.
A 'json' property isn't an object — you returned the right outer shape, but a
json key points at a string, a number or undefined. Description: "In the
returned data, every key named 'json' must point to an object." Same for
A 'binary' property isn't an object.`items` is not defined. Did you mean `$input.all()`? — you're using the old
Function node's variable. n8n adds that suggestion itself, but only when you haven't
declared items yourself. The per-item equivalent is
`item` is not defined. Did you mean `$input.item.json`?Can't use .first() here — described as "This is only available in 'Run Once for
All Items' mode". Thrown for .first(), .last(), .all() and .itemMatching()
used in per-item mode.pairedItem.The Code node sits on a boundary. Inside it you have ordinary JavaScript values; the moment you return, n8n needs items — objects in a fixed envelope that the rest of the workflow can carry, link and display. Nothing can translate an arbitrary JS value into that envelope unambiguously, so n8n validates instead of guessing, and throws at the boundary rather than letting a malformed item travel three nodes and fail somewhere that makes no sense.
The wrapping rule above is the one place n8n does guess — and it only guesses when your object gives it no reason to think you meant something else.
Sources: packages/nodes-base/nodes/Code/result-validation.ts,
ValidationError.ts, JsCodeValidator.ts and reserved-key-found-error.ts in n8n's
repository, read directly. n8n's Code node docs do not list these error strings.
Verified 15 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).
The instance you're running now is using a different encryption key than the one that encrypted those credential rows. The rows are intact and the database is fine — n8n simply cannot read them.
This almost always means one thing: the database was restored, moved or recreated without the key that went with it.
This is the part that wastes people's evenings. n8n gets its encryption key from one of two places, and they are not equal partners:
If the settings file exists, the key in that file is used.
Only if the settings file does not exist is N8N_ENCRYPTION_KEY used — and n8n
then writes it into a new settings file.
If the file exists and N8N_ENCRYPTION_KEY is set and they differ, n8n
refuses to start:
Mismatching encryption keys. The encryption key in the settings file <path> does not
match the N8N_ENCRYPTION_KEY env var. Please make sure both keys match.
The settings file lives at config inside n8n's user folder — ~/.n8n/config by
default. It's JSON, and the key is under encryptionKey.
So the reflex fix — "set N8N_ENCRYPTION_KEY to the old value and restart" — does
nothing on an instance that already has a settings file, or it turns a runtime error
into a container that won't boot. Neither outcome looks like progress and both are
commonly reported as "setting the env var didn't work".
Find it first. It is one of:
encryptionKey value in the old instance's ~/.n8n/configN8N_ENCRYPTION_KEY you set in a compose file, .env, Kubernetes secret or
hosting-provider config~/.n8n as well as the databaseThen make this instance use it, via whichever route matches how the instance is set up:
encryptionKey in ~/.n8n/config to the old value and
restart. If you also have N8N_ENCRYPTION_KEY set somewhere, make it match or remove
it, or you'll hit the mismatch error above.N8N_ENCRYPTION_KEY to the old value
before first start. n8n will adopt it and write the file itself.Restart, open any affected credential, and confirm the fields populate. That is the only proof that worked — the absence of an error on the credentials list page is not.
There is no recovery path and no tool that can help. The secrets were encrypted with a key that no longer exists; that's the entire point of encrypting them.
What survives: the workflows, the credential records themselves, their names, types and every node's reference to them. What's lost: the secret values inside.
So the repair is to open each affected credential, re-enter the secrets, and save — which re-encrypts them under the current key. Tedious, proportional to how many credentials you have, and it does not require rebuilding any workflows.
Say it plainly to yourself before you spend three hours searching: nobody on a forum has a workaround for this, because there isn't one.
The encryption key is not stored in the database. If it were, it would be encrypting data while sitting next to it, which protects nothing. It lives on the instance's filesystem instead — which means the database and the key are two separate things to back up, and it is entirely possible, and very common, to back up only one.
The recurring version is a container whose ~/.n8n isn't on a named volume. n8n starts,
finds no settings file, auto-generates a key (it logs "No encryption key found -
Auto-generating and saving to: …"), and everything works. The container is later
recreated — an image update, a host migration, a redeploy — the filesystem goes with it,
n8n generates a new key, and now the database it reconnects to is full of rows it can
no longer read. Nothing broke at the moment of failure; it broke at the moment the
volume wasn't declared.
In queue mode, a worker started without an encryption key refuses to start rather than generating one of its own. That's deliberate: a worker that silently invented a key would write credential data the main instance couldn't read.
N8N_ENCRYPTION_KEY explicitly, from the first start, rather than letting n8n
generate one. A key you chose is a key you can find again.~/.n8n on a named volume, not in the container's writable layer.These come from the same place in n8n's source and are worth telling apart:
| Message | Meaning |
|---|---|
Credentials could not be decrypted… |
wrong key — this page |
Mismatching encryption keys… |
settings file and env var disagree; n8n won't start |
No data is set on this credentials. |
the credential record has no encrypted payload at all |
Decrypted credentials data is not valid JSON. |
decryption succeeded but produced garbage — usually partial or corrupted data, not a key problem |
Credentials data is not in a valid format. |
the stored shape isn't what n8n expects |
The fourth one is the one worth noticing: if decryption succeeded and the JSON is invalid, your key is correct and your data is damaged. That is a different problem with a different owner — your storage or your backup process, not your key.
Sources: packages/core/src/constants.ts (the exact message strings),
packages/core/src/credentials.ts (where each is thrown) and
packages/core/src/instance-settings/instance-settings.ts (the precedence rule and the
mismatch error) in n8n's repository, read directly.
Verified 15 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).
An item you returned has one of n8n's reserved keys and one of your own keys at the same level. n8n refuses that combination, because it can no longer tell whether the object is an n8n item or your data.
The description under it names the key it tripped on:
An output item contains the reserved key
json. To get around this, please wrap each item in an object, under a key calledjson.
json binary pairedItem error index
That list is exact — it's a fixed set in n8n's source, not a convention. Any other top-level key is "yours".
At the top level of an item, it's all or nothing:
json for you.
return [{ name: 'Ada', role: 'admin' }] becomes { json: { name: 'Ada', role: 'admin' } }.So the failing shape is almost always someone who got the envelope right and then attached one extra thing to the outside of it:
// throws — 'json' is reserved, 'role' is yours
return [{ json: { name: 'Ada' }, role: 'admin' }];
// fine — role belongs inside
return [{ json: { name: 'Ada', role: 'admin' } }];
The same applies to binary. return [{ binary: { data: … }, filename: 'x.pdf' }]
throws for exactly the same reason; the filename goes inside json, or inside the
binary entry's own metadata.
n8n reports the first reserved key it encounters while walking the object's own
keys, in insertion order. So an item built as { pairedItem: 0, json: {…}, note: 'x' }
reports pairedItem, not json — even though json is the one you were thinking
about. Don't read the named key as "the key that's wrong". Read it as "the key that
proved this is meant to be an n8n item".
The one that's actually wrong is the unreserved one, and n8n doesn't name it here.
n8n appends location information when it has it:
Invalid output format [item 4] — item 4 specifically… [line 12] or … [line 12, for item 4] — when a line number is known tooAn item index this far into the batch usually means the shape is data-dependent: some branch of your code builds the item differently from the others.
An n8n item is a plain JavaScript object, and so is your data. There's no type tag to separate them. The only signal available is which keys are present — so n8n treats the presence of any reserved key as "this object is an envelope" and the presence of a foreign key as "this object is payload". An object claiming both is genuinely ambiguous, and every possible guess is wrong some of the time.
That's also why the no-reserved-keys case gets auto-wrapped without complaint: there's no ambiguity to resolve.
<key> — the other branch of the same check. Your item
has a foreign key but no reserved key of its own, while some other item in the
array does. That means the array is inconsistent.json isn't an object.Source: packages/nodes-base/nodes/Code/reserved-key-found-error.ts and
result-validation.ts in n8n's repository, read directly. The reserved-key list is
REQUIRED_N8N_ITEM_KEYS in that file. n8n's Code node documentation does not list this
error string.
Verified 16 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).
Your Structured Output Parser got text back from the model that either isn't valid JSON or doesn't match the schema you gave it — and if you built that schema with Generate from JSON Example, every field in your example is mandatory, so one missing key is enough to throw this.
Upgrading n8n will not fix it. This is thrown by design, and it's been reported across 1.88, 1.101, 1.107 and 1.112 on Claude, Gemini, GPT-4o and GPT-3.5 alike. The GitHub issue for it is closed as not planned.
The Structured Output Parser does three things in order, and the error message is identical whichever step fails. That's why the fixes people post in the forum contradict each other — they're fixing different bugs.
JSON.parse() whatever's leftSo before changing anything, read what the model actually sent. Two ways:
If it isn't JSON, you have Failure A. If it's JSON but a key is missing, spelled differently, or the wrong type, you have Failure B.
n8n's fence stripping is stricter than people expect. It walks the reply line by
line and only strips a fence when the opening line is exactly three backticks, or
three backticks followed by json, with nothing else on that line — and the closing
line is three backticks alone.
These all survive stripping and then blow up JSON.parse:
jsonjavascript or js instead of json{ on the same lineHere is the JSON you asked for: {"…"}Prose before the opening fence is fine, and so is prose after the closing fence. It's the fence line itself that has to be clean.
The fix is in the prompt, not the parser. Put this in the system message, as the last line so it isn't buried:
Respond with a single JSON object and nothing else. No markdown, no code fences, no explanation before or after.
If your model node has a response-format option that forces JSON, turn it on as well — that removes the failure at the source instead of asking the model nicely.
This is the common one, and it usually traces back to a single documented behaviour:
Generate from JSON Example treats every field in your example as required.
So if your example is
{ "company": "Acme", "amount": 1200, "note": "paid late" }
then the model must return note on every single item. On the run where there's
nothing to note, it omits the key, Zod rejects the object, and you get this error —
on item 40 of 50, which is why it looks intermittent.
Fix: switch Schema Type to Define using JSON Schema and write the schema
by hand, putting only the genuinely mandatory keys in required:
{
"type": "object",
"properties": {
"company": { "type": "string" },
"amount": { "type": "number" },
"note": { "type": "string" }
},
"required": ["company", "amount"]
}
Three more things that cause shape failures, in the order they're worth checking:
$ref is not supported. n8n documents this. A schema that factors out a
repeated sub-object with $ref will not work — inline it.enum. A model asked for "intent": "string" will eventually invent a
fifth category; a model asked for "intent" from an enum of four will not.If the message you actually got was "The AI model returned an empty response to the Structured Output Parser", that's a different error with a different cause: the model ran out of output tokens before it finished the object. Raise the model's max output tokens, shorten the prompt, or simplify the schema. Nothing above applies.
Several people in the main forum thread report the same prompt and the same schema working once they swap the AI Agent for a Basic LLM Chain. That isn't superstition. Under an Agent, the parser validates the agent's final answer, and an agent that has just run two tool calls has a strong habit of narrating what it did before answering. A chain has no tools and nothing to narrate.
If you don't need tool calling, use the chain. It's a smaller surface.
The Auto-fixing Output Parser wraps another output parser, and when the first one fails it calls out to another LLM to repair the output. It's worth adding — but be clear about what it can and can't do:
So: fix the schema first, then add auto-fixing as a seatbelt. Not the other way round.
An LLM produces tokens, not objects. The Structured Output Parser is a validator bolted onto the end of a process that has no native concept of a schema, so the schema is enforced after the fact by throwing. Every design decision follows from that — which is why the durable fix is always to shrink the gap between what you ask for and what the model can reliably produce, rather than to catch failures harder.
Concretely, the workflows that don't hit this error are the ones that ask for a flat object, with a short list of required keys, with fixed-value fields declared as enums, and a system message that forbids anything but JSON.
A triage workflow I built applies both of the rules above to inbound email. The
intent field is an enum of six fixed values rather than a free-text string, so
the model cannot invent a seventh category on the one message that doesn't fit. And
every company name and figure it extracts has to appear literally in the source message
before anything downstream accepts it.
The enum removes the invented-value failure. The literal check removes the confidently-wrong one. Neither costs an extra model call, which is why they beat adding a repair pass on top.
Sources: n8n's source for the Structured Output Parser; the n8n docs for the Structured Output Parser and Auto-fixing Output Parser; n8n-io/n8n issue #18765; community.n8n.io thread 106424. n8n's own documentation does not currently cover this error.
Verified 15 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).
Your expression used .item to reach back to an earlier node, and somewhere in the
chain between that node and this one, a node produced output without attaching
pairedItem metadata — so n8n has no way to know which of the earlier node's items
produced the item you're currently on.
If you just want it working: replace .item with .first(), .last() or
.all()[index]. That skips item tracing entirely. The rest of this page is for
deciding which of those is actually correct, and for the case where you need .item
to work.
n8n builds this message from a template, so the wording varies with what you wrote:
Paired item data for <method> from node '<NodeName>' is unavailable.
Ensure '<NodeName>' is providing the required output.
<method> is whichever of these you used — and it is the literal text that appears:
| In the message | What you wrote |
|---|---|
item |
$('Node').item |
itemMatching |
$('Node').itemMatching(i) |
pairedItem |
the default, when n8n resolves linking internally |
$getPairedItem |
internal resolution from a Code node |
The item index is attached to the error as context rather than being part of the message itself, so depending on where you read the error — the node's red banner, the execution log, a forum paste — you may or may not see a trailing item number. Don't treat its absence as a different error.
If the node you're reaching back to has pinned data, n8n throws a completely different message for the same underlying problem:
Using the item method doesn't work with pinned data in this scenario.
Please unpin '<NodeName>' and try again.
and in some paths simply Unpin '<NodeName>' to execute.
This is worth checking before anything else, because it's the version that hits people mid-build — you pin some test data to stop re-hitting an API, and an expression that worked five minutes ago stops working. Unpin the node and the error goes. Nothing is wrong with your expression.
This is n8n's own recommendation and it's right most of the time. Which one you want depends on what the referenced node actually returns:
.first() — the referenced node returns exactly one item (a config lookup, a
single API record, a Set node with one row). This is the common case and the one
people should reach for..last() — you want the most recent item, typically after a loop..all()[index] — the referenced node returns several items and you can address
the one you want by position.{{ $('Get Customer').first().json.email }}
If none of those is honest — if you genuinely need the item that corresponds to this one — then you need Fix 2.
The culprit is almost always a Code node in "Run Once for All Items" mode. In that
mode you build the output array yourself, and if you don't set pairedItem, nothing
downstream can trace through your node.
Two things worth knowing, both from n8n's source:
pairedItem: { item: index } for you. If
your Code node runs per item, it is not the culprit. Stop looking at it.pairedItem accepts a bare number as shorthand — n8n normalises pairedItem: 0
to pairedItem: { item: 0 } internally. Both forms work.So in "Run Once for All Items":
const items = $input.all();
return items.map((item, i) => ({
json: { ...item.json, domain: item.json.email.split('@')[1] },
pairedItem: { item: i },
}));
If your node fans out — one input item becoming several output items — point each output at the input it came from:
const out = [];
$input.all().forEach((item, i) => {
for (const line of item.json.lines) {
out.push({ json: line, pairedItem: { item: i } });
}
});
return out;
n8n will also nudge you about this on its own: when a Code node returns a different
number of items than it received, or any item without pairedItem, the output panel
shows a hint reading "To make sure expressions after this node work, return the input
items that produced each output item." That hint is not an error and is easy to scroll
past, which is why most people meet it as this error instead, three nodes later.
A node that aggregates, merges, summarises or splits has genuinely destroyed the
one-to-one relationship. There is no correct pairedItem for a row that was computed
from forty input items. For anything downstream of such a node, .item is the wrong
tool and Fix 1 is the answer, permanently.
This is the part that makes searching for it so frustrating: n8n's item-linking code throws five distinct messages, and it rewrites the message when the failure happens inside a Code node rather than in an expression field. So the same root cause reaches you under different text depending on where you hit it.
| Expression field | Inside a Code node | What it means |
|---|---|---|
Paired item data for … is unavailable |
(same) | a node in the chain didn't supply pairedItem |
Invalid expression / No path back to referenced node |
(same) | the referenced node isn't wired to this one at all. Wire it up — intermediate nodes are fine |
Branch not found / Paired item references non-existent branch |
Invalid branch reference |
a node pointed at an output branch that doesn't exist |
Paired item resolution failed / Unable to find paired item source |
Data not found |
traversal ran out of chain before reaching the node |
Multiple matches found / Multiple matching items for item [N] |
Multiple matches |
more than one item could be the source — genuinely ambiguous |
Two neighbours that look similar but aren't item-linking problems at all:
Node 'X' hasn't been executed — the referenced node hasn't run yet on this
execution. n8n's own hint here suggests guarding with
{{ $if( $("X").isExecuted, …, "") }}.No data found from `main` input — the referenced node ran but produced no
output on its main branch. Nothing to link to, because there's nothing there.n8n items don't carry their history inside them. Instead every node is expected to
stamp each output item with a pointer to the input item that produced it, and .item
resolves by walking that chain of pointers backwards, node by node, until it reaches
the node you named. One node that doesn't stamp its output breaks the chain — and the
error surfaces at the node doing the reading, which may be well downstream of the
node actually at fault.
That's the practical lesson: the node named in the error is the node that failed to supply the data, not the node you need to edit. Read the node name in the message and go look at that node, not at the expression in front of you.
A lead-reply workflow I built sidesteps this error rather than fixing it. Each branch — invalid address, unusable draft, mail-server rejection, successful send — writes its own outcome row with a reason at the point the outcome is known, instead of leaving an expression further downstream to trace back which lead a given result belonged to.
Nothing in it uses .item, so nothing in it can fail to resolve .item. That is
usually the cheaper answer: when a workflow needs item linking in several places, the
shape of the workflow is often what's wrong, not the expression.
Sources: packages/workflow/src/workflow-data-proxy.ts and the editor's English
string table in n8n's repository, read directly. n8n documents item linking in general
but does not have a page for this error string.
Verified 15 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).
Nothing is listening on that method and path at the moment your request arrived. n8n keeps a live registry of webhook routes, and a route is only in it while the workflow is active (production URL) or during the single call that follows clicking Execute workflow (test URL).
Don't read the message. Read the hint underneath it. n8n attaches one of exactly two hints, and which one you get tells you which URL n8n thinks you called — which is the whole diagnosis.
If the response hint says:
Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)
…n8n received your request on the test URL. Test registration is armed by clicking Execute workflow and is consumed by one call. Not one minute, not one session — one call. If you clicked Execute, sent a request that failed for an unrelated reason, and sent a second one, the second gets this error.
If the hint says:
The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)
…n8n received your request on the production URL, and the workflow isn't active. Flip the toggle top-right.
That second hint also quietly answers the question people ask next: a successful production call shows nothing on the canvas. It's in Executions. A silent canvas is not evidence that the call failed.
n8n checks whether that path is registered for other HTTP methods first, and if it is, you get a different message entirely:
This webhook is not registered for POST requests. Did you mean to make a GET request?
If you're reading that, stop debugging activation — the workflow is active and the path
is correct. You sent the wrong verb. (With several registered methods it lists them:
Did you mean to make a GET, PUT or PATCH request?)
So the two messages split your problem in half before you touch anything:
| What you see | What's true |
|---|---|
The requested webhook "…" is not registered. |
nothing is listening on that path at all |
This webhook is not registered for X requests. Did you mean… |
the path is live; your HTTP method is wrong |
If you found that phrase in your server logs rather than in an HTTP response, it's n8n's own logging of this same failure:
Received request for unknown webhook: <the message above>
The useful part is everything after the colon — it's the message from the table above, including the path n8n actually received. On a self-hosted instance behind a reverse proxy that's the line worth reading, because a proxy that strips or rewrites a path prefix means n8n is being asked for a route you never typed. Compare the path in that log line against the URL you sent before assuming the workflow is at fault.
The registry only holds routes that can currently be served. An inactive workflow has no listener — not a disabled one, no listener at all — so the HTTP layer answers 404 without ever consulting your workflow. That's why the error arrives instantly and why nothing appears in Executions: the request never reached an execution.
Test registration works the same way, with a deliberately tiny lifetime, because it exists to let you inspect one payload on the canvas, not to serve traffic. Treating the test URL as a real endpoint is the mistake the design invites and this error punishes.
Sources: packages/cli/src/errors/response-errors/webhook-not-found.error.ts and
packages/cli/src/webhooks/webhook-request-handler.ts in n8n's repository, read
directly. The hint text quoted above is n8n's own, verbatim from that source.
Verified 15 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).
The named key isn't one of n8n's five reserved item keys, and this item has no reserved key of its own — but some other item in the array you returned does.
That last clause is the whole diagnosis, and it isn't in the message: this error means your array is inconsistent. Some items are correctly-formed n8n items and this one is a bare object. n8n's description tells you the fix without telling you the cause:
Access the properties of an item under
.json, e.g.item.json
Both come out of the same check, three lines apart, and the difference is precise:
| This item has… | You get |
|---|---|
| a reserved key and a foreign key | Invalid output format |
| a foreign key and no reserved key | Unknown top-level item key: <key> |
And the check only runs at all when at least one item somewhere in the array carries
a reserved key. If no item does, n8n wraps every object in json for you and nothing is
validated — which is why a uniformly bare array like return [{ a: 1 }, { a: 2 }] works
perfectly.
So the shape that produces this error looks like this:
return $input.all().map(item => {
if (item.json.skip) {
return { json: { skipped: true } }; // an n8n item
}
return { name: item.json.name }; // NOT an n8n item
});
Both halves look reasonable in isolation. Together they're a mixed array, and the first branch is what switches validation on for the second.
Make every item the same shape. Pick the envelope and use it everywhere:
return $input.all().map(item => {
if (item.json.skip) {
return { json: { skipped: true } };
}
return { json: { name: item.json.name } };
});
If the inconsistency is coming from a .filter() or an early return inside a loop,
the usual culprit is one path that forgot the wrapper. Search your function for every
return and push and check they all produce the same keys.
This is worth knowing because it rules out half the places you might look.
In Run Once for Each Item mode you return a single object, and n8n normalises it
before validating: a bare object gets wrapped in json first, so there's nothing
foreign left at the top level by the time the check runs. The only way to fail in that
mode is to hand over an object that already has json plus something else — and that
produces Invalid output format, not this.
So if you're reading this message, you are in Run Once for All Items mode, and the problem is across your items rather than inside any one of them.
There's a narrow path where a mixed array slips past this check and fails one step later, inside n8n's normalisation helper, with:
Inconsistent item format
It happens when the non-json items carry only reserved keys — say
[{ json: {…} }, { pairedItem: 0 }]. Nothing foreign is present, so the check above has
nothing to complain about, and the normaliser refuses the mix instead. Same underlying
mistake, different message, and the fix is identical: one shape for every item.
n8n can't require the json envelope outright, because returning bare objects is a
documented convenience and a lot of working code depends on it. So the envelope is
optional — until you use it. The moment one item declares itself an n8n item, n8n stops
guessing for the rest of the batch and starts enforcing.
That conditional behaviour is the reason this error can appear in a workflow that ran
fine yesterday: add one branch that returns { json: … }, and every other branch is
suddenly being held to a standard it wasn't before.
Source: validateTopLevelKeys in
packages/nodes-base/nodes/Code/result-validation.ts, and
packages/core/src/execution-engine/node-execution-context/utils/normalize-items.ts
for the normalisation order and the Inconsistent item format path. Read directly from
n8n's repository; the Code node documentation does not list these error strings.
Verified 16 September 2026 against n8n 2.39.5 (source read on master @ 2.40.0).