> ## Documentation Index
> Fetch the complete documentation index at: https://superdoc-caio-pizzol-docs-ai-core-preset.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Common workflows

> Recommended patterns for working with the Document API

These are the recommended patterns for new integrations.

## Plan with query.match, then apply with mutations

This is the recommended default for most apps: match first, preview, then apply.

```ts theme={null}
const match = editor.doc.query.match({
  select: { type: 'text', pattern: 'foo' },
  require: 'first',
});

const ref = match.items?.[0]?.handle?.ref;
if (!ref) return;

const plan = {
  expectedRevision: match.evaluatedRevision,
  atomic: true,
  changeMode: 'direct',
  steps: [
    {
      id: 'replace-foo',
      op: 'text.rewrite',
      where: { by: 'ref', ref },
      args: { replacement: { text: 'bar' } },
    },
  ],
};

const preview = editor.doc.mutations.preview(plan);
if (preview.valid) {
  editor.doc.mutations.apply(plan);
}
```

## Run multiple edits as one plan

When several changes should stay together, group them into one plan:

```ts theme={null}
const match = editor.doc.query.match({
  select: { type: 'text', pattern: 'payment terms' },
  require: 'first',
});

const ref = match.items?.[0]?.handle?.ref;
if (!ref) return;

const plan = {
  expectedRevision: match.evaluatedRevision,
  atomic: true,
  changeMode: 'direct',
  steps: [
    {
      id: 'rewrite-terms',
      op: 'text.rewrite',
      where: { by: 'ref', ref },
      args: {
        replacement: { text: 'updated payment terms' },
      },
    },
    {
      id: 'format-terms',
      op: 'format.apply',
      where: { by: 'ref', ref },
      args: {
        inline: { bold: 'on' },
      },
    },
  ],
};

const preview = editor.doc.mutations.preview(plan);
if (preview.valid) {
  editor.doc.mutations.apply(plan);
}
```

## Quick search and single edit

For lightweight text edits, use `query.match` and apply against the canonical selection target returned by the match:

```ts theme={null}
const match = editor.doc.query.match({
  select: { type: 'text', pattern: 'foo' },
  require: 'first',
});

const target = match.items?.[0]?.target;
if (target) {
  editor.doc.replace({
    target,
    text: 'bar',
  });
}
```

## Find text and insert at position

Search for a heading (or any text) and insert a new paragraph relative to it:

```ts theme={null}
// 1. Find the heading by text content
const match = editor.doc.query.match({
  select: { type: 'text', pattern: 'Materials and methods' },
  require: 'first',
});

const address = match.items?.[0]?.address;
if (!address) return;

// 2. Insert a paragraph after the heading
editor.doc.create.paragraph({
  at: { kind: 'after', target: address },
  text: 'New section content goes here.',
});
```

The `address` from `query.match` is a `BlockNodeAddress` that works directly with `create.paragraph`, `create.heading`, and `create.table`. Use `kind: 'before'` to insert before the matched node instead.

To insert as a tracked change, pass `changeMode: 'tracked'`:

```ts theme={null}
editor.doc.create.paragraph(
  { at: { kind: 'after', target: address }, text: 'Suggested addition.' },
  { changeMode: 'tracked' },
);
```

<Info>
  Use `query.match` (not `find`) for this workflow. `query.match` returns `BlockNodeAddress` objects that are directly compatible with mutation targets.
</Info>

For direct single-operation calls, prefer `item.target`. For plans or multi-step edits, prefer `item.handle.ref` so every step reuses the same resolved match.

## Chain table mutations with returned refs

For non-destructive table-targeted mutations, reuse `result.table.nodeId` from the previous success result. You do not need an intermediate `find()` between calls.

```ts theme={null}
const created = editor.doc.create.table({
  rows: 2,
  columns: 2,
});

if (!created.success || !created.table) return;

const bordered = editor.doc.tables.setBorder({
  nodeId: created.table.nodeId,
  edge: 'top',
  lineStyle: 'single',
  lineWeightPt: 1,
  color: '000000',
});

if (!bordered.success || !bordered.table) return;

const inserted = editor.doc.tables.insertColumn({
  tableNodeId: bordered.table.nodeId,
  columnIndex: 0,
  position: 'right',
});

if (!inserted.success || !inserted.table) return;

editor.doc.tables.setCellSpacing({
  nodeId: inserted.table.nodeId,
  spacingPt: 2,
});
```

<Info>
  This handoff contract applies to table-targeted calls. Cell-targeted `tables.setBorder`, `tables.clearBorder`, `tables.setShading`, and `tables.clearShading` still return the targeted `tableCell` address today.
</Info>

## Build a selection explicitly with ranges.resolve

Use `ranges.resolve` when you already know the anchor points and want a transparent `SelectionTarget` plus a reusable mutation-ready ref:

```ts theme={null}
const resolved = editor.doc.ranges.resolve({
  start: {
    kind: 'point',
    point: { kind: 'text', blockId: 'p1', offset: 0 },
  },
  end: {
    kind: 'point',
    point: { kind: 'text', blockId: 'p2', offset: 12 },
  },
});

editor.doc.delete({ target: resolved.target });

if (resolved.handle.ref) {
  editor.doc.format.apply({
    ref: resolved.handle.ref,
    inline: { bold: 'on' },
  });
}
```

## Tracked-mode insert

Insert text as a tracked change so reviewers can accept or reject it:

```ts theme={null}
const receipt = editor.doc.insert(
  { value: 'new content' },
  { changeMode: 'tracked' },
);
```

The receipt includes a `resolution` with the resolved insertion point and `inserted` entries with tracked-change IDs.

## Check capabilities before acting

Use `capabilities()` to branch on what the editor supports:

```ts theme={null}
const caps = editor.doc.capabilities();
const target = {
  kind: 'selection',
  start: { kind: 'text', blockId: 'p1', offset: 0 },
  end: { kind: 'text', blockId: 'p1', offset: 3 },
};

if (caps.operations['format.apply'].available) {
  editor.doc.format.apply({
    target,
    inline: { bold: 'on' },
  });
}

if (caps.global.trackChanges.enabled) {
  editor.doc.insert({ value: 'tracked' }, { changeMode: 'tracked' });
}
```

## Cross-session block addressing

When you load a DOCX, close the editor, and load the same file again, `sdBlockId` values change: they're regenerated on every open. For cross-session block targeting, use `query.match` addresses (`NodeAddress` with `kind: 'block'`), which carry DOCX-native `paraId`-derived IDs when available.

This pattern is common in headless pipelines: extract block references in one session, then apply edits in another.

```ts theme={null}
import { Editor } from 'superdoc/super-editor';
import { readFile, writeFile } from 'node:fs/promises';

const docx = await readFile('./contract.docx');

// Session 1: extract block addresses
const editor1 = await Editor.open(docx);
const result = editor1.doc.query.match({
  select: { type: 'node', nodeType: 'paragraph' },
  require: 'any',
});

// Save addresses: for DOCX-imported blocks, nodeId uses paraId when available
const addresses = result.items.map((item) => ({
  address: item.address,
}));
await writeFile('./blocks.json', JSON.stringify(addresses));
editor1.destroy();

// Session 2: load the same file again and apply edits
const editor2 = await Editor.open(docx);
const saved = JSON.parse(await readFile('./blocks.json', 'utf-8'));

// Addresses from session 1 usually resolve when reloading the same unchanged DOCX
for (const { address } of saved) {
  const node = editor2.doc.getNode(address); // works across sessions
}
editor2.destroy();
```

### Navigate to saved addresses in the browser

When the saved addresses are used in a browser-based viewer (RAG citations, search results, cross-references), pass the `nodeId` directly to `scrollToElement`:

```ts theme={null}
// In the browser: user clicks a citation
const savedNodeId = citation.nodeId; // from your database / vector store
const found = await superdoc.scrollToElement(savedNodeId);

if (!found) {
  console.warn('Element no longer exists in the document');
}
```

`scrollToElement` also works with comment and tracked change IDs:

```ts theme={null}
await superdoc.scrollToElement(commentEntityId);
await superdoc.scrollToElement(trackedChangeEntityId);
```

<Info>
  `nodeId` stability depends on the ID source. For DOCX-imported content, `nodeId` comes from `paraId` when available and is best-effort stable across loads. Runtime-created content is still not guaranteed stable across loads; many nodes use session-scoped editor identity, while some structures such as tables or table cells may expose deterministic fallback IDs instead of raw `sdBlockId`.
</Info>

<Warning>
  No ID is guaranteed to survive all Microsoft Word round-trips. Re-extract addresses after major external edits or transformations, since Word (or other tools) may rewrite paragraph IDs and SuperDoc may rewrite duplicate IDs on import.
</Warning>

## Content extraction for RAG

`doc.extract()` returns all document content in one call: blocks with full text, comments, and tracked changes. Each item has a stable ID that works directly with [`scrollToElement`](/editor/superdoc/methods#scrolltoelement).

```ts theme={null}
const content = editor.doc.extract();

// Every block in document order, with full text
for (const block of content.blocks) {
  console.log(block.nodeId, block.type, block.text);
  // → '5AF80E61', 'heading', 'Chapter 1: Introduction'
  // → '17FBFA43', 'paragraph', 'This is the opening paragraph...'
}

// Comments anchored to blocks
for (const comment of content.comments) {
  console.log(comment.entityId, comment.blockId, comment.text);
}

// Tracked changes
for (const tc of content.trackedChanges) {
  console.log(tc.entityId, tc.type, tc.excerpt);
}
```

### RAG pipeline pattern

Extract content, chunk it, store the IDs, and navigate back on click:

```ts theme={null}
// 1. Extract all content
const { blocks } = editor.doc.extract();

// 2. Chunk and embed (your pipeline)
const chunks = blocks
  .filter((b) => b.text.length > 0)
  .map((b) => ({
    id: b.nodeId,
    text: b.text,
    type: b.type,
    headingLevel: b.headingLevel,
  }));
const embeddings = await embedChunks(chunks);

// 3. Store embeddings with nodeIds
await vectorStore.upsert(embeddings);

// 4. Later: user clicks a citation
const citation = await vectorStore.query(userQuestion);
await superdoc.scrollToElement(citation.id);
```

<Info>
  All IDs from `doc.extract()` work directly with `scrollToElement()`: no conversion needed. For DOCX-imported content, block `nodeId` values are stable across sessions.
</Info>

## Read document counts

`doc.info()` returns a snapshot of current document statistics including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, and list counts.

```ts theme={null}
const info = editor.doc.info();

console.log(info.counts.words);      // whitespace-delimited word count
console.log(info.counts.characters); // full text projection length (with spaces)
console.log(info.counts.paragraphs); // excludes headings and list items
console.log(info.counts.headings);   // style-based heading detection
console.log(info.counts.tables);     // top-level table containers
console.log(info.counts.images);     // block + inline images
console.log(info.counts.comments);   // unique anchored comment IDs
console.log(info.counts.trackedChanges); // grouped tracked-change entities
console.log(info.counts.sdtFields);      // field-like SDT/content-control nodes
console.log(info.counts.lists);          // unique list sequences
```

All counts reflect the current editor state, not OOXML metadata. They update naturally as the document changes.

### Build a live counter in the browser

`doc.info()` is a snapshot read. To build a live counter, subscribe to document-change events and refresh counts in the handler: do not poll in a render loop.

**SuperEditor (raw editor):**

```ts theme={null}
editor.on('update', ({ editor }) => {
  const { counts } = editor.doc.info();
  updateDocumentStatsUI({
    words: counts.words,
    characters: counts.characters,
    trackedChanges: counts.trackedChanges,
    sdtFields: counts.sdtFields,
    lists: counts.lists,
  });
});
```

**SuperDoc (wrapper):**

```ts theme={null}
superdoc.on('editor-update', ({ editor }) => {
  const { counts } = editor.doc.info();
  updateDocumentStatsUI({
    words: counts.words,
    characters: counts.characters,
    trackedChanges: counts.trackedChanges,
    sdtFields: counts.sdtFields,
    lists: counts.lists,
  });
});
```

### SDK usage

The SDKs do not expose browser event subscriptions. Call `doc.info()` at workflow boundaries: after opening a document, after a batch of mutations, or before saving.

```ts theme={null}
const doc = await client.open({ doc: './contract.docx' });
const info = await doc.info();
console.log(
  `${info.counts.words} words, ${info.counts.characters} characters, ${info.counts.trackedChanges} tracked changes`,
);
```

## Dry-run preview

Pass `dryRun: true` to validate an operation without applying it:

```ts theme={null}
const preview = editor.doc.insert(
  { target, value: 'hello' },
  { dryRun: true },
);
// preview.success tells you whether the insert would succeed
// preview.resolution shows the resolved target range
```
