Skip to content

Publish workflow & scheduling

TwistCue treats publish as a finite state machine with an explicit in_review gate. This lets multiple people work on the same title (content_manager drafts, another content_manager reviews, superadmin publishes) without stepping on each other.

stateDiagram-v2
[*] --> draft
draft --> in_review: submit for review
in_review --> draft: send back
in_review --> scheduled: schedule
in_review --> published: publish now
scheduled --> published: publish-scheduler cron
scheduled --> in_review: unschedule
published --> archived: archive
archived --> draft: restore
draft --> [*]: delete (approval-gated)
StateVisible on mobile?Editable?
draftNoFully.
in_reviewNoMetadata edits still allowed.
scheduledNo: waits for run_at.Metadata edits allowed; changing run_at requires unschedule → reschedule.
publishedYesMost fields locked (title, slug); price/synopsis edits allowed with audit note.
archivedNoLocked.

Every transition is one endpoint: POST /v1/admin/series/:id/workflow (or /episodes/:id/workflow), with { action: 'submit' | 'send_back' | 'publish' | 'unpublish' | 'archive' | 'restore' }. The service enforces legal transitions.

Illegal moves (e.g. published → draft) return invalid_transition with the current state in the error body.

Series → detail → Schedule (or on the series wizard’s last step).

  1. Pick a datetime in UTC. The UI shows your local timezone alongside.
  2. Optionally set run_at-per-episode for a drop cadence: episode 1 today, episode 2 in 3 days, etc.
  3. Click Schedule. Series moves to scheduled and a publish_schedules row is created.

The publish-scheduler cron runs every minute, picks up any publish_schedules rows where run_at <= now() and status='pending', and executes the transition.

For a “2 episodes / week” cadence:

  1. Series detail → Episodes tab → select the episodes you want to schedule.
  2. Click Bulk schedule.
  3. Choose:
    • Start date (UTC)
    • Cadence (daily / every N days / weekly / custom)
    • Time of day (UTC)
  4. The UI previews the resulting schedule (episode 1 → day 0, episode 2 → day 3, …). Confirm.

Behind the scenes, this creates N publish_schedules rows (one per episode) with the calculated run_at on each. Same cron picks them up.

Series → detail → Publish now. Confirms with a typed-confirmation modal (type the series title). Moves straight to published, writes an AuditLog entry with action='series.publish'.

  • Unpublish: for a scheduled series that hasn’t gone live yet. Cancels the pending publish_schedules row and moves to in_review.
  • Archive: for a live series. Hides it from mobile immediately. Existing unlocks are preserved (users who paid still have access).
  • Delete: for a draft or archived series. Two-person action: creates an approval request. Approving admin must be a different catalog:delete holder.
Mobile surfacedraft / in_review / scheduledpublishedarchived
For You rails
Direct series URL404404
Existing unlock (paid)❌ (playback denied)✅ (unlock preserved)
Search results
  • Schedule: EVERY_MINUTE (* * * * *)
  • File: services/api/src/modules/admin/media/media-sync.cron.tsrunPublishScheduler()
  • Query: publish_schedules WHERE status='pending' AND run_at <= now()
  • Failure behavior: per-row try/catch: one bad row does not block the batch. Failed rows flip to status='failed' with a last_error string and are retried on the next tick (bounded by retry_count < 5).
  • Publishing a series with 0 published episodes is allowed (rare: usually a mistake). The mobile app renders an empty “Coming soon” state.
  • Time-zone drift: the cron is UTC. Do not rely on your local server’s TZ.
  • Editing run_at on a scheduled row: do it via the UI’s reschedule action, not by mutating the row directly. The UI enforces the audit trail.
ActionEndpoint
Series workflowPOST /v1/admin/series/:id/workflow
Episode workflowPOST /v1/admin/episodes/:id/workflow
List schedulesGET /v1/admin/schedules
Create schedulePOST /v1/admin/schedules
Bulk schedulePOST /v1/admin/schedules/bulk
Cancel scheduleDELETE /v1/admin/schedules/:id

Full details in API: Publishing & schedules.