Text and ranges

Read document text and insert, replace, clear, or delete content through bodies, paragraphs, and ranges.

Use Body for a story, Paragraph for a paragraph, and Range for an addressed span. context.document.body is the main story. document.paragraphs and document.body.paragraphs cover its paragraphs, including table cells and nested controls. They do not aggregate headers, footers, or notes. See Page layout and stories for those bodies.

Create a runtime with Runtime and setup. Use context and document proxies inside runtime.run().

Refresh ranges after edits

A Range stores paragraph identities and UTF-16 offsets from the moment it was found. It does not follow later edits inside those paragraphs. For example, a range over alpha still names offsets zero through five after text is inserted before it. A later load can therefore read different text at those positions. Deleting an endpoint paragraph invalidates the range.

After a successful edit, search again before acting on another target in that paragraph. For the inserted text itself, use the Range returned by insertText() after sync. Tracking a proxy across runs preserves its lifetime; it does not turn its range into a live text region. This differs from Word's moving range behavior.

Read text without exporting proxies

Load text before reading it:

const text = await runtime.run(async (context) => {
  const body = context.document.body;
  body.load('text');
  await context.sync();
  return body.text;
});

Body.text joins paragraphs with carriage returns (\r). Paragraph.text reads paragraph text; Range.text reads the addressed content. Text reads follow the runtime's revisionTextView option.

Some document elements use a single character in API text, regardless of their displayed glyph:

Document elementAPI textRange lengthinsertText() behavior
Non-breaking hyphenU+001EOne UTF-16 unitRestores the non-breaking hyphen element
Optional hyphenU+001FOne UTF-16 unitRestores the optional hyphen element
Symbol (w:sym)(One UTF-16 unitInserts a literal parenthesis, not a symbol

Hyphen elements can survive a text read and write through these characters. This does not preserve arbitrary document structure, such as symbols or fields. Convert the hyphen characters before passing text to readers that require ordinary text.

Search never matches a symbol. A search for ( does not find it, and search does not join the text on either side. For hyphen matching rules, see Search and navigation.

The API does not expose HTML, OOXML strings, or document-wide start and end offsets. Paragraph.uniqueLocalId identifies a paragraph within the runtime; load it before reading. Do not use it as a permanent identifier across separately opened files.

Choose an insertion location

The location types describe the supported call shapes:

ReceiverinsertText(text, location)insertParagraph(text, location)
BodyReplace, Start, EndStart, End
ParagraphReplace, Start, EndBefore, After
RangeReplace, Start, End, Before, AfterBefore, After

The exported aliases are BodyInsertTextLocation, BodyInsertParagraphLocation, ParagraphInsertTextLocation, RangeInsertTextLocation, and BesideLocation. InsertLocation provides matching enum values, such as InsertLocation.replace. A method only accepts the locations shown in its signature.

insertText() returns the inserted Range. insertParagraph() returns the created Paragraph. Sync before configuring or reading the returned object. Use one paragraph insertion per new paragraph; newline-containing insertion text is refused.

Text inserted into an empty paragraph takes the character style that the paragraph mark names. It does not take the mark's direct run properties, such as a direct font size. To apply those, set font on the returned range after sync.

This example appends a paragraph and configures it after creation:

await runtime.run(async (context) => {
  const paragraph = context.document.body.insertParagraph('Review complete.', 'End');
  await context.sync();
  paragraph.spaceAfter = 12;
  const range = paragraph.getRange('Content');
  range.load('text');
  await context.sync();
  console.log(range.text);
});

Address existing content

Body.getRange(location?) and Paragraph.getRange(location?) accept Whole, Content, Start, End, Before, and After. The default is Whole. Use Content when you want the owner's text, and an endpoint for an insertion point. Range.paragraphs enumerates paragraphs intersecting the span.

Supported read-derived ranges accept text writes within one sync. For example, queue paragraph.getRange('End').insertText(' suffix', 'Before'), then call await context.sync(). Sync before inspecting loaded properties or reusing an object returned by a write. Other dependent operations can require a separate sync.

For an exact replacement, use search and verify the match count:

await runtime.run(async (context) => {
  const matches = context.document.body.search('within 7 days', {
    matchCase: true,
  });
  matches.load('items');
  await context.sync();
  if (matches.items.length !== 1) throw new Error('Choose one clause');
  const replacement = matches.items[0]!.insertText('within 30 days', 'Replace');
  await context.sync();
  replacement.load('text');
  await context.sync();
  console.log(replacement.text);
});

See Search and navigation for search options and bookmarks.

Batch plain-text replacements

Disjoint text replacements and insertions can share one sync in ordinary paragraphs with direct text runs. The batch interprets their targets using its initial snapshot coordinates. Returned inserted ranges account for all supported text edits in that batch. Multiple insertions at one explicit snapshot point keep their call order. Paragraph.insertText(..., 'Start') and Body.insertText(..., 'Start') prepend on each call; 'End' appends on each call. This behavior does not make old ranges follow later batches.

The runtime rejects overlapping edits and batches that combine text edits with formatting, hyperlinks, or structural changes in the same paragraph. Use separate syncs, then obtain fresh ranges before the dependent operation. Rich paragraphs and browser suggesting edits support one text edit per paragraph per sync. On ConflictingChanges, read again and revise the batch before retrying.

Clear or delete content

MemberEffect
Body.clear()Clears supported story content and leaves one empty paragraph
Paragraph.clear()Clears paragraph content while retaining the paragraph
Paragraph.delete()Deletes the addressed paragraph
Range.clear() or Range.delete()Deletes the addressed range content

If a body has no paragraphs, clear() fails with InvalidArgument. These methods do not flatten protected or unsupported structures to force an edit. Cross-cell boundaries, pending revisions, and preserved structures can cause atomic refusal. Read the target again before choosing another edit.

Structural edits sharing a paragraph can conflict within one sync. Separate dependent edits and reconsider their targets after each successful transaction. For tracked replacements or deletions, use Tracked changes. Tracked range edits support adjacent sibling paragraphs outside collaboration, including paragraphs within one cell. Table and wrapper boundaries refuse. Collaborative tracked range edits must remain within one paragraph. Whole-body replacement and paragraph deletion remain unsupported while tracking.

Reach other document objects

Body exposes font, style, lists, tables, inlinePictures, shapes, fields, contentControls, bookmarks, and revisions. Body.getComments() returns story comments. Range exposes font, style, hyperlink, tables, inlinePictures, fields, and bookmarks. A range can create a table, picture, field, content control, comment, or break through its insertion methods. Paragraph exposes font, style, list, listItem, and shapes. Use the API member directory to find each dedicated guide and exact signatures.

For shape enumeration and text-box editing, see Page layout and stories. Range.shapes is not available.

Next steps