Skip to main content
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.
If you need real-time sync between the agent and a frontend editor, add collaboration. The SDK client joins the same Yjs room as the frontend — edits appear live.

Prerequisites

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

Step 1: Install

npm install @superdoc-dev/sdk openai

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.
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.
import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';

const { tools } = await chooseTools({ provider: 'openai' });
const systemPrompt = await getSystemPrompt();

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

Step 5: Save and clean up

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:
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();

Other providers

AWS Bedrock

Use chooseTools({ provider: 'anthropic' }) and convert to Bedrock’s toolSpec shape:
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();
Auth: AWS credentials via aws configure, env vars, or IAM role. No API key needed.
  • LLM tools — tool catalog and SDK functions
  • Best practices — prompting, workflow tips, and tested prompt examples
  • Debugging — troubleshoot tool call failures
  • Collaboration — add real-time sync between agent and frontend
  • SDKs — typed Node.js and Python wrappers