Documentation Index
Fetch the complete documentation index at: https://superdoc-caio-sd-2929-configurable-toolbar.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
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
OpenAI
Anthropic
Vercel AI
npm install @superdoc-dev/sdk openai
npm install @superdoc-dev/sdk @anthropic-ai/sdk
npm install @superdoc-dev/sdk ai @ai-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' });
Load the tool definitions for your provider and the default system prompt. Both can be cached: they don’t change between requests.
OpenAI
Anthropic
Vercel AI
import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';
const { tools } = await chooseTools({ provider: 'openai' });
const systemPrompt = await getSystemPrompt();
import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';
const { tools } = await chooseTools({ provider: 'anthropic' });
const systemPrompt = await getSystemPrompt();
import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';
const { tools: sdkTools } = await chooseTools({ provider: 'vercel' });
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.
OpenAI
Anthropic
Vercel AI
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:
- The system prompt teaches the model how to use SuperDoc tools.
- The
while(true) loop calls OpenAI, checks for tool calls, dispatches them via dispatchSuperDocTool, and feeds results back.
- When the model returns
finish_reason: 'stop' (no more tool calls), the loop ends.
- Errors are caught and returned as tool results so the model can see what went wrong and retry.
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:
- The system prompt is passed via the
system parameter (not as a message).
- The loop calls Anthropic, checks for
tool_use blocks, dispatches them, and collects tool_result blocks.
- Tool results are sent back as a
user message with an array of tool_result blocks.
- When the model returns
stop_reason: 'end_turn' (no more tool calls), the loop ends.
- Errors use
is_error: true so the model knows the call failed.
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:
- SDK tool definitions are converted into Vercel AI tool objects: each with an
execute function that calls dispatchSuperDocTool.
generateText handles the agent loop internally: it calls the model, executes tools, feeds results back, and repeats.
stopWhen: stepCountIs(10) sets a max iteration guard.
- No manual
while(true) loop needed: Vercel AI manages it for you.
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:
OpenAI
Anthropic
Vercel AI
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();
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();
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();
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();
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()
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:
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.
- 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