Posting this because it cost me a few hours and it fails silently, which is the worst combination. If you route anything important through an IF or Switch node on a boolean, this is worth ten minutes of your time.
There are a few open issues and forum threads describing this behavior without a root cause attached. This is what I found.
The symptom
I had a Switch node deciding whether to send an alert. Upstream node output looked like this:
{ “shouldEscalate”: true }
Switch condition: value:
{{ $(‘Extract Response’).first().json.shouldEscalate }}
operator: Boolean → is true
Every execution routed false. No error. Execution reported success. The alert just never fired.
What it wasn’t
I burned time on the wrong hypotheses first, so here they are to save you the trip:
• Not .first() vs .item — I checked
• Not a multi-run/item-lineage issue
• Not the upstream node producing the wrong value
I proved the last one by dropping a Set node directly before the Switch that emitted the raw value plus its type: { “val”: true, “type”: “boolean”, “runs”: 1 }
Genuine boolean. Single run. Correct value arriving at the node. And the Switch still sent it false.
The actual cause
n8n renders {{ }} expression output as text. So a real boolean true arrives at the operator as the string “true” — and if there’s any trailing whitespace or a newline in the parameter field after the closing }} (very easy to introduce when pasting), you get "true ".
The strict Boolean operator gets "true ", which is not a boolean, and the comparison fails. Quietly, in the false direction.
Eventually the node surfaced it verbatim: Wrong type: 'true ’ is a string but was expecting a boolean
Why the obvious fixes didn’t work
I tried converting inside the expression:
{{ String($(‘Extract Response’).first().json.shouldEscalate).trim().toBoolean() }}
Still failed. The whitespace isn’t inside the expression — it’s introduced after }} during n8n’s own rendering. Nothing you do inside the braces can reach it.
Toggling “Convert types where required” changes the failure mode rather than fixing it. In my case one setting routed everything true and the other routed everything false. Both are wrong; one is just louder.
The fix that works
Stop comparing the boolean. Emit an explicit keyword and match on text:
value: {{ $(‘Extract Response’).first().json.shouldEscalate ? “ESCALATE” : “NORMAL” }}
operator: String → contains
match: ESCALATE
convert types: off
"ESCALATE " still contains “ESCALATE”. "NORMAL " doesn’t contain it either way. Trailing whitespace becomes harmless instead of fatal.
Verified in both directions on the same build — condition true routes true, condition false routes false. Worth doing both; testing only the direction you expect is half a test, and this bug is specifically capable of passing one direction while failing the other.
The general version
If a Switch or IF node is making a decision that actually matters — alerting, escalation, compliance gating, anything where silently taking the wrong branch is expensive — don’t route it on a raw boolean. Render a sentinel string and match on it.
“The failure mode here isn’t an error. It’s a green execution that did the wrong thing.”