Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Upload Files
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Authored Changes
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting
  • Locations
  • Transactions
  • Serializing
  • Roots
  • Document Meta
  • Clipboard and Paste
  • Decorations and annotations
  • Schema
  • History
  • Pagination
  • Annotations
  • DOM Coverage
  • External Text Views
  • Virtualized Rendering

Authored Changes

PreviousNext

Capture attributed edits, review proposals, retain author history, and export explicit projections.

SuggestionsCommentsCollaboration

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.

Choose the right surface

NeedSurface
Switch between editing and suggestingSuggestionPlugin from platejs/suggestion/react
Add copied highlighting and review cardsSuggestionKit and DiscussionKit
Editing BehaviorPlugin Input Rules

On This Page

Choose the right surfaceInstall authored changesCapture changesImport a compared revisionConfigure a viewReview changesDecision resultsRetain author historySave the documentExport explicit projectionsJSONPlain text and MarkdownHTML and DOCXCollaborationAPI ReferenceDefaultAuthoredPluginauthored(options)Transaction, reads, and updatesTypes and format helpers
Build your editor
Production-ready AI template and reusable components.
Get all-access
Record changes using Plate's userIdDefaultAuthoredPlugin from platejs/authored
Choose a custom author resolver or retain historyauthored(options) from platejs/authored
Read, decide, resolve, or revert changesThe installed authored plugin portal
Attach a conversationCommentsPlugin 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.

Install authored changes

DefaultAuthoredPlugin resolves the author from the Plate editor's userId once per transaction:

editor.ts
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);
editor.ts
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:

history-editor.ts
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);
history-editor.ts
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.

Capture changes

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.

Import a compared revision

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.

Configure a view

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",
});
ProjectionVisible content
acceptedAccepted content without pending proposals
proposedThe proposed result without retained review fragments
markupProposed 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.

Review changes

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.

Decision results

Always inspect the result of decide, resolve, or revert:

StatusMeaning
appliedThe operation applied to the returned IDs.
unchangedNo change was needed.
staleThe selected revisions or heads changed. Read the records again before retrying.
blockedThe selection has unresolved dependencies, dependants, or conflicts. Inspect the returned lists.
invalidThe selection or document identity is invalid.
unavailableA 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.

Retain author history

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 document

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.

Export explicit projections

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.

JSON

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.

Plain text and Markdown

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.

HTML and DOCX

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.

FormatAccepted or proposedReview
JSONPlain projected documentCanonical authored document
Plain textStructural textNot supported
MarkdownPlain MarkdownNot supported; save authored JSON
Semantic HTMLFeature-mapped HTMLNot supported; save authored JSON
Static HTMLComponent-rendered HTMLNot supported; save authored JSON
DOCXProjected Word documentWord 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.

Collaboration

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.

API Reference

DefaultAuthoredPlugin

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.

authored(options)

Create a custom authored descriptor. Import it from platejs/authored.

OptionTypePurpose
authorIdstring | ((editor) => string | null | undefined)Required. Resolve a nonempty author ID once per transaction.
retainHistorybooleanKeep closed edit bodies for selective revert. Defaults to false.

Transaction, reads, and updates

After const authored = editor.plugin(DefaultAuthoredPlugin), use these methods:

SurfacePurpose
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.

Types and format helpers

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.