A content generation queue fails in practice when its statuses are too vague.
If "pending" means scheduled, ready, and unclaimed all at once, workers will race each other. If "failed" means transient API error, invalid brief, and blocked by policy, retry logic becomes guesswork. If "published" is used both for "content exists" and "content is live on the site," the queue stops being a source of truth and turns into a pile of ambiguous labels.
The fix is not more statuses. It is a small set of states with explicit transitions and a clear rule for what each transition means.
Start by separating lifecycle from outcome
A content generation queue is not the whole publishing system. It usually sits between topic selection and publication, so its statuses should describe work in motion, not every possible business condition.
A practical set is:
pendingreservedgeneratedpublishedfailedrepairskippedarchived
Those states are enough if you define them strictly.
pendingmeans the item is eligible to be worked on but has not been claimed.reservedmeans a worker has claimed it and is actively processing it.generatedmeans the content payload exists and passed whatever generation-step checks you require, but it has not yet been published.publishedmeans the content is live in the destination system.failedmeans the current attempt ended in an error that needs a decision.repairmeans the item is known to be broken or incomplete and is waiting for human or automated correction before another attempt.skippedmeans the item will not be processed under current rules, usually because eligibility changed.archivedmeans the item is closed out and should no longer be picked up by workers or displayed in active queues.
If you need to retain richer detail, store it as fields like failure_reason, retry_count, reserved_at, published_at, archived_at, and an immutable event log. Do not solve detail by multiplying states.
The valid transitions should be boring
The healthiest queue machines are the ones with few legal edges.
A sensible transition graph is:
pending→reservedreserved→generatedreserved→failedreserved→skippedgenerated→publishedgenerated→repairgenerated→failedfailed→pendingfailed→repairfailed→skippedrepair→pendingrepair→skippedrepair→archivedskipped→archivedpublished→archived
You may also allow generated → archived if generated output becomes obsolete before publication, but that should be a deliberate business decision, not a silent cleanup path.
What should not be allowed is more important than what should.
pendingshould not jump straight topublished.publishedshould not return togenerated.archivedshould not transition anywhere.skippedshould not be reprocessed automatically.failedshould not auto-loop forever without a retry policy.
That last point matters. Infinite retry loops usually mean the queue is hiding a permanent problem behind a transient-looking state.
This is why safe retry behavior for failed AI content generation jobs belongs in the same design discussion. Retry is a transition policy, not just a worker setting.
Reserve is not the same as lock forever
reserved should mean "this worker owns the item for now," not "nobody else can touch this row until someone remembers it tomorrow."
Use a reservation lease with an expiration timestamp.
A typical pattern is:
- Worker selects one
pendingitem. - Worker atomically changes it to
reservedand setsreserved_byplusreserved_until. - Worker processes the item.
- Worker moves it out of
reservedintogenerated,failed, orskipped.
If the worker dies, the lease expires and the item becomes eligible for recovery. You can either move expired reservations back to pending with a sweeper job or treat them as a special subset of failed depending on how much diagnostic history you want to preserve.
The important thing is that reservation is temporary and observable. A stuck reserved item is a useful signal, not a mystery.
Treat generated as a real checkpoint
In many systems, generated is the state that gets skipped because teams want to jump from queue to publish.
That usually creates trouble.
Keeping generated as an explicit state gives you a place to store the content payload, validation results, and any final checks before publication. It also creates a clean boundary between generation failures and publishing failures.
That separation is useful when you need to debug whether a bad page came from prompt output, template assembly, validation, deploy, or CMS publish logic. It also lines up with the principle behind separating content generation from publishing.
A good rule: if the content exists in a form that could be reviewed, rendered, or published, it deserves its own state.
Distinguish failed, repair, and skipped
These three states often get collapsed into one another, but they serve different operational purposes.
failed is for an attempt that could not complete. The item may still be valid, but something went wrong during generation, validation, storage, or publish handoff.
repair is for items that need intervention before another attempt makes sense. Examples include malformed source data, missing required fields, a broken template, or a page that now conflicts with a newer topic rule.
skipped is for items that should not continue in the queue as currently defined. The reason might be business logic, duplicate topic detection, eligibility changes, or a decision that the page is no longer worth publishing.
That distinction matters because each state implies a different next action:
failedmay retry or move torepairafter repeated attempts.repairneeds a fix before it can return topending.skippedshould usually be closed and archived, not retried.
If you already have topic validation upstream, this is where duplicate topic detection before generating SEO content helps prevent queue contamination. A clean queue is easier to reason about than one filled with items that should never have been admitted.
published should be terminal for the queue, not for the content
Once an item is published, the queue item's job is done.
That does not mean the page is immutable. It means the queue no longer owns the active lifecycle. Updates, refreshes, and future re-generation should usually create a new queue item or a distinct revision record rather than mutating the old published job back into an active state.
This is one of the easiest ways to avoid state drift:
- queue item tracks work
- content record tracks current page state
- publish record tracks what was sent live
If you need to audit behavior later, the queue should show how a page arrived at published, while the content store should show what is currently live. That separation is especially helpful when combined with a proper schema design, like the one described in how to design database tables for an SEO publishing pipeline.
Use events to explain transitions
Statuses alone are not enough for debugging.
Store each transition as an event:
- from state
- to state
- actor or worker id
- timestamp
- reason code
- optional payload diff or error reference
That makes it possible to answer questions like:
- Why did this item go from
generatedtorepair? - Which worker reserved it?
- How long did it remain
reserved? - Did a publish failure happen after generation succeeded?
- Was it skipped because of eligibility, duplication, or policy?
Without transition events, the queue becomes a static snapshot. With them, it becomes an operational log.
A practical state policy
If you want a conservative default, implement the queue with these rules:
- Only workers can move
pending→reserved. - Only the owning worker can move
reservedonward, unless a sweeper reclaims expired reservations. - Human review can move
generated→repairorskipped. - Automated validators can move
generated→failed. - Retry logic can move
failed→pendingup to a defined limit. - Repair completion can move
repair→pending. - Publishing can move
generated→published. - Any terminal cleanup job can move
published,skipped, orfailedintoarchivedonce the record is no longer operationally active.
That is enough to support a queue that is safe to retry, safe to inspect, and hard to corrupt by accident.
The main design choice is not the exact list of labels. It is whether each label has one meaning, one next action, and one owner. If those three things are true, the queue can scale without becoming a second, weaker CMS.