Capture attributed edits, review proposals, retain author history, and export explicit projections.
Authored changes keep accepted content, proposals, decisions, and stable change identities in one saved document. Use Suggestions for the common editing and suggesting modes. This guide covers the underlying records, advanced views, decisions, retained history, and formats.
| Need | Surface |
|---|---|
| Switch between editing and suggesting | SuggestionPlugin from platejs/suggestion/react |
| Add copied highlighting and review cards | SuggestionKit and DiscussionKit |
Record changes using Plate's userId | DefaultAuthoredPlugin from platejs/authored |
| Choose a custom author resolver or retain history | authored(options) from platejs/authored |
| Read, decide, resolve, or revert changes | The installed authored plugin portal |
| Attach a conversation | CommentsPlugin with a { type: 'change', id } target |
Suggestion is a behavior and presentation layer over authored changes. It does not define another change record, decision result, or persistence format.
DefaultAuthoredPlugin resolves the author from the Plate editor's userId once per transaction:
import { BaseParagraphPlugin, createEditor } from "platejs";
import { DefaultAuthoredPlugin } from "platejs/authored";
const editor = createEditor({
plugins: [BaseParagraphPlugin, DefaultAuthoredPlugin],
initialValue: [
{ type: "paragraph", children: [{ text: "Review this sentence." }] },
],
userId: "alice",
});
const authored = editor.plugin(DefaultAuthoredPlugin);import { BaseParagraphPlugin, createEditor } from "platejs";
import { DefaultAuthoredPlugin } from "platejs/authored";
const editor = createEditor({
plugins: [BaseParagraphPlugin, DefaultAuthoredPlugin],
initialValue: [
{ type: "paragraph", children: [{ text: "Review this sentence." }] },
],
userId: "alice",
});
const authored = editor.plugin(DefaultAuthoredPlugin);Use an authenticated, stable user ID. authored.read.canPropose() reports
whether the resolver currently supplies one. SuggestionPlugin rejects
entering Suggestion mode without an author, and native authored writes reject
missing identity at the transaction boundary.
For a custom resolver or retained accepted history, create one descriptor and use that same descriptor for portal access:
import { BaseParagraphPlugin, createEditor } from "platejs";
import { authored } from "platejs/authored";
const RetainedAuthoredPlugin = authored({
authorId: () => session.user.id,
retainHistory: true,
});
const historyEditor = createEditor({
plugins: [BaseParagraphPlugin, RetainedAuthoredPlugin],
initialValue,
});
const retainedAuthored = historyEditor.plugin(RetainedAuthoredPlugin);import { BaseParagraphPlugin, createEditor } from "platejs";
import { authored } from "platejs/authored";
const RetainedAuthoredPlugin = authored({
authorId: () => session.user.id,
retainHistory: true,
});
const historyEditor = createEditor({
plugins: [BaseParagraphPlugin, RetainedAuthoredPlugin],
initialValue,
});
const retainedAuthored = historyEditor.plugin(RetainedAuthoredPlugin);retainHistory keeps closed edit bodies needed for selective revert. Omit it when the application only needs pending review and attribution.
An editing view records ordinary updates as accepted contributions. Begin an explicit proposal inside the transaction before applying its document mutation:
let changeId = "";
editor.update((tx) => {
changeId = tx.authored.propose();
tx.text.insert(" reviewed", {
at: { offset: 8, path: [0, 0] },
});
});let changeId = "";
editor.update((tx) => {
changeId = tx.authored.propose();
tx.text.insert(" reviewed", {
at: { offset: 8, path: [0, 0] },
});
});Pass { changeId } to assign an identity to a new proposal or amend a pending contribution owned by the current author. The current author must own an existing change's latest head.
Compare a fixed draft with the accepted document, then publish its canonical change as one native proposal:
import { proposeAuthoredComparison } from "platejs/authored";
import { compare } from "platejs/diff";
const incomingDraft = [
{ type: "paragraph", children: [{ text: "Revised sentence." }] },
];
const comparison = await compare({
before: editor.read.value(),
after: incomingDraft,
schema: editor.read.schema,
});
const imported = proposeAuthoredComparison(editor, { comparison });import { proposeAuthoredComparison } from "platejs/authored";
import { compare } from "platejs/diff";
const incomingDraft = [
{ type: "paragraph", children: [{ text: "Revised sentence." }] },
];
const comparison = await compare({
before: editor.read.value(),
after: incomingDraft,
schema: editor.read.schema,
});
const imported = proposeAuthoredComparison(editor, { comparison });groupIds selects comparison groups and includes their required dependencies. The result maps selected group IDs to the native change ID. Import checks the schema, accepted content, and authored frontier before writing; handle stale, blocked, invalid, and unavailable without treating them as applied proposals. A resolved three-way comparison can use the same call when its selected baseline is the live accepted document.
Set the starting intent and projection on EditorRoot:
import { DefaultAuthoredPlugin } from "platejs/authored";
import { EditorContent, EditorRoot, useCreateEditor } from "platejs/react";
export function SuggestionEditor() {
const editor = useCreateEditor({
plugins: [DefaultAuthoredPlugin],
userId: "alice",
});
return (
<EditorRoot
editor={editor}
authored={{ intent: "propose", projection: "markup" }}
>
<EditorContent />
</EditorRoot>
);
}import { DefaultAuthoredPlugin } from "platejs/authored";
import { EditorContent, EditorRoot, useCreateEditor } from "platejs/react";
export function SuggestionEditor() {
const editor = useCreateEditor({
plugins: [DefaultAuthoredPlugin],
userId: "alice",
});
return (
<EditorRoot
editor={editor}
authored={{ intent: "propose", projection: "markup" }}
>
<EditorContent />
</EditorRoot>
);
}The editor must install an authored plugin, either directly or through SuggestionPlugin. Changing the intent or projection prop updates this view. Rerendering with equivalent values preserves subsequent mode changes made through the mounted view's portal.
For an interactive mode control, let SuggestionPlugin change the intent while preserving the current projection:
import { SuggestionPlugin } from "platejs/suggestion/react";
editor.plugin(SuggestionPlugin).api.setMode("suggesting");import { SuggestionPlugin } from "platejs/suggestion/react";
editor.plugin(SuggestionPlugin).api.setMode("suggesting");Use the authored portal for advanced projections:
const authored = editor.plugin(DefaultAuthoredPlugin);
authored.api.setView({
intent: "propose",
projection: "proposed",
});const authored = editor.plugin(DefaultAuthoredPlugin);
authored.api.setView({
intent: "propose",
projection: "proposed",
});| Projection | Visible content |
|---|---|
accepted | Accepted content without pending proposals |
proposed | The proposed result without retained review fragments |
markup | Proposed content with retained deletions and move fragments |
Use intent: 'edit' with any projection. An editing markup or proposed view keeps pending content visible while edits to independent accepted content publish directly. A replacement that fully contains pending changes resolves them and publishes directly in the same undoable transaction. Partial retained or dependency-spanning targets stay protected; other dependent edits remain proposals with the current author. Use intent: 'propose' with proposed or markup when ordinary input must create proposals.
Intent and projection belong to the exact view. Selection, clipboard input, commands, and anchors use that view's coordinates. Two mounted views over one document can use different policies.
Read a page of pending changes, then capture a revision-bound selection for the decision:
const authored = editor.plugin(DefaultAuthoredPlugin);
const pending = authored.read.changes({
authorId: "alice",
limit: 50,
status: "pending",
});
const selection = authored.read.select({
ids: pending.items.map(({ id }) => id),
});
const result = authored.update.decide({
action: "accept",
selection,
});const authored = editor.plugin(DefaultAuthoredPlugin);
const pending = authored.read.changes({
authorId: "alice",
limit: 50,
status: "pending",
});
const selection = authored.read.select({
ids: pending.items.map(({ id }) => id),
});
const result = authored.update.decide({
action: "accept",
selection,
});Use action: 'reject' to reject changes. The example selects only the returned page. Pass a non-null page cursor to changes(...) to read more, or call select({ authorId: 'alice', status: 'pending' }) to select every current match.
AuthoredSelection captures the document ID, change revisions, and current heads. A later amendment makes an old selection stale. Dependencies and concurrent conflicts return blocked without partially applying the batch.
For spatial controls, use change(id) to read one record and changesAt(range) to find records intersecting a text range. Each AuthoredChange includes its author, status, kind, dependencies, revision, and current ranges.
Read semantic detail when the reviewer opens one change:
const details = authored.read.details(changeId);
if (details?.parts.status === "available") {
for (const part of details.parts.items) {
// Render content, boundary, property, or root facts in review UI.
}
}const details = authored.read.details(changeId);
if (details?.parts.status === "available") {
for (const part of details.parts.items) {
// Render content, boundary, property, or root facts in review UI.
}
}details(id) returns the compact change summary, decision history, and immutable semantic parts. Content parts include before and after slices. Boundary parts distinguish paragraph splits from joins. Property parts preserve exact before and after values. Root parts describe root creation or deletion.
Closed changes without retained edit bodies return parts.status: 'unavailable' with reason: 'retention'; their summary and reviews remain readable. Keep list and spatial reads compact, and request details only for the change being reviewed.
Always inspect the result of decide, resolve, or revert:
| Status | Meaning |
|---|---|
applied | The operation applied to the returned IDs. |
unchanged | No change was needed. |
stale | The selected revisions or heads changed. Read the records again before retrying. |
blocked | The selection has unresolved dependencies, dependants, or conflicts. Inspect the returned lists. |
invalid | The selection or document identity is invalid. |
unavailable | A revert needs content that is no longer retained. |
authored.read.preview({ action, selection }) checks an accept or reject decision without applying it. An applied preview means the decision can apply at that moment; the later decide call still needs result handling.
Accepting a change can require its dependencies. Rejecting one can require its dependants. The editor never expands the selection silently. Call authored.update.resolve({ action, selection }) only after the reviewer chooses a decision for the complete conflict component.
Enable retainHistory before recording work whose content must remain available for selective revert. Loading a compacted document with retention enabled cannot recover discarded edit bodies.
Select an author's accepted contributions and create a compensating change:
const selection = retainedAuthored.read.select({
authorId: "alice",
status: "accepted",
});
const result = retainedAuthored.update.revert({ selection });const selection = retainedAuthored.read.select({
authorId: "alice",
status: "accepted",
});
const result = retainedAuthored.update.revert({ selection });The original records keep their authors. The compensation belongs to the current author. A revert can return blocked when later dependent work prevents compensation, or unavailable when the required content is absent.
Plate History reverses local interaction batches with editor.api.history.undo() and redo(). Local undo also restores mapped selection. Selective revert records another authored contribution. The Version History example lets you edit as two authors and revert one retained contribution.
Save the complete result of editor.read.value(). It contains accepted content and the meta.authored records needed to reconstruct proposals and decisions. Saving only the node array loses those records.
const savedDocument = editor.read.value();
const restored = createEditor({
plugins: [BaseParagraphPlugin, DefaultAuthoredPlugin],
initialValue: savedDocument,
userId: "alice",
});const savedDocument = editor.read.value();
const restored = createEditor({
plugins: [BaseParagraphPlugin, DefaultAuthoredPlugin],
initialValue: savedDocument,
userId: "alice",
});Use the same content plugins and schema when loading a revision into a fresh editor. Loading validates the authored records and document schema before publishing the value. Treat meta.authored as codec-owned data. Local undo starts empty.
If you install Comments, save editor.plugin(CommentsPlugin).api.toJSON() atomically with the document and its application revision and conversation generation. Verify that association, then pass the saved CommentsJSON to CommentsPlugin.configure({ initialState: { initialComments } }) in the fresh editor. Live thread targets contain semantic identities; attachment(id) reads their placement. Accepting, rejecting, and undoing a suggestion decision preserve its discussion. Resolving or reopening a thread stays outside document undo.
Historical previews and restores retain current conversations and use only the selected revision's proven targets. See Comments and versions.
Choose accepted or proposed for a clean document, or review when the
format can preserve review records. Clean projections report diagnostics when
they omit pending work or resolve a conflict to one side. Every helper accepts
or captures a complete document, including named roots and metadata.
Use the pure authored projection helpers for detached documents:
import {
parseAuthoredDocument,
projectAuthoredDocument,
projectAuthoredReview,
} from "platejs/authored";
const source = editor.read.value();
const accepted = projectAuthoredDocument(source, {
projection: "accepted",
});
const review = projectAuthoredReview(source);
const json = JSON.stringify(review.review);
const document = parseAuthoredDocument(json);import {
parseAuthoredDocument,
projectAuthoredDocument,
projectAuthoredReview,
} from "platejs/authored";
const source = editor.read.value();
const accepted = projectAuthoredDocument(source, {
projection: "accepted",
});
const review = projectAuthoredReview(source);
const json = JSON.stringify(review.review);
const document = parseAuthoredDocument(json);parseAuthoredDocument validates the document envelope. Loading it into an
editor applies the installed application schema and authored-record checks.
import { serializePlainText } from "platejs";
const text = serializePlainText(editor, { projection: "accepted" });
const markdown = editor.api.markdown.serialize({ projection: "proposed" });
if (markdown.ok) downloadMarkdown(markdown.data);import { serializePlainText } from "platejs";
const text = serializePlainText(editor, { projection: "accepted" });
const markdown = editor.api.markdown.serialize({ projection: "proposed" });
if (markdown.ok) downloadMarkdown(markdown.data);Plain text and Markdown carry visible content only. Feature-owned format mappings preserve the structures they can represent and report omitted roots, metadata, pending work, and unsupported nodes. Markdown does not contain a hidden Plate document and cannot restore exact review state.
import { exportDocx } from "platejs/docx/export";
import { renderStaticHtml } from "platejs/static";
const semanticHtml = editor.api.html.serialize({ projection: "proposed" });
const styledHtml = await renderStaticHtml(editor, { projection: "proposed" });
const docx = await exportDocx(editor, { projection: "review" });import { exportDocx } from "platejs/docx/export";
import { renderStaticHtml } from "platejs/static";
const semanticHtml = editor.api.html.serialize({ projection: "proposed" });
const styledHtml = await renderStaticHtml(editor, { projection: "proposed" });
const docx = await exportDocx(editor, { projection: "review" });renderStaticHtml uses the current editor's static components. Semantic HTML
uses feature mappings and no application styling. Both HTML forms carry visible
accepted or proposed content only. A successful DOCX result returns a Blob in
docx.blob. Every serializer returns structured diagnostics.
| Format | Accepted or proposed | Review |
|---|---|---|
| JSON | Plain projected document | Canonical authored document |
| Plain text | Structural text | Not supported |
| Markdown | Plain Markdown | Not supported; save authored JSON |
| Semantic HTML | Feature-mapped HTML | Not supported; save authored JSON |
| Static HTML | Component-rendered HTML | Not supported; save authored JSON |
| DOCX | Projected Word document | Word revisions plus the canonical authored package part |
DOCX review export reports lossy-content or unsupported-content when Word
cannot express a native construct. The DOCX authored part restores exact data
only when the caller grants authored trust and the visible Word content still
corresponds to that part.
Install the authored plugin and Yjs on every peer, with matching document and schema identities. Plate supplies local History by default.
The Yjs adapter applies authored operations and decisions in one remote transaction. Its checkpoints preserve pending work and each peer's retained content. Retention policy is local: an archival peer can retain closed bodies while another peer compacts them.
See Collaboration for setup and compaction ownership.
Import the standard descriptor from platejs/authored. It resolves the author from the Plate editor's userId and uses default retention. SuggestionPlugin and AIChatPlugin depend on this descriptor.
Create a custom authored descriptor. Import it from platejs/authored.
| Option | Type | Purpose |
|---|---|---|
authorId | string | ((editor) => string | null | undefined) | Required. Resolve a nonempty author ID once per transaction. |
retainHistory | boolean | Keep closed edit bodies for selective revert. Defaults to false. |
After const authored = editor.plugin(DefaultAuthoredPlugin), use these methods:
| Surface | Purpose |
|---|---|
tx.authored.propose(options?) | Start a proposal, assign a new { changeId }, or amend an owned pending change. |
authored.read.canPropose() | Report whether the current identity can author proposals. |
authored.read.view() | Read this view's intent and projection. |
authored.read.change(id) | Read one logical change, or null. |
authored.read.details(id) | Read one change with semantic parts and decision history, or null. |
authored.read.changes(query?) | Read a page filtered by author, status, or time. |
authored.read.changesAt(range) | Read changes intersecting a text range. |
authored.read.select(query) | Capture a revision-bound selection for a decision. |
authored.read.preview(decision) | Check an accept or reject decision without applying it. |
authored.update.decide(decision) | Accept or reject the selected changes atomically. |
authored.update.resolve(decision) | Decide a complete conflict component explicitly. |
authored.update.revert({ selection }) | Compensate retained accepted contributions. |
authored.api.setView(view) | Change the policy on this exact view. |
authored.api.subscribeChanges(listener) | Subscribe to immutable affected change IDs and decoration node keys for this view. |
subscribeChanges is a low-level integration hook for semantic decorators. Suggestion applications use useSuggestionChanges(path) instead.
platejs/authored exports the authored plugin descriptors, change and decision
types, immutable projection results and diagnostics, and
createAuthoredReviewDocument for trusted cumulative imported revisions.
projectAuthoredDocument(document, { projection }) returns one accepted or
proposed document, the exact captured review document, diagnostics, and
separate unresolved.pending and unresolved.conflicted counts.
projectAuthoredReview(document) also exposes stable visible markup segments
and property counterparts. Both helpers are pure document operations: they do
not construct, activate, or subscribe an editor runtime.
projectAuthoredRange(projection, range) maps proposed coordinates into the
accepted document captured by the same projection. parseAuthoredDocument
parses a JSON document envelope; the target editor remains responsible for its
application schema.