.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
Copy
Ask AI
npm install @superdoc-dev/sdk openai
Copy
Ask AI
npm install @superdoc-dev/sdk @anthropic-ai/sdk
Copy
Ask AI
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.
Copy
Ask AI
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.- OpenAI
- Anthropic
- Vercel AI
Copy
Ask AI
import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';
const { tools } = await chooseTools({ provider: 'openai' });
const systemPrompt = await getSystemPrompt();
Copy
Ask AI
import { chooseTools, getSystemPrompt } from '@superdoc-dev/sdk';
const { tools } = await chooseTools({ provider: 'anthropic' });
const systemPrompt = await getSystemPrompt();
Copy
Ask AI
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
Copy
Ask 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 }),
});
}
}
}
- The system prompt teaches the model how to use SuperDoc tools.
- The
while(true)loop calls OpenAI, checks for tool calls, dispatches them viadispatchSuperDocTool, 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.
Copy
Ask AI
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 });
}
- The system prompt is passed via the
systemparameter (not as a message). - The loop calls Anthropic, checks for
tool_useblocks, dispatches them, and collectstool_resultblocks. - Tool results are sent back as a
usermessage with an array oftool_resultblocks. - When the model returns
stop_reason: 'end_turn'(no more tool calls), the loop ends. - Errors use
is_error: trueso the model knows the call failed.
Copy
Ask AI
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);
- SDK tool definitions are converted into Vercel AI tool objects — each with an
executefunction that callsdispatchSuperDocTool. generateTexthandles 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
Copy
Ask AI
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
Copy
Ask 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();
Copy
Ask AI
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();
Copy
Ask AI
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
UsechooseTools({ provider: 'anthropic' }) and convert to Bedrock’s toolSpec shape:
- Node.js
- Python
Copy
Ask AI
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();
Copy
Ask AI
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()
aws configure, env vars, or IAM role. No API key needed.
Related
- 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

