Your editors need to save work in progress without it going live, roll back a bad edit, and publish a story at six in the morning without being awake for it. In Payload, most of that runs on versions, with drafts, autosave and scheduled publishing built on top of it, and trash beside them.
As of 11 November 2026, the latest stable release of Payload is 3.93.0, published on 4 November, and the Payload 4 betas change two of the defaults on this page (the 3.93.0 release). We read the published payload 3.93.0 package on 11 November 2026 alongside Payload’s docs, and where the two differ, this page says which is which. We build and run Payload projects for clients, and the last section shows which of these settings our own project template turns on.
This guide takes each setting in the order you’d turn it on, then covers what changes in Payload 4 and how we set up our own builds.
- How drafts and versions work in Payload
- How to turn on versions and drafts
- What happens when you save a draft, publish or unpublish
- Autosave
- Version history: comparing and restoring
- How to keep drafts private
- Reading drafts through the APIs
- Scheduled publishing
- Trash and soft delete
- Preview and Live Preview: what you set up in Payload
- What changes in Payload 4
- How we set up drafts on our builds
- Frequently asked questions
- What to do next
How drafts and versions work in Payload
Versions keep a full copy of a document every time it’s saved, and drafts let a saved version stay unpublished until someone presses Publish. Payload stores the copies in a separate versions collection beside your own, so turning versions on doesn’t change the shape of your documents (Payload’s versions docs). In MongoDB a stories collection gets a _stories_versions collection, and in Postgres and SQLite a _stories_v table.
There are three levels, and each one needs the one before it:
- Versions only. Every save becomes a new version, and the newest one is always live. This suits content that should change at once but still keep a history.
- Versions and drafts. Payload adds a
_statusfield holdingdraftorpublished, and editors get Save Draft next to Publish changes. - Versions, drafts and autosave. The admin panel saves drafts while the editor works.
For example, take a city news site’s newsroom. Reporters write stories that editors check and publish, some stories go live at a set time, corrections sometimes go wrong and need rolling back, and the homepage’s featured stories live in a global that changes several times a day.
| Setting | What it adds to the database | What it adds to the admin panel | What it adds to the API |
|---|---|---|---|
versions | A versions collection with a full copy per save | A Versions tab with diffs and restore | findVersions, findVersionByID, restoreVersion |
versions.drafts | A _status field on each document | Save Draft, Publish changes, Unpublish, and the Draft, Published and Changed labels | A draft argument on create, update, find and findByID |
versions.drafts.autosave | One autosave version per document, updated in place | Saving as the editor types | An autosave argument on update |
versions.drafts.schedulePublish | A job for each scheduled publish or unpublish | A Schedule Publish option on the publish button | Jobs, run by your jobs runner |
trash | A deletedAt field | A Trash view with restore and permanent delete | A trash argument on find, update and delete |
How those versions tables look in each database, and what they cost in storage, is covered in which database to choose for Payload.
How to turn on versions and drafts
Set versions on the collection or global. versions: true turns on history with the defaults, and an object lets you add drafts and change how much history is kept.
import type { CollectionConfig } from 'payload'
export const Stories: CollectionConfig = {
slug: 'stories',
admin: { useAsTitle: 'headline' },
versions: {
drafts: true,
maxPerDoc: 50,
},
fields: [
{ name: 'headline', type: 'text', required: true },
{ name: 'slug', type: 'text', required: true, unique: true },
{ name: 'body', type: 'richText' },
],
}Globals take the same versions key, with max in place of maxPerDoc:
import type { GlobalConfig } from 'payload'
export const Homepage: GlobalConfig = {
slug: 'homepage',
versions: {
drafts: true,
max: 0,
},
fields: [
{ name: 'featured', type: 'relationship', relationTo: 'stories', hasMany: true },
],
}In the payload 3.93.0 package, a collection keeps 100 versions per document and a global keeps 100 versions unless you set maxPerDoc or max, and 0 keeps every version. The docs give the 100 for collections. For example, the newsroom keeps 50 versions per story and every version of the homepage, because the homepage’s history is the record of what ran on the front page and when.
Versions also need timestamps, so a collection with timestamps: false won’t start with versions on. Turning versions or drafts on adds a table or field to your database. On Postgres and SQLite, Payload pushes that change for you in development, and in production it goes in a migration, which our Payload migrations guide walks through.
What happens when you save a draft, publish or unpublish
With drafts on, the admin panel replaces the Save button with Save Draft and Publish changes. Save Draft writes a new version and leaves the main document, the one visitors are served, as it was. Publish changes writes both, with _status set to published (Payload’s drafts docs).
| Action in the admin panel | What Payload writes | What a visitor sees |
|---|---|---|
| Save Draft on a new story | The story with _status: 'draft', plus a version | Nothing, if your read rule hides drafts |
| Save Draft on a published story | A new version only | The published story |
| Publish changes | The story with _status: 'published', plus a version | The new content |
| Unpublish | The story with _status: 'draft' | Nothing, once drafts are hidden |
| Revert to published | A new version copying the last published state | The published story |
Drafts skip required-field validation, so a reporter can save a story before it has a slug. Set validate: true under drafts if you want drafts checked too; it’s false by default. Through the API, the same split comes from the draft argument. An update with draft: true writes to the versions only, and setting _status: 'published' in the data publishes.
For example, an editor fixing a live story can save drafts all afternoon while readers keep seeing the published text. The admin panel labels the story Changed until the editor presses Publish changes.
Autosave
Autosave saves a draft for the editor while they type, so a closed tab doesn’t lose an hour’s work. It needs versions and drafts, and you turn it on under drafts:
versions: {
drafts: {
autosave: {
interval: 1500,
showSaveDraftButton: true,
},
},
},interval is how long the admin panel waits after the last change before it saves, in milliseconds. Payload’s autosave docs give the default as 800 ms. In the published payload 3.93.0 package the default is 2,000 ms, the admin panel’s autosave reads that value, and the package has carried 2,000 since 3.0.0. If the timing matters to your editors, set interval yourself. showSaveDraftButton keeps Save Draft visible, which autosave hides by default.
Payload doesn’t add a version for every autosave. It keeps one autosave version and updates it each time, so autosaves don’t fill the version history. For example, the newsroom turns autosave on for stories, where reporters write long drafts, and leaves it off the homepage global, where every save should be a deliberate one.
Version history: comparing and restoring
Every document with versions gets a Versions tab in the admin panel. It lists each saved version with its date and status, compares any version with the current one field by field, and has a Restore this version button. Restoring writes a new version, so the history keeps the edit you rolled back.
The same operations are available in code. Through REST they’re GET /api/stories/versions and POST /api/stories/versions/:id, and through the Local API they look like this:
const { docs } = await payload.findVersions({
collection: 'stories',
where: { parent: { equals: storyId } },
sort: '-updatedAt',
limit: 10,
})
await payload.restoreVersion({
collection: 'stories',
id: docs[1].id,
})For example, when a correction to a published story introduces a new error, the newsroom’s duty editor opens the Versions tab, compares the last two versions, and restores the one before the correction.
Who can read the history is a separate access rule, readVersions, and it’s part of keeping drafts private.
How to keep drafts private
Drafts are only private if your read rule makes them private. Turning drafts on adds the _status field, but it doesn’t change who can read the collection. A collection with read: () => true returns never-published stories to anyone who calls the API, and a request with draft=true gets the newest draft of every story (Payload’s drafts docs).
The rule Payload’s docs recommend lets signed-in users read everything and gives everyone else published documents only:
access: {
read: ({ req }) => {
if (req.user) return true
return { _status: { equals: 'published' } }
},
},Returning a query instead of false means visitors still get the published stories. Documents created before you turned drafts on have no _status until they’re saved again. So for an existing collection, the docs add a second condition with or, { _status: { exists: false } }, which keeps the older documents readable.
Version history follows readVersions, not read. In Payload 3, a collection without a readVersions rule lets only signed-in users read versions (the access check in payload 3.93.0). That changes in Payload 4, as the section on Payload 4 below explains.
Server code that reads through the Local API skips access rules unless you pass overrideAccess: false with a user. So a front end that reads Payload directly has to ask for published content itself, for example with where: { _status: { equals: 'published' } }.
Access rules also decide who can publish. Save Draft sends _status: 'draft' and Publish sends _status: 'published', so an update rule that checks data._status separates the two. The admin panel runs the same rule with _status: 'published' to decide whether to show Publish and Unpublish, so users the rule turns away don’t see them. This example assumes your users have a role field:
access: {
update: ({ req: { user }, data }) => {
if (!user) return false
if (user.role === 'editor') return true
return data?._status !== 'published'
},
},In payload 3.93.0, a rule that returns a query such as { _status: { equals: 'draft' } } is tested against the stored document instead, so it stops users changing stories that are already live, but it still lets them publish a draft. For example, the newsroom’s reporters can save drafts, including drafts of live stories, and only editors can publish or unpublish. Setting up roles like these, and deciding who signs in where, is covered in our Payload authentication guide.
Reading drafts through the APIs
With drafts on, find and findByID accept a draft argument in REST, GraphQL and the Local API. With draft set, Payload returns each document’s newest version, draft or published. Without it, Payload returns the main document, which is the published version once a document has been published.
curl -g "https://news.example.com/api/stories?where[slug][equals]=city-budget-2027&draft=true" \
-H "Authorization: JWT <token>"const { docs } = await payload.find({
collection: 'stories',
where: { slug: { equals: 'city-budget-2027' } },
draft: true,
overrideAccess: false,
user,
})In GraphQL, the same request passes draft: true as an argument on the query. Keep in mind that the read rule still applies, so a caller the rule limits to published stories doesn’t get drafts by adding draft=true.
For example, a signed-in editor’s preview page asks for draft: true to show the newest draft of a story before it’s published, while the public page leaves draft off.
Scheduled publishing
Scheduled publishing adds a Schedule Publish option to the publish button, where an editor picks a date, a time and a time zone for Payload to publish or unpublish the document. It’s another option under drafts:
versions: {
drafts: {
schedulePublish: true,
},
},When the editor schedules, Payload adds a job to its jobs queue, on the queue named default, set to wait until the chosen time. In the payload 3.93.0 package the job publishes as the user who scheduled it, so that user’s update rule applies when it runs. Nothing publishes until something runs the queue (Payload’s drafts docs).
On a server that stays running, such as a VPS or a container, Payload can run the queue itself with autoRun:
export default buildConfig({
// ...
jobs: {
autoRun: [{ cron: '* * * * *', queue: 'default' }],
},
})On serverless hosting, autoRun doesn’t work, because no process stays up to run it. Instead, a scheduled call from your platform hits /api/payload-jobs/run. That endpoint lets only signed-in users run jobs by default, so the scheduled call needs its own rule, like this one from Payload’s jobs docs for Vercel Cron:
jobs: {
access: {
run: ({ req }) => {
if (req.user) return true
const secret = process.env.CRON_SECRET
if (!secret) return false
return req.headers.get('authorization') === `Bearer ${secret}`
},
},
},The schedule of that call sets how close to the chosen time a story goes live. For example, if the newsroom schedules an embargoed budget story for 06:00 and the queue runs every five minutes, the story goes live at the first run after 06:00. Which hosts can run a queue, and how often, is in our guide to hosting Payload.
As of 11 November 2026, several scheduling bugs are open on Payload’s issue tracker, including one where scheduling doesn’t check the user’s right to update the document, so let only the roles that may publish schedule posts.
Trash and soft delete
Trash lets editors delete a document without losing it. Set trash: true on a collection, and a delete moves the document to the collection’s trash and stamps it with a deletedAt date instead of removing it (Payload’s trash docs). Trash has been in Payload’s core since 3.49.0, released on 25 July 2025.
export const Stories: CollectionConfig = {
slug: 'stories',
trash: true,
// ...
}Each collection with trash gets a Trash view in the admin panel. From there, editors can restore documents, delete them permanently, or empty the trash. The delete dialog in the list view also has a “Skip trash and delete permanently” checkbox. A trashed document opens read-only, with its version history visible, but a version can’t be restored until the document itself is out of the trash.
In the API, find operations leave trashed documents out. Pass trash: true to include them, or add where: { deletedAt: { exists: true } } as well to get only the trashed ones.
Moving to trash and deleting permanently both go through the collection’s delete rule. When a document is being trashed, data.deletedAt is set, and on a permanent delete data is empty, so one rule can treat the two differently:
access: {
delete: ({ req: { user }, data }) => {
if (!user) return false
if (user.role === 'admin') return true
return Boolean(data?.deletedAt)
},
},With this rule, editors can move stories to the trash, and only admins can delete them for good, so the admin panel hides the skip-trash checkbox from editors. For example, when the newsroom pulls a story for a legal check, it goes to the trash with its history, and an editor restores it the next day. Trash adds a column to the collection, so it needs a migration too.
Preview and Live Preview: what you set up in Payload
Payload has two ways to show editors their content on the real site. admin.preview adds a preview button that opens a URL built from the document, and admin.livePreview shows your front end in a panel beside the edit form (Payload’s preview docs).
export const Stories: CollectionConfig = {
slug: 'stories',
admin: {
preview: ({ slug }) => `https://news.example.com/stories/${slug}`,
},
// ...
}export default buildConfig({
// ...
admin: {
livePreview: {
url: ({ data }) => `https://news.example.com/stories/${data.slug}`,
collections: ['stories'],
breakpoints: [{ label: 'Mobile', name: 'mobile', width: 375, height: 667 }],
},
},
})The preview button opens the URL as it is. For it to show a draft, the URL points at a route on your front end that checks the editor is signed in and switches the site to reading drafts. Live Preview loads your front end in an iframe and sends it the document as the editor works (Payload’s Live Preview docs). A client-side front end re-renders as the editor types. A server-side one refreshes each time the document is saved, whether by Save Draft, autosave or Publish, which is why Payload recommends turning autosave on alongside it (server-side Live Preview).
For example, the newsroom’s editor opens Live Preview on a story, switches to the Mobile breakpoint, and checks the headline at phone width before publishing. The front-end half, with Draft Mode and the refresh component, is in the previews section of our Payload and Next.js guide.
What changes in Payload 4
Two defaults on this page change in the 4.0 betas (4.0.0-beta.8, 10 November 2026), according to Payload’s 4.0 migration guide. Where Payload 4 stands as a release is covered in our Payload review.
- Versions are on by default for every collection and global except auth collections such as users. Any collection without a
versionssetting gains a versions table on upgrade, which needs a migration. The guide’s codemod,npx @payloadcms/codemod --transform migrate-versions-default, addsversions: falseeverywhere it isn’t set, if you’d rather keep today’s behaviour. - Version history follows the read rule. Without a
readVersionsrule, versions inherit the collection’sreadaccess instead of requiring a signed-in user. A public read rule then makes old and unpublished versions public too. To keep history for signed-in users only, set the rule yourself:
access: {
read: () => true,
readVersions: ({ req: { user } }) => Boolean(user),
},Payload 4 also adds createdBy and updatedBy fields to collections by default, and our Payload admin panel guide’s Payload 4 section lists the other admin changes. For example, before the newsroom upgrades, it adds a readVersions rule to stories, because its published-only read rule would otherwise make every old published version of a story readable by anyone.
How we set up drafts on our builds
In our project template (v2.3.2, 4 November 2026, on Payload 3.90.1), pages are the one core collection with drafts, and each page keeps its last 10 versions. The header, footer and navigation, and the other core collections, keep no version history. A project can turn drafts on for its own collections, and when a project switches ecommerce on, visitors see published products only.
Both public front ends, one reading over GraphQL and one over REST, ask only for published pages. Blocks that list documents show only published ones from any collection that keeps drafts. Publishing, unpublishing or renaming a page refreshes the public pages it affects, and saving a draft leaves the public site as it was. Pages have a preview button that opens the page’s address on the public site.
Autosave, Live Preview, scheduled publishing and trash stay off until a project needs them. Anyone who may edit a page may also publish it, and creating and deleting pages is kept to a smaller role. The template already runs Payload’s jobs queue for its own plugins, with Payload’s own auto-run switched off because our apps run serverless, so a project that needs scheduled publishing would run it on that same queue. If you’d like a team to shape drafts and publishing around your editors, our Payload development work starts there.
