Two workflows, one shared assumption
The split was reasonable: rendering is slow and can fail, uploading is quick, so the two live in separate workflows joined by a render queue in Google Sheets. The Creator's only protection against duplicates was its own read of the topic queue, filtered to rows whose status is still unused. That status was flipped by the Publisher, after a successful upload.
How the same topic got rendered twice
The Creator can't see the render queue. If the Publisher had not yet uploaded the previous ready row, the topic's status hadn't changed, so the next Creator run read the same row and rendered it again. It happened twice, each time about one Creator interval after the first render.
The fix: claim at pickup
A new Google Sheets update node, Mark Topic Claimed, sits between the check that a topic is available and the node that starts the render. It sets the status the instant the Creator picks the row. The Publisher's own mark-used step stays, and now rewrites the same value harmlessly.
The other half matters just as much. A Reset Topic On Failure node, fanned out from the branch that runs when every render attempt has failed, puts the status back to unused. Without it, a topic that never produced a video stays claimed forever. That branch also sends a Telegram alert, and putting the two side by side has a catch: on one occasion the alert node crashed on an HTML formatting error and the reset never ran, leaving two topics stuck as used with no video. The Telegram parse-mode article covers that crash.
Cleaning up what had already shipped
The older upload of each pair was left alone. The newer duplicate was renamed through the API with a title, description and tags in the channel's normal style, and all 18 videos were re-read to confirm every title and description was unique. I only found the duplicates because I audited the channel through the API; the execution log showed nothing wrong at any point.
The rule
Any producer and publisher pair that share a queue needs the producer to claim its item on pickup rather than rely on the publisher's later mark. It is a different bug from matching rows on a non-unique column: that one marks the wrong row after publishing, while this one never marks the right row soon enough.
The status values that make claim-at-pickup work
A queue with only two states, unused and used, can't express "someone is working on this". Adding a third value fixes that and makes the sheet readable at a glance:
| Status | Set by | Meaning |
|---|---|---|
| unused | Whoever fills the queue, or the reset branch | Free to be picked |
| claimed | Creator, immediately after picking | A render is in progress or has finished |
| used | Publisher, after the upload | The topic is live and must never be picked again |
The Creator filters strictly on unused, so a claimed topic is invisible to it. The failure branch puts a claimed topic back to unused only when no video was produced.
What the Creator's first nodes look like now
Schedule Trigger
-> Read Topic Queue (Sheets, filter status = unused)
-> IF - Topic Available
true -> Mark Topic Claimed (Sheets update, key = row_id, status = claimed)
-> Start Render ...
false -> Telegram: queue empty
Two details matter in the update node. It must match on a unique key such as row_id, never on the topic text, and anything downstream that needs the topic must read it from the node that read the queue, not from $json. A Sheets update only returns the columns it wrote, which is the subject of a separate article on this site.
Stuck claims, and how to catch them
Claiming early creates a new failure mode: a run that dies in a way the failure branch never sees, for example when n8n restarts mid-render, leaves the topic claimed with no video. It does not create duplicates, which is the important part, but the topic is lost until someone notices.
A cheap guard is a weekly check that lists topics that have been claimed for longer than a few days without a matching row in the render queue, and sends them to Telegram. In my case the more common cause of a stuck claim was the alert node crashing before the reset node ran, so the reset now runs first and the alert second.
When a lock in Sheets is not enough
Claim-at-pickup works here because only one Creator workflow reads the queue and its runs are hours apart. If several workflows, or several parallel executions of one workflow, read the same queue within seconds, two of them can still read the same row before either has written claimed. At that point a real database with an atomic update (for example an UPDATE ... WHERE status = 'unused' that returns the row it changed) is the right tool, not a spreadsheet.
Frequently asked questions
Why not mark the topic used only after a successful publish?
Because the producer can run again before the publisher has finished. Until the publisher writes its status, the topic still looks free, so the next producer run picks it again and creates a duplicate.
What happens to a claimed topic if the render fails?
A reset node on the failure branch sets it back to unused. Run the reset before any alert node, so an alert that fails can't block it.
Is Google Sheets safe as a queue for parallel workers?
Not for workers that read within seconds of each other. Sheets has no atomic claim operation, so use a database when several executions can pick at the same time.
How did you find the duplicate videos?
By reading the channel back through the YouTube Data API and comparing titles and descriptions. The n8n execution log showed nothing wrong.
n8n Automation Hub