This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/document-authoring/working-with-documents/programmatic-editing.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Programmatic document editing in Document Authoring

Before you start, ensure you have the Document Authoring library installed and running. For more information, check out the getting started guides.

Document Authoring includes a programmatic API for reading and updating document content through DocAuthDocument.transaction(). Transactions are atomic and isolated until committed, and they provide access to a draft document you can inspect or modify.

Any error handling in the examples below is left out for brevity.

Running a transaction

Use DocAuthDocument.transaction() to work with a draft document:

// Assuming the `editor` instance exists.
const currentDoc = editor.currentDocument();
await currentDoc.transaction(async ({ draft }) => {
const firstSection = draft.body().sections()[0];
const paragraph = firstSection.content().addParagraph();
paragraph.asTextView().setText('Created in a transaction.');
return { commit: true };
});

Read paragraph list formatting

As of 1.14.0, you can inspect paragraph list state with Paragraph.list():

const currentDoc = editor.currentDocument();
await currentDoc.transaction(async ({ draft }) => {
const firstSection = draft.body().sections()[0];
const blockLevels = firstSection.content().blocklevels();
for (const blockLevel of blockLevels) {
if (blockLevel.type !== 'paragraph') continue;
const list = blockLevel.list();
if (!list) {
console.log('Not a list item');
continue;
}
if ('unsupported' in list) {
console.log('List format is present, but not classified by the API');
continue;
}
console.log(`List kind: ${list.kind}, level: ${list.level}`);
}
return { commit: false };
});

Paragraph.list() returns:

  • null when the paragraph isn’t a list item.
  • A supported list state with kind ('bullet' or 'numbered') and level.
  • An unsupported result for imported or custom list formats the API can’t safely classify.

Apply paragraph list formatting

Use BlockLevelContainer.setParagraphList() to apply or clear list formatting for a contiguous range of paragraphs:

const currentDoc = editor.currentDocument();
await currentDoc.transaction(async ({ draft }) => {
const content = draft.body().sections()[0].content();
const start = content.blocklevels().length;
content.addParagraph().asTextView().setText('Draft announcement');
content.addParagraph().asTextView().setText('Prepare demo');
content.addParagraph().asTextView().setText('Send follow-up');
content.setParagraphList(
{ index: start, count: 3 },
{ kind: 'bullet' },
);
return { commit: true };
});

Within the same transaction, you can remove list formatting while keeping the paragraph text by using kind: 'none':

content.setParagraphList(
{ index: start, count: 3 },
{ kind: 'none' },
);

You can also copy list formatting from an existing paragraph with BlockLevelContainer.applyParagraphListFrom(). In the example below, start comes from the previous snippet. This is useful when you want target paragraphs to continue the same list formatting or start a new list instance with the same format:

const sourceIndex = start;
const fourth = content.addParagraph();
fourth.asTextView().setText('Share notes');
const fifth = content.addParagraph();
fifth.asTextView().setText('Schedule review');
content.applyParagraphListFrom(
sourceIndex,
{ index: start + 3, count: 2 },
{ list: 'startNew' },
);

Query and manage comment threads

As of 1.14.0, you can query document comment threads through draft.commentThreads():

const currentDoc = editor.currentDocument();
await currentDoc.transaction(async ({ draft }) => {
const openThreads = draft.commentThreads().all({ status: 'open' });
for (const thread of openThreads) {
console.log(thread.comment.text);
}
return { commit: false };
});

Create a new text-anchored comment thread by passing a plain-text body and one or more text anchor input ranges:

const currentDoc = editor.currentDocument();
await currentDoc.transaction(async ({ draft }) => {
const paragraph = draft.body().sections()[0].content().addParagraph();
const textView = paragraph.asTextView();
textView.setText('The supplier must provide reviewed text before signing.');
const match = textView.searchText('reviewed text');
if (!match) {
return { commit: false };
}
draft.commentThreads().add({
body: 'Can Legal confirm this wording?',
anchor: {
type: 'text',
ranges: [{ textView, range: match.range }],
},
});
return { commit: true };
});

After querying or creating a thread, you can manage it with CommentThread methods such as:

  • reply(body)
  • edit(body)
  • resolve()
  • unresolve()
  • remove()

For example:

await currentDoc.transaction(async ({ draft }) => {
const [thread] = draft.commentThreads().all({ status: 'open' });
if (thread) {
thread.reply('Checked by automated review.');
thread.resolve();
}
return { commit: true };
});

Use review mode in transactions

As of 1.14.0, TransactionOptions.review lets you run supported document mutations in review mode so they’re authored as tracked revisions:

const currentDoc = editor.currentDocument();
await currentDoc.transaction(
async ({ draft }) => {
const paragraph = draft.body().sections()[0].content().addParagraph();
paragraph.asTextView().setText('This change is authored in review mode.');
return { commit: true };
},
{
review: {
author: 'Nutrient Docs',
},
},
);

Supported mutations are recorded as revisions for the supplied author. Mutations without tracked-change markup support still apply directly.

Comment thread mutations also work inside review transactions, but they don’t create tracked-change revisions.

Learn more