Payload CMS drafts: how versions, autosave and scheduled publishing work

Idea Labz · Nov 12, 2026 · 14 min read

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:

  1. 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.
  2. Versions and drafts. Payload adds a _status field holding draft or published, and editors get Save Draft next to Publish changes.
  3. 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.

SettingWhat it adds to the databaseWhat it adds to the admin panelWhat it adds to the API
versionsA versions collection with a full copy per saveA Versions tab with diffs and restorefindVersions, findVersionByID, restoreVersion
versions.draftsA _status field on each documentSave Draft, Publish changes, Unpublish, and the Draft, Published and Changed labelsA draft argument on create, update, find and findByID
versions.drafts.autosaveOne autosave version per document, updated in placeSaving as the editor typesAn autosave argument on update
versions.drafts.schedulePublishA job for each scheduled publish or unpublishA Schedule Publish option on the publish buttonJobs, run by your jobs runner
trashA deletedAt fieldA Trash view with restore and permanent deleteA 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 panelWhat Payload writesWhat a visitor sees
Save Draft on a new storyThe story with _status: 'draft', plus a versionNothing, if your read rule hides drafts
Save Draft on a published storyA new version onlyThe published story
Publish changesThe story with _status: 'published', plus a versionThe new content
UnpublishThe story with _status: 'draft'Nothing, once drafts are hidden
Revert to publishedA new version copying the last published stateThe 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 versions setting gains a versions table on upgrade, which needs a migration. The guide’s codemod, npx @payloadcms/codemod --transform migrate-versions-default, adds versions: false everywhere it isn’t set, if you’d rather keep today’s behaviour.
  • Version history follows the read rule. Without a readVersions rule, versions inherit the collection’s read access 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.

Frequently asked questions

Does Payload have version history or an audit log?

Payload has version history in its free core. With versions on, every save is kept with its date and status, and the Versions tab compares and restores them. Payload’s Enterprise page also lists audit logs, and Enterprise is sold under an Enterprise licence, quoted on application, as our Payload pricing guide explains.

Can Payload require approval before something is published?

In the core, an update rule can let some roles publish while others only save drafts. Multi-step approval with notifications is Payload’s Publishing Workflows, which is part of Payload Enterprise.

How do I change a published document without publishing the change?

Save a draft. In the admin panel that’s Save Draft, and through the API it’s an update with draft: true. The published version stays live until someone publishes the draft.

Why didn’t my scheduled post publish?

Check first that something runs Payload’s jobs queue. A scheduled publish is a job waiting for its time, so it needs autoRun on a server that stays running, or a scheduled call to /api/payload-jobs/run on serverless hosting. If the call runs but nothing publishes, check that it passes the endpoint’s access.run rule and that the user who scheduled the post can still update it.

How many versions does Payload keep?

100 per document for a collection and 100 for a global, by default. Change it with maxPerDoc on a collection or max on a global, and set 0 to keep every version.

Can I restore a deleted document in Payload?

Only if trash was on for that collection when it was deleted. With trash on, deleted documents wait in the Trash view until someone restores them or deletes them permanently. Without trash, a delete removes the document and its versions, and only a database backup brings it back.

Do I need drafts for Live Preview?

No. Live Preview runs on any collection listed in admin.livePreview. With drafts on, editors can try changes in the preview without publishing them. Without drafts, every save publishes.

Can drafts be published per language?

Yes, with a beta setting in Payload 3. localizeStatus stores the draft or published status per locale, so the English version can be live while the Arabic one is still a draft. Our Payload localisation guide covers the two settings it takes.

What to do next

Turn drafts on together with a read rule that hides them, in the same change. Add autosave where editors write long pieces, and scheduled publishing only once something runs the jobs queue on your host. Turn trash on for collections where a mistaken delete would hurt, and check every readVersions rule before you move to Payload 4.

Your next step depends on where you are. If your front end needs to show drafts to editors, how a Next.js front end shows drafts is next. If you don’t have a project yet, our Payload tutorial builds one from an empty folder.

References

All links checked and active as of 11 November 2026.

READY TO MAKE A REAL CHANGE?

Let's build it together