YouTube Node: Create Playlist lanza error 400 INVALID_ARGUMENT en producción, pero funciona perfectamente en prueba manual UI

Describe the problem/error/question

Hey everyone, I’m hitting a really strange issue with the YouTube Node while trying to dynamically create a playlist. It works flawlessly when I manually click “Execute Step” inside the n8n UI, but the moment the workflow runs in production (automated/live trigger), it completely fails with a 400 Bad Request error.

Node Configuration & Setup

  • Resource: Playlist

  • Operation: Create

  • Title Expression: {{ $json.artist_name }} - {{ $json.album_name }} (Full Release)

  • Settings: Execute Once is turned ON (to prevent looping over multiple incoming items).

The Behavior

  1. In Manual UI Test: The expression resolves perfectly to a clean string: „Zedd, Bright Lights - Follow You Down - Keys N Krates Remix„ (around 77 characters, well within YouTube’s 150-char limit). The input node (Match) passes both JSON fields and binary info data.

  2. In Production Run: The exact same data structure comes in, the title string should be identical, but the node throws the following API error.

What I’ve Already Checked/Tried:

  • Google Cloud Console App: Set to Production mode (not in Testing/Sandbox).

  • Privacy Status: Tried both Public and Private/Unlisted. Same error on both in production.

  • Character Limits: The title is only ~77 characters.

  • Special Characters: Tried using a strict regex to strip everything except basic alphanumeric characters, but the 400 error persists.

  • Channel Presence: The Google account has an active, fully created YouTube channel, and other nodes (like video upload) work fine.

Why does n8n behave differently between manual execution and an automated production run when parsing or sending this payload to YouTube? Any ideas on how to debug or bypass this?

Please share your workflow

Share the output returned by the last node

Information on your n8n setup

  • n8n version: v2.23.2
  • Database (default: SQLite): default
  • n8n EXECUTIONS_PROCESS setting (default: own, main): main
  • Running n8n via (Docker, npm, n8n cloud, desktop app): Docker
  • Operating system: Raspberry Pi OS 64-bit Debian 13 trixie (aarch64)

La prueba manual de la interfaz de usuario puede ocultar la discrepancia porque se ejecuta contra el elemento seleccionado actualmente en el editor. En la ejecución de producción fallida, la única entrada que importa es el panel de entrada directamente en el nodo de YouTube, no el nodo Match aguas arriba.

Verifica ese panel para el elemento que el nodo de YouTube realmente recibió. Si artist_name / album_name faltan ahí o provienen de un elemento emparejado, congela el título en un nodo Set/Edit Fields justo antes de YouTube y asigna el título de la lista de reproducción desde ese nuevo campo. Si el campo ya es una cadena simple ahí, publica ese elemento de entrada del nodo de YouTube redactado más el cuerpo de respuesta completo de 400.

Hola, gracias por tu respuesta. Reestructuré el flujo de trabajo para usar el nodo Merge en modo combine (como se ve en el archivo adjunto) para asegurar que los datos binarios y los metadatos JSON se mantengan perfectamente sincronizados a lo largo del pipeline.

Entonces, todo parecía perfecto hasta ejecutar aquí. Con respecto a tu respuesta, pensé que podría aplicar la misma situación para cargar videos. Sin embargo, no funcionó. También cambié el modo de mi nodo merge a combine. Desafortunadamente, no funcionó. Puedes encontrar el estado más reciente de mi flujo de trabajo a continuación.

Gracias. Las capturas de pantalla aún no muestran la entrada fallida del nodo YouTube, así que Merge mode es un paso anterior a la cosa que decide esto.

Abre la ejecución de producción fallida, haz clic en el nodo YouTube en sí, y copia un elemento JSON de entrada redactado de ese nodo más el cuerpo completo de la respuesta 400. Si ese nodo ya recibe una cadena de título de lista de reproducción simple, el siguiente paso es verificar la carga exacta que YouTube rechaza; si no es así, la solución está inmediatamente antes de YouTube.

Tienes toda la razón. La corrección del nodo Merge resolvió el problema de Video Upload, pero el error inicial 400 de Playlist estaba ocurriendo efectivamente aguas arriba antes de que el nodo Merge ni siquiera se ejecutara.

Como el workflow ahora funciona sin problemas en producción después de que reconstruí esa sección, volví a los registros de ejecución fallidos anteriores para obtener exactamente lo que el nodo YouTube Playlist recibió en su panel de entrada durante el fallo, solo para satisfacer el análisis de causa raíz.

Aquí está el JSON editado del panel INPUT del nodo YouTube Playlist que falló:

{
"json": {
"album_name": "Follow You Down - Keys N Krates Remix",
"artist_name": "Zedd, Bright Lights"
}
}

Y la expresión en el campo Title era:
{{ $json.artist_name }} - {{ $json.album_name }} (Full Release)

Aunque se resolvió visualmente bien en la vista previa de la UI, en esa ejecución de producción específica, n8n estaba perdiendo el contexto de elemento emparejado porque el flujo de entrada provenía directamente de un nodo binario de múltiples elementos sin anclaje explícito de índice de elemento (como usar .item.json). Entonces YouTube probablemente estaba recibiendo una cadena de carga vacía o malformada bajo el capó.

Ahora, tengo algunas preguntas:

1- Dijiste que:

«Las pruebas manuales de UI pueden ocultar la discrepancia porque se ejecutan contra el elemento actualmente seleccionado en el editor».

¿Entonces, cómo puedo ir a probarlo? ¿Siempre es en producción o hay algún entorno especial?

2- Cuando comencé a ejecutar el workflow (publicado) comienza perfectamente, pero algunos errores comienzan a ocurrir sin razón aparente. Por ejemplo, relacionados con expresiones, etc. ¿Por qué sucede incluso si no hay problema?

Gracias.

Buen trabajo de detective excavando en los antiguos registros de ejecución.

En la pregunta 1 — probar el comportamiento de producción sin un disparador en vivo:
La forma más limpia es usar el botón “Test workflow” (el icono de reproducción en la parte superior del canvas cuando el workflow está activo/publicado). Esto ejecuta el workflow exactamente como lo haría producción, utilizando el disparador real — para workflows basados en webhooks, pone a n8n en estado de escucha para una llamada entrante real. También puedes hacer POST manualmente de un payload de prueba a tu URL de webhook desde una herramienta como Postman o curl mientras estés en este modo. Esto te da una ejecución de producción real que puedes inspeccionar, no una simulada.

En la pregunta 2 — errores de expresión aleatorios después de publicar:
Esto casi siempre es porque el contexto de items se rompe en algún lugar hacia arriba. Cuando un workflow se ejecuta en vivo, cada nodo solo ve los items que le pasa el padre directo — si tienes una rama o un nodo que devuelve una forma diferente a la que probaste, las expresiones que funcionaban en modo manual comienzan a lanzar errores. La solución es agregar un nodo “Set” (Edit Fields) justo antes de cualquier nodo que use expresiones de antes en la cadena, remapeando explícitamente los campos que necesitas. De esa manera la forma de los datos siempre es explícita y no depende de lo que haya sido seleccionado en el editor cuando probaste.

Hey, solo una actualización antes de pasar esto a producción, el problema sigue presente. El nodo Merge no funcionó, e intenté varias formas diferentes de pasar datos binarios a través del nodo Code pero aún no logré que funcione correctamente. ¡Gracias de todas formas por la ayuda!

Código de coincidencia:

const jsonData = $("Multi-track splitter").all();
const videoFiles = $input.all();

let matchedItems = [];
const normalize = (str) => str ? str.toLowerCase().replace(/[^a-z0-9]/g, '') : '';

for (const item of jsonData) {
    const song = item.json;
    const safeSongName = normalize(song.song_name);
    
    const match = videoFiles.find(v => {
        const fileName = v.json.fileName || '';
        const cleanFileName = fileName.replace('.mp4', '').replace(/_\d+$/, '');
        const safeFileName = normalize(cleanFileName);
        
        return safeFileName.includes(safeSongName) || safeSongName.includes(safeFileName);
    });

    if (match) {
        matchedItems.push({
            json: {
                ...song,
                yt_description: `Artist: ${song.artist_name}\n\nAlbum: ${song.album_name}\n\nRelease Date: ${song.release_date}\n\n${song.company_name}\n${song.publisher_name}\n\nNOT auto-generated by YouTube.`,
                yt_title: `${song.artist_name} - ${song.song_name}`
            },
          
            binary: match.binary 
        });
    }
}

return matchedItems;