N8n: nó Switch roteia silenciosamente para false ao comparar um booleano — "Tipo incorreto: 'true ' é uma string mas esperava um booleano"

Postando isso porque me custou algumas horas e falha silenciosamente, que é a pior combinação. Se você rotear algo importante através de um nó IF ou Switch em um booleano, vale dez minutos do seu tempo.

Existem alguns problemas abertos e tópicos de fórum descrevendo esse comportamento sem uma causa raiz anexada. Isso é o que descobri.

O sintoma

Eu tinha um nó Switch decidindo se deveria enviar um alerta. A saída do nó upstream parecia assim:

{ “shouldEscalate”: true }

Condição do Switch: value:

{{ $(‘Extract Response’).first().json.shouldEscalate }}
operador: Boolean → is true
Toda execução roteiou false. Nenhum erro. Execução relatada como sucesso. O alerta simplesmente nunca disparou.

O que não era

Eu perdi tempo com as hipóteses erradas primeiro, então aqui estão para economizar seu tempo:

•	Não .first() vs .item — Eu verifiquei
•	Não é um problema de multi-run/item-lineage
•	Não é o nó upstream produzindo o valor errado

Provei o último deixando um nó Set diretamente antes do Switch que emitiu o valor bruto mais seu tipo: { “val”: true, “type”: “boolean”, “runs”: 1 }
Booleano genuíno. Execução única. Valor correto chegando no nó. E o Switch ainda assim o enviou como false.

A causa real

n8n renderiza a saída da expressão {{ }} como texto. Portanto, um booleano true real chega no operador como a string “true” — e se houver espaço em branco ou uma quebra de linha no campo de parâmetro após o }} de fechamento (muito fácil de introduzir ao colar), você obtém "true ".

O operador Boolean rigoroso obtém "true ", que não é um booleano, e a comparação falha. Silenciosamente, na direção false.

Eventualmente o nó o apresentou literalmente: Wrong type: 'true ’ is a string but was expecting a boolean

Por que as correções óbvias não funcionaram

Tentei converter dentro da expressão:
{{ String($(‘Extract Response’).first().json.shouldEscalate).trim().toBoolean() }}

Ainda falhou. O espaço em branco não está dentro da expressão — é introduzido após }} durante a própria renderização do n8n. Nada que você faça dentro das chaves pode alcançá-lo.

Alternar “Convert types where required” muda o modo de falha em vez de corrigi-lo. No meu caso, uma configuração roteiou tudo como true e a outra roteiou tudo como false. Ambas estão erradas; uma é apenas mais aparente.

A correção que funciona
Deixe de comparar o booleano. Emita uma palavra-chave explícita e corresponda ao texto:
value: {{ $(‘Extract Response’).first().json.shouldEscalate ? “ESCALATE” : “NORMAL” }}
operador: String → contains
match: ESCALATE
convert types: off

"ESCALATE " ainda contém “ESCALATE”. "NORMAL " não contém em nenhum dos casos. O espaço em branco à direita torna-se inofensivo em vez de fatal.

Verificado em ambas as direções no mesmo build — condição true roteia true, condição false roteia false. Vale fazer ambas; testar apenas a direção que você espera é meio teste, e esse bug é especificamente capaz de passar em uma direção enquanto falha na outra.

A versão geral

Se um nó Switch ou IF está tomando uma decisão que realmente importa — alertar, escalar, gating de conformidade, qualquer coisa onde silenciosamente tomar o ramo errado é caro — não a roteie em um booleano bruto. Renderize uma string sentinela e corresponda a ela.

„O modo de falha aqui não é um erro. É uma execução verde que fez a coisa errada.

Depuração sólida, esse é brutal porque falha silenciosamente e mostra verde. Seu rastreamento de causa raiz: o valor é renderizado para texto pelo mecanismo de expressão, e qualquer espaço em branco após }} quebra operadores de tipo estrito. Aparar dentro das chaves não pode corrigir, já que o espaço em branco aparece após a renderização, não antes.

Sua correção de cadeia sentinela é a escolha certa — é a forma mais robusta de rotear em um booleano depois que passa por uma expressão. Uma coisa que vale a pena adicionar: você também pode tentar forçar o tipo em um nó Set upstream (usando o seletor de tipo, não apenas a expressão) antes do Switch — às vezes isso roteia pelo caminho de coerção interna do n8n em vez do caminho de renderização de texto. Não é garantido que escape em todas as versões, mas é barato testar.

Boa observação — esse é o tipo de bug que custa muito mais do que os dez minutos que você está pedindo às pessoas para gastar.

Oi @Tonylw14 Bem-vindo!
A coerção não é incondicional, ela só acontece quando a expressão fica dentro de uma string. Um campo que contém nada além de {{ ... }} retorna o valor nativo, e um espaço único após }} transforma o campo em um template de string, de onde vem "true ". O roteamento booleano funciona quando o campo contém a expressão e nada mais.
Clique no campo de valor, selecione tudo, redigite a expressão e não deixe espaço ou quebra de linha após }}. Para verificar, copie o nó Switch e cole em um editor de texto: o parâmetro deve ler ={{ $('Extract Response').first().json.shouldEscalate }} e terminar nas chaves de fechamento.
O n8n confirmou isso como comportamento intencional, uma expressão incorporada em uma string é coagida a uma string mesmo quando a string circundante é apenas espaço em branco:

Uma pequena dica para quem se depara com isso: o problema é mais grave quando você copia e cola expressões do Slack/docs/ChatGPT, porque eles inserem um espaço à direita ou um espaço não-quebrável que é quase invisível. Se uma rota booleana falha silenciosamente, como o Anshul diz, escrever a expressão manualmente é muito mais rápido do que tentar encontrar o espaço em branco olhando para a expressão.

@Anshul_Namdev @ShawnWilliams — obrigado a ambos. Esse é um mecanismo mais preciso do que o que escrevi, e é exatamente a parte que estava errada. Eu disse que a coerção era incondicional; não é. Um campo contendo apenas a expressão retorna o valor nativo, e qualquer coisa — incluindo um único espaço final — a torna um modelo de string, que é o que produziu 'true. Isso também explica por que aparar dentro das chaves não fez nada: a coerção acontece quando o modelo é renderizado, depois que a expressão já foi avaliada.

@ShawnWilliams é exatamente isso. Colei essas expressões de um chat enquanto construía. A partir de agora vou redigitar manualmente.

Atualizando o post com o mecanismo corrigido e o link do issue.

Salman_Mehboob boa observação ao forçar o tipo em um nó Set upstream. Não tinha tentado esse caminho, vou testar e reportar.

Estou mantendo a string sentinela em roteamento crítico para segurança, por uma razão sobre o modo de falha em vez do mecanismo: um espaço final invisível não é visível na interface, e se um for reintroduzido depois o branch falha silenciosamente com uma execução verde. Uma correspondência contém em uma palavra-chave explícita é imune a toda essa classe. Mas o campo limpo é a correção real e deve vir em primeiro lugar.