What was actually happening before the fix
The search step only ever tried the first result that wasn't already a duplicate, fired it off to the processing workflow as a webhook call it didn't wait on, and then exited — no matter what happened next. The processing workflow, on the other end, had zero error handling around its download step. A blocked download — a license restriction, a copyright claim, anything the downloader couldn't get past — crashed the entire execution and routed to a generic error-alert workflow. Since the caller had already exited by the time that alert fired, nothing else got tried. One blocked video meant zero videos published that day, and the only sign anything had gone wrong was a generic alert that didn't say "try the next one," because nothing was listening for that instruction in the first place.
Splitting the webhook into two real outcomes
The processing workflow's webhook was set to fire-and-forget. I changed it to wait for a response, then wrapped the actual download step so a failure produces a distinct output branch instead of crashing the run — a blocked download now returns a small JSON payload saying so, on a separate path from the one that continues into the rest of the pipeline. The success path still fires its response immediately so the caller isn't blocked on the full render — it just needed the failure branch to exist so a rejection had somewhere to go besides "unhandled."
Making the caller actually listen
On the search side, the fix was to stop treating that webhook call as fire-and-forget: parse the response, check whether it reports success, and only stop there if it does. A failure routes to a skip notification and loops back into the search results to try the next candidate — instead of the workflow just being done after one attempt regardless of the outcome. A network hiccup on that call doesn't crash the loop either; it's set to continue past connection errors rather than take the whole run down over something that has nothing to do with the actual video.
Why this matters more than it looks like
None of this is complicated logic — it's a webhook that reports its real outcome and a caller that checks before giving up. The part that's easy to skip is deciding, up front, what "failure" looks like for a step that's allowed to fail sometimes (a blocked download is not the same category of problem as a broken workflow) and making sure that distinction actually reaches the node that decides whether to retry. Most of the times I've seen a daily automation quietly produce nothing, it wasn't because everything broke — it was because the one thing that failed was treated the same as the whole pipeline failing.
The processing side, as it is wired today
I re-read the current export of the processing workflow described above. The parts that matter for the retry logic are these:
- The webhook waits for an answer. Its response mode is set to reply from a Respond to Webhook node, so the caller gets a real result instead of an acknowledgement.
- The download node has an error output. The yt-dlp step is an SSH command node with
onError: continueErrorOutput. Its normal output goes on to audio extraction and to the success response; its error output goes to a failure response. The failure response is JSON withsuccess: false, thevideo_idand the reasondownload_failed_or_blocked, which is the small payload the caller can test. - The command is safe to run twice. It first checks whether the target MP4 already exists and, if so, prints that it skipped the download. Otherwise it runs yt-dlp with browser cookies from a dedicated profile, three retries, a 30-second socket timeout,
--no-playlist, and an MP4-preferring format selection.
A caveat about the caller
When I searched every workflow on my instance for a caller of this webhook path, none turned up, so the candidate-search loop described in this article is not running from n8n today. The processing half above is still live, and the pattern (report the real outcome, let the caller decide what to do next) is unchanged. If you copy it, test the failure branch on purpose with a URL you know will be refused, and confirm that the caller receives success: false rather than a timeout.
What counts as a failure
The design decision that matters most is choosing which failures are allowed. A blocked or removed video is an expected outcome and gets its own branch. A crashed workflow, an expired credential or a full disk is not, and should still reach your alerting. If both end up in the same branch, the retry loop hides real problems behind normal skips.
n8n Automation Hub