N8n: el nodo Switch silenciosamente enruta false al comparar un booleano — "Tipo incorrecto: 'true ' es una cadena pero se esperaba un booleano"

Publico esto porque me costó un par de horas y falla silenciosamente, que es la peor combinación. Si enrutas algo importante a través de un nodo IF o Switch en un booleano, esto vale diez minutos de tu tiempo.

Hay algunos problemas abiertos y temas del foro que describen este comportamiento sin una causa raíz adjunta. Esto es lo que encontré.

El síntoma

Tenía un nodo Switch decidiendo si enviar una alerta. La salida del nodo anterior se veía así:

{ “shouldEscalate”: true }

Condición del Switch: value:

{{ $(‘Extract Response’).first().json.shouldEscalate }}
operator: Boolean → is true
Cada ejecución se enrutó como false. Sin error. La ejecución se reportó como exitosa. La alerta simplemente nunca se disparó.

Qué no era

Perdí tiempo en hipótesis incorrectas primero, así que aquí están para ahorrarte el viaje:

•	No era .first() vs .item — lo verifiqué
•	No era un problema de multi-run/item-lineage
•	No era el nodo anterior produciendo el valor incorrecto

Probé lo último soltando un nodo Set directamente antes del Switch que emitiera el valor sin procesar más su tipo: { “val”: true, “type”: “boolean”, “runs”: 1 }
Booleano genuino. Una sola ejecución. Valor correcto llegando al nodo. Y el Switch aún lo envió como false.

La causa real

n8n renderiza la salida de la expresión {{ }} como texto. Así que un booleano verdadero real llega al operador como la cadena “true” — y si hay algún espacio en blanco final o una nueva línea en el campo de parámetro después de los }} de cierre (muy fácil de introducir al pegar), obtienes "true ".

El operador Boolean estricto obtiene "true ", que no es un booleano, y la comparación falla. Silenciosamente, en la dirección false.

Eventualmente el nodo lo mostró literalmente: Wrong type: 'true ’ is a string but was expecting a boolean

Por qué los arreglos obvios no funcionaron

Intenté convertir dentro de la expresión:
{{ String($(‘Extract Response’).first().json.shouldEscalate).trim().toBoolean() }}

Aún falló. El espacio en blanco no está dentro de la expresión — es introducido después de }} durante el propio renderizado de n8n. Nada de lo que hagas dentro de las llaves puede alcanzarlo.

Alternar “Convert types where required” cambia el modo de fallo en lugar de arreglarlo. En mi caso, una configuración enrutaba todo como true y la otra enrutaba todo como false. Ambas son incorrectas; una es simplemente más ruidosa.

El arreglo que funciona
Deja de comparar el booleano. Emite una palabra clave explícita y coincide con texto:
value: {{ $(‘Extract Response’).first().json.shouldEscalate ? “ESCALATE” : “NORMAL” }}
operator: String → contains
match: ESCALATE
convert types: off

"ESCALATE " aún contiene “ESCALATE”. "NORMAL " no lo contiene de ninguna manera. El espacio en blanco final se vuelve inofensivo en lugar de fatal.

Verificado en ambas direcciones en la misma compilación — condición true enruta true, condición false enruta false. Vale la pena hacerlo en ambas direcciones; probar solo la dirección que esperas es medio test, y este bug es específicamente capaz de pasar una dirección mientras falla en la otra.

La versión general

Si un nodo Switch o IF está tomando una decisión que realmente importa — alerta, escalación, puerta de cumplimiento, cualquier cosa donde silenciosamente tomar la rama incorrecta es costoso — no la enrutes en un booleano sin procesar. Renderiza una cadena centinela y coincide con ella.

“El modo de fallo aquí no es un error. Es una ejecución verde que hizo lo incorrecto.”

Debugging sólido, este es brutal porque falla silenciosamente y muestra verde. Tu rastreo de causa raíz: el valor se renderiza a texto por el motor de expresiones, y cualquier espacio en blanco extraño después de }} rompe los operadores de tipo estricto. Recortar dentro de las llaves no puede arreglarlo, ya que el espacio en blanco aparece después de renderizar, no antes.

Tu corrección de cadena centinela es la opción correcta es la forma más robusta de enrutar en un booleano una vez que ha pasado a través de una expresión. Una cosa que vale la pena agregar: también puedes intentar forzar el tipo en un nodo Set ascendente (usando el selector de tipo, no solo la expresión) antes del Switch a veces eso se enruta a través de la coerción interna de n8n en lugar de la ruta de representación de texto. No está garantizado que lo evite en todas las versiones, pero es barato de probar.

Buena observación este es el tipo de bug que cuesta mucho más que los diez minutos que estás pidiendo a la gente que inviertan.

¡Hola @Tonylw14 Bienvenido!
La coerción no es incondicional, solo ocurre cuando la expresión se encuentra dentro de una cadena. Un campo que contiene nada más que {{ ... }} devuelve el valor nativo, y un único espacio después de }} convierte el campo en una plantilla de cadena, de donde proviene "true ". El enrutamiento booleano funciona una vez que el campo contiene la expresión y nada más.
Haz clic en el campo de valor, selecciona todo, vuelve a escribir la expresión y no dejes espacio ni salto de línea después de }}. Para verificar, copia el nodo Switch y pégalo en un editor de texto: el parámetro debe leer ={{ $('Extract Response').first().json.shouldEscalate }} y terminar en las llaves de cierre.
n8n ha confirmado esto como comportamiento previsto; una expresión incrustada en una cadena se coerciona a una cadena incluso cuando la cadena circundante es solo espacios en blanco:

Una pequeña adición para cualquiera que se encuentre con esto: duele más cuando copias y pegas expresiones desde Slack/documentos/ChatGPT, porque insertan un espacio final o un espacio de no separación que es casi invisible. Si una ruta booleana falla silenciosamente, como dice Anshul, escribir la expresión a mano es mucho más rápido que intentar encontrar el espacio en blanco mirando fijamente la expresión.

@Anshul_Namdev @ShawnWilliams — gracias a ambos. Es un mecanismo más preciso que el que escribí, y es la parte que tenía mal. Dije que la coerción era incondicional; no lo es. Un campo que contiene solo la expresión devuelve el valor nativo, y cualquier otra cosa — incluyendo un único espacio al final — la convierte en una plantilla de cadena, que es lo que produjo **'true. Eso también explica por qué recortar dentro de las llaves no hizo nada: la coerción ocurre cuando se representa la plantilla, después de que la expresión ya ha sido evaluada.

@ShawnWilliams eso es exactamente. Pegué estas expresiones de un chat mientras estaba construyendo. De ahora en adelante escribiré a mano.

Actualizando el post con el mecanismo corregido y el enlace del problema.

Salman_Mehboob buena observación al forzar el tipo en un nodo Set ascendente. No había probado ese camino, lo probaré y te reportaré.

Estoy manteniendo la cadena centinela en el enrutamiento crítico de seguridad, por una razón relacionada con el modo de fallo más que con el mecanismo: un espacio al final invisible no es visible en la UI, y si se reintroduce más tarde la rama falla silenciosamente en una ejecución verde. Una coincidencia de contiene en una palabra clave explícita es inmune a toda esa clase de problemas. Pero el campo limpio es la solución real y debería venir primero.