Plugins
Form Variants

Saving

The save guard, what happens after a save, autosave and drafts, and document drawers.

Where the variants render

native is Payload's own edit view, untouched, with the switcher in its controls bar.

Every other variant renders in a form of the plugin's own, with the same document locking, stale-data and leave-without-saving modals and the same upload area.

Switching keeps every value, in either direction and on either surface. Two step variants share one form; crossing to or from native hands the state of the form being left to the one taking its place, fields only the other form shows included.

Two things a variant does not carry from the native form: the live preview window, and the auth fields (email, password) of an auth collection. Put those on native.

The guard

Every save the variant offers goes through one guard, the save controls and Ctrl+S alike. It allows a save only when:

  • save is always, or the user is on the last visible step, and
  • no step has called blockSave(), and
  • the document is not read-only.

The save controls render the guard's state: while it refuses, they are disabled. useWizard().save() goes through the same guard, and so does Ctrl+S.

Switching to native and saving there is not a bypass: access to native is the trust decision.

After a save

afterSave, if set, runs on the server through the evaluate endpoint. It receives the usual context plus savedDoc, the document as the server reads it after the save (never the browser's copy), and operation is create when that save created the document. It returns one of:

ActionEffect
nullPayload's behaviour: on a full-page create, redirect to /:id, where defaultVariant picks again; in a drawer, close and hand the document back
{ redirect: string }Navigate
{ outcome: { ... } }Show the variant's Outcome slot, which reads the data through useOutcome()
{ switchTo: string }Switch to another available variant. On a full-page create this redirects to /:id?variant=<key>

On a full-page create the action is applied instead of Payload's redirect, not after it.

Drafts

The save controls are Payload's own, with the same labels and the same enabled state: Save on a collection without drafts, Save Draft beside Publish changes on one with them. They render wherever the variant saves, which is the top bar under save: 'always' and the last step under save: 'final-step'.

A draft save leaves the published version standing and skips validation, so a half-filled wizard can be put down and picked up later. Publish stays enabled while there is anything to publish: an unpublished draft, or a document that was never published. An account without publish permission gets the one button, and it saves a draft. Ctrl+S saves a draft rather than publishing, as it does on the native form.

Scheduled publishing and per-locale publishing stay on native.

Autosave

A variant does not autosave, even on a collection that has autosave enabled. Payload still creates the document the moment /create opens there, so the variant runs as an update from the first render, but nothing is written to it until a save.

That is also the one place the save controls differ from native: native drops Save Draft when autosave is writing the drafts, and a variant keeps it, so a draft can still be saved.

save: 'final-step' therefore holds the values out of the database until the last step, on an autosave collection as on any other. When a component step calls finish(outcome) instead of saving, the document Payload created stays in the database; removing it is the consumer's call, made in the same code that calls finish, which has the document id through useDocumentInfo().

Drawers

Drawers use the same view resolution as the page, so the edit.default override applies inside a relationship field's "create new" drawer, and the user who never sees the full form on the page does not see it there either.

In a drawer there is no ?variant=, since a drawer has no URL. The current step is kept in memory as it is on the page, and the saved document goes back to the relationship field as it does now.

Security

Variants change what a person is shown, not what the API accepts:

  • REST, GraphQL, the Local API and scripts ignore variants. Restricting which fields an account may write is field access on the collection.
  • Gates are UX. A check that must hold for the data, such as duplicate detection, lives in a collection hook.
  • values and state sent to the evaluate endpoint are client input. On every call the endpoint loads the document with access enforced, answers 403 when the account lacks update access to it (create access when there is no id yet, or for afterSave after a create), and re-checks the variant's access. afterSave sees the document as the server reads it, not the copy the browser holds.

On this page