Content Releases
Group documents into a release and publish or unpublish them together, at a time you choose.
A release is a set of documents that go live at the same instant. A product launch touching a landing page, three blog posts and a pricing single becomes one thing you schedule, rather than five things someone has to publish in the right order at 9am.
Each member of a release is set to either publish or unpublish, so the same release can put new pages live and retire old ones in the same moment.
Availability
| Built in | No plugin required |
| Admin | A Releases section with a calendar view, and an "Add to release" action on documents |
| Permission | Scheduling requires publish access. A key scoped only to publishing releases can schedule without holding a general read grant |
What it is not
This is scheduled publishing, not an approval workflow. There are no review stages, transitions or assignees. If you need "editor submits, manager approves, then it goes live", that is not what a release does.
The shape of a release
| Step | What happens |
|---|---|
| Create | An empty release with a name |
| Add members | Each member names a document and an action, publish or unpublish |
| Schedule | The release is given a time. Blocking problems are checked first |
| Apply | On a background-jobs pass after that time, every member's action is applied |
| Cancel | An unapplied release can be cancelled |
Scheduling stores a time; a job runner applies it
Scheduling records when a release is due. The work is done by a releases:drain sweep that
runs as part of the background jobs pass, triggered by calling the jobs route
from a cron job, a platform scheduler, or an operator.
The scaffolded apps mount Nextly's handlers at src/app/admin/api/[[...params]]/route.ts, so
the URL is:
POST /admin/api/jobs/runThere is no general /api handler in a scaffolded app, so a scheduler pointed at
/api/jobs/run gets a 404 and your releases never publish. If you mounted the handlers
somewhere else, use that path instead.
With no scheduler configured, a release stays scheduled forever. Its due time passes and
nothing happens, because nothing ran the sweep. Set up an authenticated periodic call to
/api/jobs/run before you rely on scheduling.
A pass is bounded: it claims at most 10 queued jobs, and it stops starting new ones after 20 seconds. That 20 seconds is a budget for starting work, not a hard ceiling on the request — the check happens before each claim, so a handler already running is awaited to completion and the call can overrun. Size any serverless timeout for the slowest handler, not for 20 seconds.
The sweep can also defer work past its deadline while still reporting success. So a release is applied on the first pass that actually reaches and completes it, which is not always the first pass after its due time. With a backlog, or several expensive releases due together, expect it a pass or two later. Treat the scheduled time as "not before", not as "at".
Blocking reasons
Before a release can be scheduled, it is checked for problems that would make it fail half-way — a member pointing at a document that no longer exists, for example. Those are reported as blocking reasons so they can be fixed while there is still time, rather than surfacing as a partial publish at 9am.
An empty release has no blockers; there is nothing to go wrong.
One release covers every language
A member is the whole document. Adding a post to a release schedules that post in every language it has.
Per-locale scheduling is not supported: adding a member that names a single locale is refused, and the admin only offers the action from a document's default locale. Each language does have its own published state — see Localization — but that is publishing, not scheduling.
Related
- Localization — per-language publishing
- Draft and published status
- Versions — what a publish changed