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.
The workflow states
Section titled “The workflow states”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)| State | Visible on mobile? | Editable? |
|---|---|---|
draft | No | Fully. |
in_review | No | Metadata edits still allowed. |
scheduled | No: waits for run_at. | Metadata edits allowed; changing run_at requires unschedule → reschedule. |
published | Yes | Most fields locked (title, slug); price/synopsis edits allowed with audit note. |
archived | No | Locked. |
Transitions
Section titled “Transitions”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.
Scheduling a publish
Section titled “Scheduling a publish”Series → detail → Schedule (or on the series wizard’s last step).
- Pick a datetime in UTC. The UI shows your local timezone alongside.
- Optionally set
run_at-per-episode for a drop cadence: episode 1 today, episode 2 in 3 days, etc. - Click Schedule. Series moves to
scheduledand apublish_schedulesrow 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.
Bulk-schedule episode drops
Section titled “Bulk-schedule episode drops”For a “2 episodes / week” cadence:
- Series detail → Episodes tab → select the episodes you want to schedule.
- Click Bulk schedule.
- Choose:
- Start date (UTC)
- Cadence (daily / every N days / weekly / custom)
- Time of day (UTC)
- 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.
Publishing now (bypass schedule)
Section titled “Publishing now (bypass schedule)”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'.
Unpublishing / archiving
Section titled “Unpublishing / archiving”- Unpublish: for a scheduled series that hasn’t gone live yet. Cancels the pending
publish_schedulesrow and moves toin_review. - Archive: for a live series. Hides it from mobile immediately. Existing unlocks are preserved (users who paid still have access).
- Delete: for a
draftorarchivedseries. Two-person action: creates an approval request. Approving admin must be a differentcatalog:deleteholder.
What the mobile app sees at each state
Section titled “What the mobile app sees at each state”| Mobile surface | draft / in_review / scheduled | published | archived |
|---|---|---|---|
| For You rails | ❌ | ✅ | ❌ |
| Direct series URL | 404 | ✅ | 404 |
| Existing unlock (paid) | ❌ (playback denied) | ✅ | ✅ (unlock preserved) |
| Search results | ❌ | ✅ | ❌ |
The publish-scheduler cron
Section titled “The publish-scheduler cron”- Schedule:
EVERY_MINUTE(* * * * *) - File:
services/api/src/modules/admin/media/media-sync.cron.ts→runPublishScheduler() - 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 alast_errorstring and are retried on the next tick (bounded byretry_count < 5).
Gotchas
Section titled “Gotchas”- 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_aton ascheduledrow: do it via the UI’s reschedule action, not by mutating the row directly. The UI enforces the audit trail.
API calls behind this
Section titled “API calls behind this”| Action | Endpoint |
|---|---|
| Series workflow | POST /v1/admin/series/:id/workflow |
| Episode workflow | POST /v1/admin/episodes/:id/workflow |
| List schedules | GET /v1/admin/schedules |
| Create schedule | POST /v1/admin/schedules |
| Bulk schedule | POST /v1/admin/schedules/bulk |
| Cancel schedule | DELETE /v1/admin/schedules/:id |
Full details in API: Publishing & schedules.