YouTube Node: Create Playlist throws 400 INVALID_ARGUMENT in Production, but works perfectly in Manual UI Test

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)

Manual UI Test can hide the mismatch because it runs against the item currently selected in the editor. In the failed production execution, the only input that matters is the input panel directly on the YouTube node, not the Match node upstream.

Check that panel for the item the YouTube node actually received. If artist_name / album_name are missing there or coming from a paired item, freeze the title in a Set/Edit Fields node right before YouTube and map the playlist title from that new field. If the field is already a plain string there, post that one redacted YouTube-node input item plus the full 400 response body.

Hey, thank you for your answer. I restructured the workflow to use the Merge node in combine mode (as seen in attachment) to ensure the binary data and JSON metadata stay perfectly synced throughout the pipeline.

So, everything is seemed perfect until executing here. Regarding with your answer, I thought that I can apply the same situation for uploading videos. However, it didn’t work. I also changed my merge node mode to combine. Unfortunately, it didn’t work. You can find the latest status of my workflow below.

Thanks. The screenshots still do not show the failed YouTube node input, so Merge mode is one step upstream from the thing that decides this.

Open the failed production execution, click the YouTube node itself, and copy one redacted input JSON item from that node plus the full 400 response body. If that node already receives a plain playlist title string, the next check is the exact payload YouTube rejects; if it does not, the fix is immediately before YouTube.

You’re absolutely right. The Merge node fix solved the Video Upload issue, but the initial Playlist 400 error was indeed happening upstream before the Merge node even executed.

Since the workflow is now running smoothly in production after I rebuilt that section, I went back to the old failed execution logs to grab exactly what the YouTube Playlist node received at its input panel during the crash, just to satisfy the root cause analysis.

Here is the redacted JSON from the INPUT panel of the failed YouTube Playlist node:

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

And the expression on the Title field was:
{{ $json.artist_name }} - {{ $json.album_name }} (Full Release)

Even though it visually resolved fine in the UI preview, in that specific production run, n8n was losing the paired item context because the input stream was coming directly from a multi-item binary node without explicit item index anchoring (like using .item.json). So YouTube was likely receiving an empty or malformed payload string under the hood.

Now, I have some questions:

1- You were said that:

“Manual UI Test can hide the mismatch because it runs against the item currently selected in the editor.”

So, how can I go to test it? Is it always in production or is there any special environment?

2- When I started to run workflow (published) it starts perfectly, but some errors start to occur without any reason. For instance, expression related etc. Why does it happen even there’s no problem?

Thank you.

Good detective work digging through the old execution logs.

On question 1 — testing production behavior without a live trigger:
The cleanest way is to use the “Test workflow” button (the play icon at the top of the canvas when the workflow is active/published). This runs the workflow exactly as production would, using the actual trigger — for webhook-based workflows, it puts n8n into a listening state for one real incoming call. You can also manually POST a test payload to your webhook URL from a tool like Postman or curl while in this mode. That gives you a real production execution you can inspect, not a simulated one.

On question 2 — random expression errors after publishing:
This is almost always item context breaking somewhere upstream. When a workflow runs live, each node only sees items passed to it from the direct parent — if you have a branch or a node that returns a different shape than what you tested with, expressions that worked in manual mode start throwing errors. The fix is to add a “Set” (Edit Fields) node right before any node that uses expressions from earlier in the chain, explicitly remapping the fields you need. That way the data shape is always explicit and not dependent on what happened to be selected in the editor when you tested.

Hey, just an update before moving this to production, the issue is still persisting. The Merge node didn’t work out, and I’ve tried a few different ways to pass binary data through the Code node but still couldn’t get it to run properly. Thanks for the help anyway!

Match code:

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;