The queue said the thumbnail existed. The disk said otherwise.
The pipeline renders a video, runs a caption pass, then calls a small Python script over an SSH node to build a thumbnail from the video's topic, and finally appends a row to a Google Sheets render queue. That row has a thumbnail_path column. The column is written from a string the workflow builds, not from checking that the file exists, so it was filled in on every row whether or not a thumbnail had been produced.
I only noticed because I listed the task folders by hand after a batch of test renders. One long video and four shorts had their video files and no thumbnail.png at all. The same command had been copied from one channel's workflow into a second channel's, so both pipelines had the problem.
Bug one: Buffer inside an expression field
Topics can contain quotes and umlauts, so the topic is passed to the script base64-encoded, which keeps the shell command safe. The encoding was done inline, in the SSH node's command field:
python3 make_thumbnail.py {{ Buffer.from($json.topic).toString('base64') }} /path/to/thumbnail.png
That expression evaluates to an empty string. I re-checked it on a throwaway workflow so this wasn't just a memory:
typeof Buffer -> "object"
Set node, {{ Buffer.from('hallo').toString('base64') }} -> empty
SSH node command, same expression -> empty
Code node, Buffer.from('hallo').toString('base64') -> "aGFsbG8="
The global exists, but the encoded string never comes out of an inline expression. The identical call works inside a Code node. I don't know which layer of n8n's expression handling drops it, so I treat it as a rule: no Node.js encoding calls in parameter fields, only in Code nodes.
Why the error said "Incorrect padding"
With the expression empty, the command became python3 make_thumbnail.py /path/to/thumbnail.png. The shell collapses the empty gap, so the script's first argument was the output path, which it then tried to base64-decode as the topic. The traceback had nothing to do with an encoding library being missing:
TOPIC = base64.b64decode(sys.argv[1]).decode("utf-8")
binascii.Error: Incorrect padding
n8n's SSH node returns the exit code and stderr as data rather than raising an error, which is why the execution stayed green. The article on exit code 124 covers that behaviour and why a dedicated exit-code check node is worth adding.
Bug two: the fix that failed on the next scheduled run
The obvious fix was to compute the base64 in an upstream Code node, where Buffer works, and reference the new field in the SSH command as {{ $json.topic_b64 }}. That went into every affected workflow. A scheduled run of the second pipeline on September 17 then produced exactly the same traceback, even though the workflow snapshot stored with that execution contained the new field.
The reason is what $json means: the item arriving at the current node, which is the output of the node right before it. In this workflow the thumbnail node follows another SSH node (the caption burn), and an SSH node's output is only code, signal, stdout and stderr. Neither topic_b64 nor the task_id used later in the same command existed there. A chain of Code node, SSH node, SSH node on a scratch workflow shows it directly:
{{ $json.topic_b64 }} -> ""
{{ $('Make Field').item.json.topic_b64 }} -> "aGFsbG8="
It's the same family of problem as a Sheets update node sitting between a value and the node that reads it, with a different node in the middle.
The fix
Encode in a Code node, then reference that node by name in everything downstream:
// Code node "Parse Task Status"
const topic_b64 = Buffer.from(prev.topic || '', 'utf-8').toString('base64');
return [{ json: { ...prev, topic_b64 } }];
// SSH node command
python3 make_thumbnail.py {{ $('Parse Task Status').item.json.topic_b64 }} /path/{{ $('Parse Task Status').item.json.task_id }}/thumbnail.png
Two habits would have caught both bugs earlier. Search the instance for Buffer. in any node that isn't a Code node. And write the queue row's file path only after a check that the file exists, for example a follow-up command ending in test -s thumbnail.png whose exit code is checked, so a missing file stops the run instead of surfacing at publish time.
n8n Automation Hub