Skip to main content
The SuperDoc SDK ships tool definitions that give LLMs structured access to document operations: reading, searching, editing, formatting, lists, tables, comments, and tracked changes. Pick a provider format, pass the tools to your model, dispatch the calls, and the SDK handles schema formatting, argument validation, and execution.

How the pieces fit

Your loop is the broker: the model only ever sees tools, a system prompt, and tool results; documents live in sessions inside the CLI host the SDK spawns; the browser editor renders the same file for the user. Three rules prevent most first-hour confusion:
  1. The SDK is server-sidedispatch needs a session-bound handle from createSuperDocClient().open(...); it does not run in the browser.
  2. The editor is browser-side — never import superdoc / @superdoc-dev/react in backend code or API routes.
  3. Pair everything from one preset — tools, system prompt, and dispatch must come from the same preset (the toolkit guarantees this).
The full mechanics — what crosses the SDK ↔ CLI boundary, sessions and revisions, a complete tool-call round trip with sequence diagrams, and where Python and MCP fit — have their own page: How it works.

Two presets

The SDK ships two tool surfaces. Pass the same preset to chooseTools, getSystemPrompt, and dispatchSuperDocTool.
legacy (default)core
Surface10 grouped intent tools (superdoc_edit, superdoc_search, …)2 tools: superdoc_inspect + superdoc_perform_action (40 named actions)
StyleLow-level: search for handles, then edit by addressHigh-level: named product verbs with deterministic targeting
ResultsOperation resultsReceipts with pre/post evidence and verification
Tracked changesVia changeMode on individual opsFirst-class: every mutating action is redline-aware, plus accept/reject/undo/redo actions
Best forFine-grained control, existing integrationsAgent loops, review/redlining workflows, fastest correct results
Use the core preset for new integrations. Legacy remains the default only for backwards compatibility — existing integrations keep working unchanged. Core scores measurably higher on our revision-fidelity evals and returns verifiable receipts. Each preset has its own reference page: core · legacy.
Both presets are also served over MCP: the SuperDoc MCP server registers the legacy intent tools by default, or the core action surface with MCP_PRESET=core (two tools plus session lifecycle, with the core MCP instructions).

Quick start

Install the SDK, create a client, open a document, and wire up an agentic loop.
npm install @superdoc-dev/sdk openai
import { createSuperDocClient, createAgentToolkit } from '@superdoc-dev/sdk';
import OpenAI from 'openai';

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

// One call — tools, system prompt, and dispatch, guaranteed coherent.
const { tools, systemPrompt, dispatch } = await createAgentToolkit({
  provider: 'openai',
  preset: 'core',
});
const openai = new OpenAI();

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

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

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

  if (!message.tool_calls?.length) break;

  for (const call of message.tool_calls) {
    const result = await dispatch(doc, call.function.name, JSON.parse(call.function.arguments));
    messages.push({
      role: 'tool',
      tool_call_id: call.id,
      content: JSON.stringify(result),
    });
  }
}

await doc.save({ inPlace: true });
await doc.close();
await client.dispose();

Tool selection

The one-call setup — tools, system prompt, and a pre-bound dispatcher that always agree on preset and exclusions:
import { createAgentToolkit } from '@superdoc-dev/sdk';

const { tools, systemPrompt, dispatch, meta } = await createAgentToolkit({
  provider: 'openai',
  preset: 'core',
  excludeActions: ['delete_table'],   // applied to tools, prompt, AND dispatch
});

// in the loop:
const receipt = await dispatch(doc, call.function.name, JSON.parse(call.function.arguments));
The toolkit makes preset/exclusion mismatches impossible by construction — an excluded action is simultaneously out of the tool enum, out of the system prompt, and refused at dispatch. The standalone functions below remain available when you need the pieces individually; if you use them with excludeActions, pass the same list to all three. chooseTools() returns provider-formatted tool definitions plus metadata about the selection.
import { chooseTools } from '@superdoc-dev/sdk';

const { tools, meta } = await chooseTools({
  provider: 'openai',   // 'openai' | 'anthropic' | 'vercel' | 'generic'
  preset: 'core',       // omit for the default 'legacy' surface
});

// meta = { preset: 'core', provider: 'openai', toolCount: 2, cacheStrategy: 'disabled' }
// cacheStrategy: 'disabled' | 'explicit' | 'automatic' | 'unsupported' —
// this call returns 'disabled'; anthropic with cache: true returns 'explicit'.

Legacy tool catalog

The legacy preset’s 10 grouped intent tools, their behaviors (superdoc_search require semantics, ref expiry, superdoc_mutations batching), and the migration mapping to core actions now live on the legacy preset page.

Dispatching tool calls

dispatchSuperDocTool() resolves a tool name to the correct SDK method, validates arguments, and executes the call against a bound document handle.
import { dispatchSuperDocTool } from '@superdoc-dev/sdk';

const result = await dispatchSuperDocTool(doc, toolName, args, { preset: 'core' });
The dispatcher validates required parameters, rejects unknown arguments, and throws descriptive errors the LLM can act on. doc must be the session-bound handle from client.open(...) — a plain object or a browser editor instance will not work.

System prompt

getSystemPrompt(preset?) returns the prompt each tool surface was designed — and evaluated — with. It teaches the model the document vocabulary the tools use (blocks, ordinals, markers, visual sections), when to inspect before editing, how to read results, and the tracked-changes rules.
import { getSystemPrompt } from '@superdoc-dev/sdk';

const legacyPrompt = await getSystemPrompt();        // legacy surface
const corePrompt = await getSystemPrompt('core');    // action surface
Guidance:
  • Use it as-is as your system message, or as the first section of one.
  • Extend, don’t replace: append your product’s instructions (tone, guardrails, domain language) after it. The prompt’s tool-usage sections encode behavior the schemas alone can’t teach; dropping it measurably degrades edit quality.
  • Pair prompt and tools from the same preset — the prompt documents exactly the surface the model was given.

Provider formats

Each provider gets tool definitions in its native format:
const { tools } = await chooseTools({ provider: 'openai', preset: 'core' });
// [{ type: 'function', function: { name, description, parameters } }]
Agent loops are not provider-interchangeable. The tool definitions adapt automatically, but the message protocol does not: OpenAI uses message.tool_calls + role: "tool" replies; Anthropic uses tool_use content blocks + role: "user" messages containing tool_result blocks. Budget for a per-provider loop — the Anthropic variant is below.

Anthropic loop

import Anthropic from '@anthropic-ai/sdk';
import { chooseTools, getSystemPromptForProvider, dispatchSuperDocTool } from '@superdoc-dev/sdk';

const anthropic = new Anthropic();
// cache: true on both halves of the static prefix — the tool array and the
// system prompt — so Anthropic caches them across turns (see Token budget).
const { tools } = await chooseTools({ provider: 'anthropic', preset: 'core', cache: true });
const sys = await getSystemPromptForProvider({ provider: 'anthropic', preset: 'core', cache: true });

const messages: Anthropic.MessageParam[] = [
  { role: 'user', content: 'Rewrite the termination clause for 30-day notice, tracked.' },
];

while (true) {
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-5',
    max_tokens: 4096,
    system: sys.content, // text blocks carrying cache_control markers
    tools,
    messages,
  });
  messages.push({ role: 'assistant', content: response.content });

  const toolUses = response.content.filter((block) => block.type === 'tool_use');
  if (toolUses.length === 0) break;

  const results = [];
  for (const use of toolUses) {
    const result = await dispatchSuperDocTool(doc, use.name, use.input, { preset: 'core' });
    results.push({ type: 'tool_result', tool_use_id: use.id, content: JSON.stringify(result) });
  }
  messages.push({ role: 'user', content: results });
}

Token budget

Tool schemas and the system prompt are re-sent on every turn, and every tool result lives in conversation history forever. Untended, a typical loop crosses low-tier per-minute token ceilings within a few turns. What the SDK gives you and what to do yourself:
  • Prompt caching (Anthropic) — pass cache: true to chooseTools({ provider: 'anthropic', cache: true, ... }): the SDK marks the tool array with cache_control: {type: 'ephemeral'} so the static prefix is cached across turns (~90% cost reduction on the cached portion). For the other half of the prefix, getSystemPromptForProvider({ provider: 'anthropic', cache: true }) returns the system prompt as cacheable system blocks — pass its content as the system parameter (the Anthropic loop shows both together).
  • Narrow the surfaceexcludeActions (core preset) removes actions from the schema and the prompt in one move.
  • Windowed reads — on large documents, inspect in block windows (blockOffset/blockLimit) instead of pulling the whole document into history; with legacy superdoc_get_content action:"text", be aware the full text lands in history on every use.
  • Receipts are pre-capped — core-preset receipts cap long per-item lists at 8 entries with count fields, specifically to keep history lean.
  • Plan for 429s — tier-1 accounts should implement exponential backoff and history truncation from day one.

Error codes

Runtime errors carry a stable code your loop (and your model) can branch on:
CodeMeaningRecoverable?
REVISION_MISMATCHA ref/handle from before a mutation was used after it (legacy) or the session revision guard failedYes — re-search / re-inspect and retry
AMBIGUOUS_MATCHexactlyOne matched several occurrencesYes — narrow the pattern or use all
MATCH_NOT_FOUNDTarget text/element not found; nothing was changedYes — re-inspect, fix the target
INVALID_ARGUMENT / INVALID_INPUTBad or unknown arguments (includes actions excluded by configuration)Fix the call
TOOL_DISPATCH_NOT_FOUNDTool name unknown to the selected presetFix preset/tool pairing
TOOLS_ASSET_NOT_FOUND / TOOLS_ASSET_UNREADABLEBundled prompt asset missing vs. unreadable (IO/permissions — details carry the cause)Environment issue
HOST_HANDSHAKE_FAILEDCLI host binary could not startNo — fix the environment (see below)
Core-preset action failures additionally return structured recovery hints (reinspect / retry / revert with a paste-ready call) inside the receipt.

Troubleshooting: Host process disconnected

This one error has several distinct causes — check in order:
  1. macOS Gatekeeper killed the unsigned binary (SIGKILL at launch). Check xattr -d com.apple.quarantine <binary> / your MDM policy.
  2. Unsupported Node version — the SDK supports current LTS versions, but doesn’t declare engines, so npm won’t warn you at install time. Check node --version first.
  3. The host crashed mid-call — enable transport debug logs (DEBUG=superdoc.transport) to see the host’s stderr and exit code.
  4. Next.js bundling — mark the SDK as external (serverExternalPackages: ['@superdoc-dev/sdk']) so the native binary isn’t bundled away.

Streaming status to your UI

The agent loop is the natural place to emit progress events — each tool call is a meaningful step. Server-sent events sketch:
// Express/Node SSE endpoint around the agent loop
for (const call of message.tool_calls) {
  const args = JSON.parse(call.function.arguments);
  send({ type: 'tool_start', tool: call.function.name, action: args.action ?? null });

  const receipt = (await dispatchSuperDocTool(doc, call.function.name, args, {
    preset: 'core',
  })) as { status?: string; verificationPassed?: boolean };

  send({
    type: 'tool_done',
    tool: call.function.name,
    action: args.action ?? null,
    status: receipt.status ?? 'ok',         // core receipts: ok | partial | failed
    verified: receipt.verificationPassed ?? null,
  });
  messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(receipt) });
}
send({ type: 'assistant_message', text: finalText });
Core-preset receipts make the events meaningful for users: action names read like product verbs (“replace_text”, “add_comments”), and status/verified let you render success/warning states without parsing prose. For the final message, instruct the model (in your appended system-prompt section) to end with a short user-facing summary of what changed — receipts give it the evidence to be specific.

Creating custom tools

Custom capabilities are documented per preset:

SDK functions

FunctionDescription
createAgentToolkit(input)One call: coherent {tools, systemPrompt, dispatch, meta} for a preset (recommended)
chooseTools(input)Load tool definitions for a provider (preset, excludeActions, cache options)
dispatchSuperDocTool(doc, name, args, options?)Execute a tool call against a bound document handle
listTools(provider, preset?)List all tool definitions for a provider
getToolCatalog(preset?)Load the full tool catalog with metadata
getSystemPrompt(preset?, options?)Read the bundled system prompt for a tool surface
getSystemPromptForProvider(input)System prompt shaped for a provider — for anthropic with cache: true, returns system blocks carrying cache_control (Node only; in Python build the block from get_system_prompt)