The press release goes out Monday at nine, the campaign banner comes down at the end of the month. Nobody wants to set an alarm for that. In RustyCMS the entry carries its own date; a background loop flips it, and to the frontend it looks like any other status change.
Three states, one rule
_status is draft, published or archived. Anonymous readers get published only — drafts and archived entries stay invisible unless a valid preview token unlocks them. Logged-in users and API keys filter as they like. In the admin UI a click on the badge cycles the state, and bulk actions handle many entries at once.
Set a date, done
_publishAt on a draft is all it takes. The background loop checks every 30 seconds (RUSTYCMS_SCHEDULE_INTERVAL_SECS, 0 disables it), flips due drafts to published and moves the date to its past-tense twin _publishedAt — so you can still see what was planned and when it actually happened.
PATCH /api/content/post/my-post/status
{ "_status": "draft", "_publishAt": "2099-01-01T09:00:00+01:00" }
# Clear a schedule: explicit null
PATCH /api/content/post/my-post/status
{ "_status": "draft", "_publishAt": null }
The transition takes the same path as a manual status change: version snapshot, search index, webhook, git autocommit — all of it fires. The audit log records the actor system:scheduler, so automatic and manual are easy to tell apart.
Let it expire
_unpublishAt is the mirror image: a published entry is archived once the date passes. The two fields are independent and can be set together — on a draft, the expiry date waits until the entry is live. If both dates are already in the past, one pass walks the entry draft → published → archived and writes two snapshots, not a silent jump.
What’s coming up, at a glance
?_scheduled=true lists the entries in any collection that still have a transition to fire. GET /api/schedule returns one flat list across all collections, sorted by due date — one row per transition, filterable by kind=publish|expire and until. The admin UI’s Schedule page is built on it. What already happened lives in the audit log under actor=system:scheduler; whether the loop is running at all is answered by four counters on /api/metrics.
What doesn’t happen
A date in the past publishes on the next tick — no backdating, no catch-up beyond that. A _publishAt on an already published entry is inert, and archived entries are never touched again. A date that has fired cannot fire twice.