# Automate a DOCX with Rust

> Accept tracked changes and save a separate DOCX from Rust.



Use the Rust 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. Later sections build on the same project.

## 1. Install the Rust SDK [#1-install-the-rust-sdk]

The SDK requires Rust 1.85 or newer and uses Tokio. Create a project and add the crate:

```bash
cargo new accept-changes
cd accept-changes
cargo add superdoc-sdk
cargo add serde_json@1
cargo add tokio@1 --features macros,rt
```

Cargo selects the latest stable release and records the resolved version in `Cargo.lock`.

## 2. Prepare a document [#2-prepare-a-document]

Download the [tracked-changes fixture](/fixtures/tracked-changes.docx) and save it as `contract.docx` in the project
directory. It contains one tracked change.

[Download the tracked-changes fixture](/fixtures/tracked-changes.docx): Synthetic NDA with one tracked change · DOCX


> **Keep the source file (warning)**
>
> Write the first result to a separate path so you can compare the output with the original document.


## 3. Accept the changes and save [#3-accept-the-changes-and-save]

Replace `src/main.rs` with:

```rs
use std::{error::Error, path::Path};

use serde_json::json;
use superdoc_sdk::{
    generated::types::{
        DocCloseBoundParams, DocOpenParams, DocSaveBoundParams,
        DocTrackChangesDecideBoundParams,
    },
    installation::{host_target, RuntimeManifest},
    Client, Document,
};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
    let manifest = RuntimeManifest::pinned()?;
    let (cache, target) = (Path::new(".superdoc-runtime"), host_target()?);
    let runtime = manifest
        .resolve(cache, target)
        .or_else(|_| manifest.install(cache, target))?;
    let client = Client::connect(runtime.host_options()?).await?;

    let result = accept_contract(&client).await;

    client.shutdown().await?;
    result
}

async fn accept_contract(client: &Client) -> Result<(), Box<dyn Error>> {
    let document = client
        .open(&DocOpenParams::new().with_doc("./contract.docx".into()))
        .await?;

    let result = accept_all(&document).await;

    document
        .close(&DocCloseBoundParams::new().with_discard(true))
        .await?;
    result
}

async fn accept_all(document: &Document) -> Result<(), Box<dyn Error>> {
    let decision: DocTrackChangesDecideBoundParams =
        serde_json::from_value(json!({
            "decision": "accept",
            "target": { "kind": "all" }
        }))?;
    document.api().track_changes().decide(&decision).await?;

    document
        .api()
        .save(
            &DocSaveBoundParams::new()
                .with_out("./contract.accepted.docx".into())
                .with_force(true),
        )
        .await?;
    Ok(())
}

```

Run it from the directory containing `contract.docx`:

```bash
cargo run
```

The program does four things:

1. **Installs the runtime.** The SDK drives a native SuperDoc process. `resolve` uses the runtime already in
   `.superdoc-runtime`. On the first run nothing is there, so `install` downloads the runtime that matches the crate
   (about 90 MB) and verifies it. Add `.superdoc-runtime` to `.gitignore`.
2. **Opens the document.** `Client::connect` starts the process. `client.open` loads `contract.docx` into a document
   session.
3. **Accepts and saves.** `accept_all` accepts every tracked change and writes `contract.accepted.docx`. If an operation
   fails, it returns an error instead of a receipt. `with_force(true)` replaces an output file left by a previous run.
4. **Cleans up.** `accept_contract` closes the session even when `accept_all` fails, and `main` stops the process
   even when opening or closing the document fails. `with_discard(true)` closes the session without saving its changes
   back to `contract.docx`.

## 4. Verify the output [#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.

> **Verification target (success)**
>
> The output contains the same document content and formatting with no pending tracked changes. The source file remains
> unchanged.


## Call any Document API operation [#call-any-document-api-operation]

`accept_all` called one operation from the shared [Document API reference](/document-api/reference). Every operation
in that reference has a Rust method on `document.api()`, with namespaces and names in snake case:

| Reference                      | Rust                                               |
| ------------------------------ | -------------------------------------------------- |
| `doc.trackChanges.decide(...)` | `document.api().track_changes().decide(...).await` |
| `doc.comments.create(...)`     | `document.api().comments().create(...).await`      |
| `doc.getText(...)`             | `document.api().get_text(...).await`               |
| `doc.query.match(...)`         | `document.api().query().match_(...).await`         |

`match` is a Rust keyword, so that method is `match_`.

Each method takes a generated parameter type. Build simple ones with the constructor and `with_*` methods, as
`DocSaveBoundParams` does in the quickstart. For inputs with nested targets, copy the JSON from the reference into
`json!` and convert it with `serde_json::from_value`, as `accept_all` does. Keep the reference's camelCase keys, such as
`trackedChangeId`, inside `json!`.

The [Rust API reference on docs.rs](https://docs.rs/superdoc-sdk/latest/superdoc_sdk/) lists every generated type and
method. Select the docs.rs version that matches your installed crate.

## Add several comments in one request [#add-several-comments-in-one-request]

Each method call is one request to the SuperDoc process. To make many related edits, send them together with
`document.api().plan().execute(...)`. Save this function as `src/comments.rs`:

```rs
use std::error::Error;

use serde_json::json;
use superdoc_sdk::{
    generated::types::DocPlanExecuteBoundParams, wire::Presence, Document,
};

pub async fn add_review_comments(
    document: &Document,
    review_notes: &[(&str, &str)],
) -> Result<(), Box<dyn Error>> {
    let entries: Vec<_> = review_notes
        .iter()
        .map(|(quote, note)| {
            json!({
                "operationId": "comments.create",
                "input": { "target": { "text": quote }, "text": note }
            })
        })
        .collect();
    let plan: DocPlanExecuteBoundParams =
        serde_json::from_value(json!({ "entries": entries }))?;
    let result = document.api().plan().execute(&plan).await?;

    if let Presence::Present(failure) = result.failure {
        return Err(format!(
            "comment {} ({}) failed: {}",
            failure.entry_index.get(),
            failure.operation_id,
            failure.message
        )
        .into());
    }
    Ok(())
}

```

In `src/main.rs`, add `mod comments;` below the `use` lines. Then call the function in `accept_all`, before the save:

```rust
comments::add_review_comments(
    document,
    &[
        ("Confidential Information", "Is this definition broad enough?"),
        ("Receiving Party", "Confirm the legal name."),
    ],
)
.await?;
```

Run `cargo run` again. `contract.accepted.docx` now has two comments.

Each entry in a plan runs as its own transaction. The plan stops at the first entry that fails, and the entries before
it stay applied, so check `failure` before you save. For a group of text or formatting edits that must succeed or fail
together, use `document.api().mutations().apply(...)` instead. See [Mutation plans](/document-api/mutation-plans).

> **Comment on a tracked change one call at a time (warning)**
>
> A comment that targets a tracked change by its ID does not resolve inside `plan.execute`. Create those comments with
> individual `document.api().comments().create(...)` calls.


## Handle errors [#handle-errors]

When the document rejects an operation, the method returns a `superdoc_sdk::Error`. Match on its `code` to handle the
cases your application expects. This function adds a comment only when the quoted text exists:

```rs
use std::error::Error;

use serde_json::json;
use superdoc_sdk::{generated::types::DocCommentsCreateBoundParams, Document};

pub async fn comment_if_present(
    document: &Document,
    quote: &str,
    note: &str,
) -> Result<bool, Box<dyn Error>> {
    let params: DocCommentsCreateBoundParams = serde_json::from_value(json!({
        "target": { "text": quote },
        "text": note
    }))?;

    match document.api().comments().create(&params).await {
        Ok(_) => Ok(true),
        Err(error) if error.code == "TARGET_NOT_FOUND" => Ok(false),
        Err(error) if error.outcome_unknown => {
            Err(format!("{error}; check the document before retrying").into())
        }
        Err(error) => Err(error.into()),
    }
}

```

The [Receipts and errors](/document-api/receipts-and-errors) page lists the codes each operation can return. An error
also carries `operation_id`, `details`, and the original `host_message`. `Display` leaves out the host message because it
can contain document text.

If a request times out or is cancelled after a write was sent, `outcome_unknown` is `true`: the edit may or may not
have been applied. The SDK never retries a write, so read the document again before you decide to retry.

## Run in a service [#run-in-a-service]

The quickstart starts one SuperDoc process, makes one edit, and stops. A service should start the process once and keep
it:

* **Keep one client.** `Client::connect` starts a process, which is the slowest step. Connect when the service starts,
  not for each request.
* **Keep documents open between edits.** Each `client.open` returns a document session. One client can hold several.
* **Use a small pool for parallel work.** One client runs one operation at a time. Give each concurrent workflow its own
  client.
* **Close everything explicitly.** Dropping a `Document` does not save it. Call `close`, then
  `client.shutdown()` when the service stops.
* **Re-query after edits.** A `query.match` result points at the document as it was when you ran the query. Run the
  query again after a change. Block `nodeId` values stay stable across edits.

> **Rust does not restart the process (warning)**
>
> If the SuperDoc process exits, every request on that client fails. Connect a new client and open the documents again.
> Changes that were not saved are lost.


## Install the runtime before deployment [#install-the-runtime-before-deployment]

The quickstart downloads the runtime the first time it runs. In production, install it during the build instead, so
the service starts without downloading anything:

```rs
use std::path::Path;

use superdoc_sdk::{
    installation::{host_target, RuntimeManifest},
    Client, Result,
};

pub fn install_runtime(cache: &Path) -> Result<()> {
    RuntimeManifest::pinned()?.install(cache, host_target()?)?;
    Ok(())
}

pub async fn connect_installed_runtime(cache: &Path) -> Result<Client> {
    let runtime = RuntimeManifest::pinned()?.resolve(cache, host_target()?)?;
    Client::connect(runtime.host_options()?).await
}

```

Call `install_runtime` in a build step or Docker layer. At startup, `connect_installed_runtime` uses
`resolve`, which verifies the installed files and never downloads. Both functions block, so call them before other async
work starts or wrap them in `tokio::task::spawn_blocking`. For a machine without network access, copy the runtime
archive there and call `manifest.install_archive(cache, target, archive_path)`.

Published runtimes support macOS (Apple Silicon and Intel), Linux with glibc (x64 and ARM64), and Windows x64. musl,
WASM, and Windows ARM64 are not supported.

## Open a collaboration document [#open-a-collaboration-document]

To edit a document that people are also editing in the browser, join its collaboration room. This function connects to
a Hocuspocus server with a token your application issued:

```rs
use std::error::Error;

use serde_json::json;
use superdoc_sdk::{
    generated::types::DocOpenParams, Client, CollaborationAuth, Document,
    OpenOptions,
};

pub async fn join_collaboration_document(
    client: &Client,
    url: &str,
    document_id: &str,
    token: &str,
) -> Result<Document, Box<dyn Error>> {
    let params: DocOpenParams = serde_json::from_value(json!({
        "collaboration": {
            "providerType": "hocuspocus",
            "url": url,
            "documentId": document_id,
            "roomMode": "join"
        }
    }))?;
    let options = OpenOptions {
        collaboration_auth: Some(CollaborationAuth::token(token)?),
        ..OpenOptions::default()
    };
    Ok(client.open_with_options(&params, options).await?)
}

```

Call it with the server URL, the room's document ID, and the token:

```rust
let document = join_collaboration_document(
    &client,
    "wss://collaboration.example.com",
    "contract-123",
    &token,
)
.await?;
```

The returned `Document` supports the same `document.api()` operations as a local file. For a y-websocket server, set
`providerType` to `y-websocket`. Token authentication is not available for Liveblocks.

`roomMode: "join"` expects the room to exist. To create a room from a DOCX, add `"doc": "./contract.docx"` and use
`"roomMode": "create"` in one worker only. See [Coordinate room creation](/editor/collaboration/coordinate-room-creation).
The SDK does not include a collaboration server, and closing a document does not mean the server has stored it. See
[Run a collaboration server](/editor/collaboration/run-a-server) and
[Save and restore a room](/editor/collaboration/save-and-restore-a-room).

## Record who changed what [#record-who-changed-what]

A Rust backend can join a room as an observer and copy attributed change records into its own store with `document.api().changes().list(&params).await`. See [Record who changed what](/editor/collaboration/record-changes).

## Next steps [#next-steps]

* Read the [Document API mental model](/document-api/mental-model) for how targets, receipts, and revisions work.
* Find operations in the [Document API reference](/document-api/reference).
* Use the [Node.js SDK](/agents/automation/node-sdk) or [Python SDK](/agents/automation/python-sdk) for the same
  workflow in those languages.
