Work with tracked changes

Create reviewable edits, inspect open changes, and accept or reject an explicit change.

Tracked changes connect programmatic editing to human review. Operations that report tracked-mode support can request it. The review API then lists, inspects, accepts, or rejects the resulting logical changes.

For a successful content mutation, inspect receipt.appliedChangeMode to learn the effective mode. The requested changeMode and the Editor's suggesting documentMode describe intent and defaults; neither proves how that call committed. The field is absent for dry runs and no-op results. Edits absorbed into your own existing tracked insertion report tracked even when no new change ID is created.

Create a reviewable edit

Pass changeMode: 'tracked' as the mutation options argument. There is no separate operation for creating a tracked change:

const result = await doc.query.match({
  select: { type: 'text', pattern: 'Amazing' },
  require: 'exactlyOne',
});
const match = result.items[0];

if (!match || match.matchKind !== 'text') {
  throw new Error('The company name was not found.');
}

const receipt = await doc.replace(
  { target: match.target, text: 'Northstar' },
  { changeMode: 'tracked', expectedRevision: result.evaluatedRevision },
);

if (!receipt.success) {
  throw new Error(receipt.failure?.message ?? 'The tracked replacement failed.');
}

The replacement remains open for review. Saving the DOCX preserves that review state until a person or programmatic workflow accepts or rejects it.

Attribute one tracked text edit

Use author: { name } to set the display name on one tracked plain-text insert, delete, or replace without changing the session user:

const author = { name: 'Automated Reviewer' };
await doc.insert({ value: 'Suggested text' }, { changeMode: 'tracked', author });
const toReplace = await doc.query.match({ select: { type: 'text', pattern: 'Amazing' }, require: 'first' });
if (!toReplace.items[0]) throw new Error('No text to replace.');
await doc.replace({ ref: toReplace.items[0].handle.ref, text: 'Northstar' }, { changeMode: 'tracked', author });
const toDelete = await doc.query.match({ select: { type: 'text', pattern: 'Suggested text' }, require: 'first' });
if (!toDelete.items[0]) throw new Error('No text to delete.');
await doc.delete({ ref: toDelete.items[0].handle.ref }, { changeMode: 'tracked', author });

Resolve a fresh target and revision before each dependent edit; refs from an earlier mutation may be stale. The name is copied when you call the method, before a browser worker queues it. Without author, the session's startup author is used. Separate sessions with different startup users remain an alternative.

Only a non-empty name is accepted. This is application-provided display attribution, not a verified identity or permission grant: the integration remains responsible for authenticating callers and authorizing edits. email, initials, and authorImage are not accepted in this option. It is not supported for direct edits, HTML/Markdown, structural writes, lists, or mutations.apply/preview; those option combinations return INVALID_INPUT. Cross-paragraph replacements that restructure the document, cross-cell deletions, and appended control payloads that create paragraphs may be rejected instead of silently changing attribution. In a shared room, presence identifies the session participant, while each surviving tracked revision records the author of its mutation. Concurrent edits can still be rejected by the document's normal conflict rules; the author option does not decide conflicts.

Review the result in the Editor

The Editor owns document rendering, selection, and human review controls. Follow Review tracked changes to open the sample DOCX and accept or reject a proposal visually.

The underlying tracked-change operations remain the same in Editor and Headless hosts.

List and inspect changes

trackChanges.list() returns a compact, paginated result. Call trackChanges.get() when the workflow needs full before/after details for one logical change:

const changes = await doc.trackChanges.list({ limit: 20, offset: 0 });
const change = changes.items[0];

if (!change) {
  throw new Error('The document has no open tracked changes.');
}

const detail = await doc.trackChanges.get({ id: change.id });

console.log({
  id: detail.id,
  type: detail.type,
  author: detail.author,
  before: detail.before,
  after: detail.after,
});

The list searches the document body by default. Pass in: 'all' when the workflow must include supported headers, footers, footnotes, and endnotes.

Quote a replacement safely

A tracked replacement is one logical change with two sides, and the API always keeps them in separate fields. A trackChanges.list entry carries insertedText (the new text) and deletedText (the struck text). trackChanges.get({ id }) returns the full TrackChangeInfo, which adds replacement.inserted.excerpt and replacement.deleted.excerpt for grouped replacements. None of these fields ever contains the other side.

Both shapes also carry a flat excerpt. It is a single preview string, not a pair: for a grouped replacement it is the inserted text, falling back to the deleted text when there is no inserted side. Read it to label a change in a list; read the side-specific fields when you need to know what the change did.

If a consumer sees the struck text and the inserted text run together in one string, it is reading a text projection, not the change. A redline projection deliberately renders both sides in sequence, deleted then inserted, because that is what a redline is. To quote what a change did, read the side you want from the entry; to quote what the document now says, project with reviewMode: 'final'.

Read final, original, or redline output

Use projectHtml() or projectMarkdown() when a workflow needs a serialized review view rather than the tracked-change catalog. final emits accepted-side content, original emits the before-side, and redline identifies representable before/after content with the same logical change IDs returned here. The view applies to structural paragraph, list, and table changes as well as inline text. Moves distinguish source and destination; formatting-only changes emit one content copy with an annotation carrier.

The projection result records annotations removed by the selected view instead of silently dropping their identity. Source targets and maps use tracked coordinates and are valid only at evaluatedRevision. Read the HTML and Markdown projection guide before saving a map for a later review action.

Accept or reject one change

Decide against the logical change ID, and guard the decision with the revision returned by the list operation:

const decisionReceipt = await doc.trackChanges.decide(
  {
    decision: 'accept',
    target: { kind: 'id', id: detail.id },
  },
  {
    expectedRevision: changes.evaluatedRevision,
  },
);

if (!decisionReceipt.success) {
  throw new Error(`${decisionReceipt.failure.code}: ${decisionReceipt.failure.message}`);
}

console.log('Resolved change:', decisionReceipt.removed);

Use decision: 'reject' to restore the change's before-state instead. A review decision resolves an existing change, so changeMode and dryRun do not apply.

Re-list changes after each decision. The receipt can invalidate or remap references, and partial range decisions can create successor fragments with new IDs.

For a complete code-to-human handoff, follow Review tracked changes. The generated trackChanges reference covers range, side, bulk, and story-specific targets.

On this page