> ## 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.

# How to use

> Build an AI agent that edits documents using SuperDoc LLM tools: complete, copy-pasteable examples

Wire up an LLM agent that reads and edits `.docx` files headlessly. Install the SDK, open a document, and run an agentic tool loop. Full working code below.

<Note>
  If you need real-time sync between the agent and a frontend editor, add [collaboration](/editor/collaboration/overview). The SDK client joins the same Yjs room as the frontend: edits appear live.
</Note>

## Prerequisites

* Node.js 18+
* `@superdoc-dev/sdk`
* An LLM provider API key (e.g., `OPENAI_API_KEY`)

## Step 1: Install

<Tabs>
  <Tab title="OpenAI">
    ```bash theme={null}
    npm install @superdoc-dev/sdk openai
    ```
  </Tab>

  <Tab title="Anthropic">
    ```bash theme={null}
    npm install @superdoc-dev/sdk @anthropic-ai/sdk
    ```
  </Tab>

  <Tab title="Vercel AI">
    ```bash theme={null}
    npm install @superdoc-dev/sdk ai @ai-sdk/openai
    ```
  </Tab>
</Tabs>

## Step 2: Open a document

Create an SDK client and open a `.docx` file. `client.open()` returns a document handle you'll pass to the dispatcher.

```typescript theme={null}
import { createSuperDocClient } from '@superdoc-dev/sdk';

const client = createSuperDocClient();
await client.connect();

const doc = await client.open({ doc: './contract.docx' });
```

## Step 3: Load tools and system prompt

Load the tool definitions for your provider and the default system prompt. Both can be cached: they don't change between requests.

<Tabs>
  <Tab title="OpenAI">
    ```typescript theme={null}
    import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';

    const { tools } = await chooseTools({ provider: 'openai' });
    const systemPrompt = await getSystemPrompt();
    ```
  </Tab>

  <Tab title="Anthropic">
    ```typescript theme={null}
    import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';

    const { tools } = await chooseTools({ provider: 'anthropic' });
    const systemPrompt = await getSystemPrompt();
    ```
  </Tab>

  <Tab title="Vercel AI">
    ```typescript theme={null}
    import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';

    const { tools: sdkTools } = await chooseTools({ provider: 'vercel' });
    const systemPrompt = await getSystemPrompt();
    ```
  </Tab>
</Tabs>

## Step 4: Run the agent loop

The agent loop sends messages to the LLM, dispatches tool calls, feeds results back, and repeats until the model is done.

<Tabs>
  <Tab title="OpenAI">
    ```typescript theme={null}
    import OpenAI from 'openai';
    import { dispatchSuperDocTool } from '@superdoc-dev/sdk';

    const openai = new OpenAI(); // uses OPENAI_API_KEY env var

    const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice.' },
    ];

    while (true) {
      const response = await openai.chat.completions.create({
        model: 'gpt-5.4',
        messages,
        tools,
      });

      const choice = response.choices[0];
      messages.push(choice.message);

      // Stop when the model has no more tool calls
      if (choice.finish_reason === 'stop' || !choice.message.tool_calls?.length) {
        console.log(choice.message.content);
        break;
      }

      // Execute each tool call and feed results back
      for (const toolCall of choice.message.tool_calls) {
        if (toolCall.type !== 'function') continue;

        try {
          const result = await dispatchSuperDocTool(
            doc,
            toolCall.function.name,
            JSON.parse(toolCall.function.arguments),
          );
          messages.push({
            role: 'tool',
            tool_call_id: toolCall.id,
            content: JSON.stringify(result),
          });
        } catch (err: any) {
          // Return errors as tool results: the model will self-correct
          messages.push({
            role: 'tool',
            tool_call_id: toolCall.id,
            content: JSON.stringify({ error: err.message }),
          });
        }
      }
    }
    ```

    **What's happening:**

    1. The system prompt teaches the model how to use SuperDoc tools.
    2. The `while(true)` loop calls OpenAI, checks for tool calls, dispatches them via `dispatchSuperDocTool`, and feeds results back.
    3. When the model returns `finish_reason: 'stop'` (no more tool calls), the loop ends.
    4. Errors are caught and returned as tool results so the model can see what went wrong and retry.
  </Tab>

  <Tab title="Anthropic">
    ```typescript theme={null}
    import Anthropic from '@anthropic-ai/sdk';
    import { dispatchSuperDocTool } from '@superdoc-dev/sdk';

    const anthropic = new Anthropic(); // uses ANTHROPIC_API_KEY env var

    const messages: Anthropic.MessageParam[] = [
      { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice.' },
    ];

    while (true) {
      const response = await anthropic.messages.create({
        model: 'claude-sonnet-4-6',
        max_tokens: 4096,
        system: systemPrompt,
        messages,
        tools,
      });

      messages.push({ role: 'assistant', content: response.content });

      // Stop when the model has no more tool calls
      if (response.stop_reason === 'end_turn' || !response.content.some((b) => b.type === 'tool_use')) {
        const textBlock = response.content.find((b) => b.type === 'text');
        console.log(textBlock?.text);
        break;
      }

      // Execute each tool call and feed results back
      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      for (const block of response.content) {
        if (block.type !== 'tool_use') continue;

        try {
          const result = await dispatchSuperDocTool(
            doc,
            block.name,
            block.input as Record<string, unknown>,
          );
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: JSON.stringify(result),
          });
        } catch (err: any) {
          // Return errors as tool results: the model will self-correct
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: JSON.stringify({ error: err.message }),
            is_error: true,
          });
        }
      }

      messages.push({ role: 'user', content: toolResults });
    }
    ```

    **What's happening:**

    1. The system prompt is passed via the `system` parameter (not as a message).
    2. The loop calls Anthropic, checks for `tool_use` blocks, dispatches them, and collects `tool_result` blocks.
    3. Tool results are sent back as a `user` message with an array of `tool_result` blocks.
    4. When the model returns `stop_reason: 'end_turn'` (no more tool calls), the loop ends.
    5. Errors use `is_error: true` so the model knows the call failed.
  </Tab>

  <Tab title="Vercel AI">
    ```typescript theme={null}
    import { generateText, jsonSchema, stepCountIs } from 'ai';
    import { openai } from '@ai-sdk/openai';
    import { dispatchSuperDocTool } from '@superdoc-dev/sdk';

    // Convert SDK tool definitions into Vercel AI tool objects with execute functions
    const tools: Record<string, any> = {};
    for (const t of sdkTools as any[]) {
      const fn = t.function;
      tools[fn.name] = {
        description: fn.description,
        inputSchema: jsonSchema<Record<string, unknown>>(fn.parameters),
        execute: async (args: Record<string, unknown>) => {
          try {
            return await dispatchSuperDocTool(doc, fn.name, args);
          } catch (err: any) {
            return { error: err.message };
          }
        },
      };
    }

    // generateText handles the agent loop internally
    const { text } = await generateText({
      model: openai.chat('gpt-5.4'),
      system: systemPrompt,
      messages: [
        { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice.' },
      ],
      tools,
      stopWhen: stepCountIs(10),
    });

    console.log(text);
    ```

    **What's happening:**

    1. SDK tool definitions are converted into Vercel AI tool objects: each with an `execute` function that calls `dispatchSuperDocTool`.
    2. `generateText` handles the agent loop internally: it calls the model, executes tools, feeds results back, and repeats.
    3. `stopWhen: stepCountIs(10)` sets a max iteration guard.
    4. No manual `while(true)` loop needed: Vercel AI manages it for you.
  </Tab>
</Tabs>

## Step 5: Save and clean up

```typescript theme={null}
await doc.save({ inPlace: true });
await doc.close();
await client.dispose();
```

## Full example

A complete, copy-pasteable script that opens a document, runs an agent, saves, and exits:

<Tabs>
  <Tab title="OpenAI">
    ```typescript theme={null}
    import OpenAI from 'openai';
    import {
      createSuperDocClient,
      chooseTools,
      dispatchSuperDocTool,
      getSystemPrompt,
    } from '@superdoc-dev/sdk';

    // 1. Open the document
    const client = createSuperDocClient();
    await client.connect();
    const doc = await client.open({ doc: './contract.docx' });

    // 2. Load tools and system prompt
    const { tools } = await chooseTools({ provider: 'openai' });
    const systemPrompt = await getSystemPrompt();

    // 3. Build the conversation
    const openai = new OpenAI();
    const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice.' },
    ];

    // 4. Agent loop
    while (true) {
      const response = await openai.chat.completions.create({
        model: 'gpt-5.4',
        messages,
        tools,
      });

      const choice = response.choices[0];
      messages.push(choice.message);

      if (choice.finish_reason === 'stop' || !choice.message.tool_calls?.length) {
        console.log(choice.message.content);
        break;
      }

      for (const toolCall of choice.message.tool_calls) {
        if (toolCall.type !== 'function') continue;

        try {
          const result = await dispatchSuperDocTool(
            doc,
            toolCall.function.name,
            JSON.parse(toolCall.function.arguments),
          );
          messages.push({
            role: 'tool',
            tool_call_id: toolCall.id,
            content: JSON.stringify(result),
          });
        } catch (err: any) {
          messages.push({
            role: 'tool',
            tool_call_id: toolCall.id,
            content: JSON.stringify({ error: err.message }),
          });
        }
      }
    }

    // 5. Save and clean up
    await doc.save({ inPlace: true });
    await doc.close();
    await client.dispose();
    ```
  </Tab>

  <Tab title="Anthropic">
    ```typescript theme={null}
    import Anthropic from '@anthropic-ai/sdk';
    import {
      createSuperDocClient,
      chooseTools,
      dispatchSuperDocTool,
      getSystemPrompt,
    } from '@superdoc-dev/sdk';

    // 1. Open the document
    const client = createSuperDocClient();
    await client.connect();
    const doc = await client.open({ doc: './contract.docx' });

    // 2. Load tools and system prompt
    const { tools } = await chooseTools({ provider: 'anthropic' });
    const systemPrompt = await getSystemPrompt();

    // 3. Build the conversation
    const anthropic = new Anthropic();
    const messages: Anthropic.MessageParam[] = [
      { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice.' },
    ];

    // 4. Agent loop
    while (true) {
      const response = await anthropic.messages.create({
        model: 'claude-sonnet-4-6',
        max_tokens: 4096,
        system: systemPrompt,
        messages,
        tools,
      });

      messages.push({ role: 'assistant', content: response.content });

      if (response.stop_reason === 'end_turn' || !response.content.some((b) => b.type === 'tool_use')) {
        const textBlock = response.content.find((b) => b.type === 'text');
        console.log(textBlock?.text);
        break;
      }

      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      for (const block of response.content) {
        if (block.type !== 'tool_use') continue;

        try {
          const result = await dispatchSuperDocTool(
            doc,
            block.name,
            block.input as Record<string, unknown>,
          );
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: JSON.stringify(result),
          });
        } catch (err: any) {
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: JSON.stringify({ error: err.message }),
            is_error: true,
          });
        }
      }

      messages.push({ role: 'user', content: toolResults });
    }

    // 5. Save and clean up
    await doc.save({ inPlace: true });
    await doc.close();
    await client.dispose();
    ```
  </Tab>

  <Tab title="Vercel AI">
    ```typescript theme={null}
    import { generateText, jsonSchema, stepCountIs } from 'ai';
    import { openai } from '@ai-sdk/openai';
    import {
      createSuperDocClient,
      chooseTools,
      dispatchSuperDocTool,
      getSystemPrompt,
    } from '@superdoc-dev/sdk';

    // 1. Open the document
    const client = createSuperDocClient();
    await client.connect();
    const doc = await client.open({ doc: './contract.docx' });

    // 2. Load tools and system prompt
    const { tools: sdkTools } = await chooseTools({ provider: 'vercel' });
    const systemPrompt = await getSystemPrompt();

    // 3. Convert SDK tools into Vercel AI tool objects
    const tools: Record<string, any> = {};
    for (const t of sdkTools as any[]) {
      const fn = t.function;
      tools[fn.name] = {
        description: fn.description,
        inputSchema: jsonSchema<Record<string, unknown>>(fn.parameters),
        execute: async (args: Record<string, unknown>) => {
          try {
            return await dispatchSuperDocTool(doc, fn.name, args);
          } catch (err: any) {
            return { error: err.message };
          }
        },
      };
    }

    // 4. Run the agent (loop handled by generateText)
    const { text } = await generateText({
      model: openai.chat('gpt-5.4'),
      system: systemPrompt,
      messages: [
        { role: 'user', content: 'Find the termination clause and rewrite it to allow 30-day notice.' },
      ],
      tools,
      stopWhen: stepCountIs(10),
    });

    console.log(text);

    // 5. Save and clean up
    await doc.save({ inPlace: true });
    await doc.close();
    await client.dispose();
    ```
  </Tab>
</Tabs>

## Other providers

### AWS Bedrock

Use `chooseTools({ provider: 'anthropic' })` and convert to Bedrock's `toolSpec` shape:

<Tabs>
  <Tab title="Node.js">
    ```typescript theme={null}
    import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime';
    import { createSuperDocClient, chooseTools, dispatchSuperDocTool } from '@superdoc-dev/sdk';

    const client = createSuperDocClient();
    await client.connect();
    const doc = await client.open({ doc: './contract.docx' });

    // Get tools in Anthropic format, convert to Bedrock toolSpec shape
    const { tools } = await chooseTools({ provider: 'anthropic' });
    const toolConfig = {
      tools: tools.map((t) => ({
        toolSpec: {
          name: t.name,
          description: t.description,
          inputSchema: { json: t.input_schema },
        },
      })),
    };

    const bedrock = new BedrockRuntimeClient({ region: 'us-east-1' });
    const messages = [
      { role: 'user', content: [{ text: 'Review this contract.' }] },
    ];

    while (true) {
      const res = await bedrock.send(new ConverseCommand({
        modelId: 'us.anthropic.claude-sonnet-4-6',
        messages,
        system: [{ text: 'You edit .docx files using SuperDoc tools. Use tracked changes for all edits.' }],
        toolConfig,
      }));

      const output = res.output?.message;
      if (!output) break;
      messages.push(output);

      const toolUses = output.content?.filter((b) => b.toolUse) ?? [];
      if (!toolUses.length) break;

      const results = [];
      for (const block of toolUses) {
        const { name, input, toolUseId } = block.toolUse;
        const result = await dispatchSuperDocTool(doc, name, input ?? {});
        const json = typeof result === 'object' && result !== null ? result : { result };
        results.push({ toolResult: { toolUseId, content: [{ json }] } });
      }
      messages.push({ role: 'user', content: results });
    }

    await doc.save();
    await doc.close();
    await client.dispose();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import boto3
    from superdoc import SuperDocClient, choose_tools, dispatch_superdoc_tool

    client = SuperDocClient()
    client.connect()
    doc = client.open({"doc": "./contract.docx"})

    # Get tools in Anthropic format, convert to Bedrock toolSpec shape
    sd_tools = choose_tools({"provider": "anthropic"})
    tool_config = {
        "tools": [
            {
                "toolSpec": {
                    "name": t["name"],
                    "description": t["description"],
                    "inputSchema": {"json": t.get("input_schema", {})},
                }
            }
            for t in sd_tools["tools"]
        ]
    }

    bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
    messages = [{"role": "user", "content": [{"text": "Review this contract."}]}]

    while True:
        response = bedrock.converse(
            modelId="us.anthropic.claude-sonnet-4-6",
            messages=messages,
            system=[{"text": "You edit .docx files using SuperDoc tools. Use tracked changes for all edits."}],
            toolConfig=tool_config,
        )

        output = response["output"]["message"]
        messages.append(output)

        tool_uses = [b for b in output.get("content", []) if "toolUse" in b]
        if not tool_uses:
            break

        tool_results = []
        for block in tool_uses:
            tu = block["toolUse"]
            result = dispatch_superdoc_tool(doc, tu["name"], tu.get("input", {}))
            json_result = result if isinstance(result, dict) else {"result": result}
            tool_results.append(
                {"toolResult": {"toolUseId": tu["toolUseId"], "content": [{"json": json_result}]}}
            )
        messages.append({"role": "user", "content": tool_results})

    doc.save({})
    doc.close({})
    client.dispose()
    ```
  </Tab>
</Tabs>

**Auth**: AWS credentials via `aws configure`, env vars, or IAM role. No API key needed.

## Streaming generated text into a visible editor

Sometimes you don't need a full agent loop. You just want the model to write into the document while the user watches. Stream the output through a small backend proxy and append each delta to the editor:

```ts theme={null}
for await (const chunk of streamFromServer(prompt, signal)) {
  buffer += chunk;
  if (chunk.includes('\n')) flush();
  else if (!pendingFlush) pendingFlush = setTimeout(flush, 150);
}

function flush() {
  editor.doc.insert({ value: buffer, type: 'text' });
  buffer = '';
}
```

`editor.doc.insert` is the public Document API. With no `target`, content appends at the end. Newlines from the model become real paragraph breaks.

A few things to get right:

* **Keep the model key on the server.** A small Node proxy that forwards Server-Sent Events keeps the key out of client bundles.
* **Buffer deltas.** Inserting on every token causes one document mutation per token, which floods the layout engine and undo stack. Flush on a timer (\~150ms) or whenever a newline arrives.
* **Abort on unmount and Stop.** Tie an `AbortController` to the fetch and call it from your cleanup. The server should also abort upstream when the client disconnects so neither side burns tokens.

Full working example: [examples/ai/streaming](https://github.com/superdoc-dev/superdoc/tree/main/examples/ai/streaming).

## Related

* [LLM tools](/ai/agents/llm-tools): tool catalog and SDK functions
* [Best practices](/ai/agents/best-practices): prompting, workflow tips, and tested prompt examples
* [Debugging](/ai/agents/debugging): troubleshoot tool call failures
* [Collaboration](/editor/collaboration/overview): add real-time sync between agent and frontend
* [SDKs](/document-engine/sdks): typed Node.js and Python wrappers
