Automation

Automate a DOCX with Python

Accept tracked changes and save a separate DOCX from Python.

Use the Python SDK when application code already knows which document operation to run. This guide accepts every tracked change in a DOCX and writes the result to a separate file.

1. Install the Python SDK

SuperDoc supports Python 3.9 and newer:

python -m pip install superdoc-sdk

The package installs the matching SuperDoc CLI companion automatically. You do not need Node.js or a separate CLI installation. Published companions support macOS on Apple Silicon and Intel, Linux on ARM64 and x64, and Windows on x64.

2. Prepare a document

Download the tracked-changes fixture and save it as contract.docx in your working directory. It contains one tracked change.

Download the tracked-changes fixtureSynthetic NDA with one tracked changeDOCX

3. Accept the changes and save

Create accept_changes.py:

from superdoc import SuperDocClient


def is_successful_receipt(value: object) -> bool:
    return isinstance(value, dict) and value.get("success") is True


with SuperDocClient() as client:
    document = client.open({"doc": "./contract.docx"})

    try:
        receipt = document.track_changes.decide(
            {
                "decision": "accept",
                "target": {"kind": "all"},
            }
        )
        if not is_successful_receipt(receipt):
            raise RuntimeError("Accepting tracked changes failed.")

        document.save(
            {
                "out": "./contract.accepted.docx",
                "force": True,
            }
        )
    finally:
        document.close({"discard": True})

Run it from the directory containing contract.docx:

python accept_changes.py

SuperDocClient owns one persistent host process. The context manager starts it on entry and disposes it on exit. The document still has its own lifecycle, so the finally block closes the session even when a mutation or save fails.

4. Verify the output

Open contract.accepted.docx in Microsoft Word or the SuperDoc editor. Every tracked change should be accepted. The original contract.docx should remain unchanged.

Reuse one client across many operations

One SuperDocClient owns one SuperDoc host process. connect() (or entering the context manager) starts that process once. Every open() adds a document session inside the same process, and every method on a document handle is one request to it. The SDK never starts a process per operation.

A client created per operation pays the whole cold path every time: process start, engine load, and for a collaboration document the room connect and initial sync. That cost is the same for a read, a search, or a comment, so it dominates any workflow that issues many small operations. Hoist the client and the open out of the per-operation path:

from superdoc import SuperDocClient

# Once per service, or once per collaboration room you serve.
with SuperDocClient() as client:
    document = client.open(
        {
            "doc": "contract-123",
            "collaboration": {
                "providerType": "hocuspocus",
                "url": "wss://collaboration.example.com",
                "documentId": "contract-123",
            },
        }
    )
    try:
        # Many times per agent turn. Each call reuses the open session.
        info = document.info({})
        matches = document.query.match(
            {"select": {"type": "text", "pattern": "termination"}, "require": "all"}
        )
    finally:
        # When the room or the service shuts down.
        document.close({})

The same shape applies to AsyncSuperDocClient; only the await keywords change.

Typical cost of each lifecycle for a 53-operation agent turn on a mid-sized document. Both SDKs drive the same host process over the same protocol, so the numbers apply equally to Python:

LifecycleMedian per callWhole turn
New client per operation~460 ms~24 s
One client, open and close per operation~140 ms~8 s
One client, one open document~40 ms~3 s

What that lifetime means in practice:

  • A handle is a session. Open a document once and keep the handle for as long as the workflow needs it. One client can hold several open documents; each handle is its own session.
  • One host runs one request at a time. Operations on the same client queue. For parallel throughput across documents, run a small pool of clients and lease one per in-flight workflow.
  • Restart is automatic, sessions are not. If the host process exits, the next request starts a new one and every previously open handle is gone; reopen the document.
  • Refs expire, node ids do not. A query.match() ref is bound to the revision that produced it and stops resolving after any mutation. Block nodeId values stay stable across edits, so target follow-up work with {"kind": "block", "nodeType": ..., "nodeId": ...} and re-run query.match() only when you need a text range.

Batch many operations into one request

Two document-level batches collapse several operations into one request:

  • document.mutations.apply(plan) commits several text or formatting steps as one atomic transaction. Selector targets re-resolve before each step, so you do not re-search between them. See Mutation plans.
  • document.plan.execute({"entries": [...]}) runs a list of arbitrary operations in one request. Each entry is its own transaction, so a failure partway through keeps the earlier entries committed.

For example, anchor a batch of comments in a single request instead of one request per comment:

result = document.plan.execute(
    {
        "entries": [
            {
                "operationId": "comments.create",
                "input": {"target": {"text": quote}, "text": note},
            }
            for quote, note in review_notes
        ]
    }
)

# plan.execute reports a failed entry in "failure" rather than raising. An
# ignored result is an ignored batch: it stops at the first hard failure, so
# later entries never ran.
failure = result.get("failure")
if failure:
    raise RuntimeError(
        f"comment batch stopped at entry {failure['entryIndex']} "
        f"({failure['operationId']}): {failure['message']}"
    )

Use the asynchronous client

Use AsyncSuperDocClient when the surrounding application already runs an event loop. Its document methods mirror the synchronous client and are awaited:

import asyncio

from superdoc import AsyncSuperDocClient


async def main():
    async with AsyncSuperDocClient() as client:
        document = await client.open({"doc": "./contract.docx"})
        try:
            info = await document.info({})
            print(info["counts"])
        finally:
            await document.close({"discard": True})


asyncio.run(main())

Open a token-authenticated collaboration document

Pass a Hocuspocus or y-websocket token with the document's open request:

document = await client.open(
    {
        "doc": "./contract.docx",
        "collaboration": {
            "providerType": "hocuspocus",
            "url": "wss://collaboration.example.com",
            "documentId": "contract-123",
        },
    },
    collaboration_auth={"type": "token", "token": user_token},
)

The credential is separate from the collaboration profile and command-line arguments. It applies to the provider created for this open request. Per-open token authentication is not supported for Liveblocks or the standalone document host.

Use the Node.js SDK for the same deterministic workflow from JavaScript or TypeScript. For the operation and receipt model shared by both SDKs, continue with the Document API mental model.

On this page