This is a technical decision aid. Check current package and hosted-access details before implementation.
This tutorial builds an AI agent that takes a natural language prompt — "create a 5-slide investor deck about our Q3 results" — and generates a valid, editable PowerPoint file. Claude produces the JSON schema via tool use (function calling). PaperJSX converts the JSON to PPTX. The entire pipeline is 3 API calls: user → Claude → PaperJSX → file. No MCP server required — this is the custom integration path for building document generation into your own product.
How does the prompt → JSON → document flow work?
The key insight: LLMs produce structured JSON reliably via tool use. They do not produce valid OOXML reliably — according to our repair dialog analysis, AI-generated raw PPTX consistently triggers PowerPoint's repair dialog (OpenAI Codex issue #16315). PaperJSX inserts a validation layer between the LLM and the file format: the AI handles content and structure (JSON), PaperJSX handles format compliance (OOXML).
1. Install
npm install @paperjsx/json-to-pptx @anthropic-ai/sdk
How do you define the tool schema?
Claude's tool use requires a JSON Schema definition that describes the function the model can call. This schema teaches Claude the PaperJSX document structure.
export const generateDocumentTool = {
name: "generate_document",
description: "Generate a PowerPoint presentation from a structured JSON schema. Each slide has elements: text, chart, table, or image.",
input_schema: {
type: "object",
properties: {
slides: {
type: "array",
description: "Array of slides. Each slide has an elements array.",
items: {
type: "object",
properties: {
elements: {
type: "array",
items: {
type: "object",
properties: {
type: {
type: "string",
enum: ["text", "chart", "table", "image"]
},
value: { type: "string", description: "For text elements" },
style: {
type: "object",
properties: {
fontSize: { type: "number" },
bold: { type: "boolean" },
color: { type: "string" }
}
},
chartType: { type: "string", enum: ["bar", "line", "pie"] },
data: { type: "object", description: "Chart data with categories and series" },
headers: { type: "array", items: { type: "string" } },
rows: { type: "array", description: "Table rows" }
},
required: ["type"]
}
}
}
}
}
},
required: ["slides"]
}
};
The schema is intentionally minimal — Claude performs better with concise tool schemas than exhaustive ones. The description fields guide the model. For example, Claude learns that chart elements need chartType and data, while text elements need value and optional style.
3. Call Claude with the tool
import Anthropic from "@anthropic-ai/sdk";
import { generateDocumentTool } from "./tool-definition.mjs";
const client = new Anthropic();
export async function generateSchema(userPrompt) {
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: [generateDocumentTool],
tool_choice: { type: "tool", name: "generate_document" },
messages: [{
role: "user",
content: userPrompt
}]
});
// extract the tool_use block
const toolUse = response.content.find(
block => block.type === "tool_use"
);
return toolUse.input; // this is the PaperJSX JSON schema
}
The tool_choice: { type: "tool", name: "generate_document" } forces Claude to always call the tool — no ambiguity about whether the model should respond with text or produce a document. Claude receives the user's natural language instructions and returns a valid PaperJSX JSON schema as the tool call's input.
How do you extract JSON and generate the PPTX?
import { generate } from "@paperjsx/json-to-pptx";
import { writeFileSync } from "node:fs";
import { generateSchema } from "./call-claude.mjs";
const schema = await generateSchema(
"Create a 5-slide investor deck about a SaaS company. Include a title slide, problem slide, solution slide, traction slide with a bar chart showing MRR growth from $10K to $80K over 6 months, and an ask slide requesting $2M seed funding."
);
const buffer = await generate(schema);
writeFileSync("investor-deck.pptx", buffer);
console.log("Investor deck generated");
The output is a 5-slide PowerPoint file with text, a bar chart with real data, and proper slide structure. The chart is native and editable — the investor can click it and see the data table. No repair dialog, no corrupted XML.
Complete working agent
Here is the entire agent in one file — 40 lines
of production code.
import Anthropic from "@anthropic-ai/sdk";
import { generate } from "@paperjsx/json-to-pptx";
import { writeFileSync } from "node:fs";
import { generateDocumentTool } from "./tool-definition.mjs";
const client = new Anthropic();
async function createPresentation(prompt, filename = "output.pptx") {
console.log(`Asking Claude to generate schema...`);
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
tools: [generateDocumentTool],
tool_choice: { type: "tool", name: "generate_document" },
messages: [{ role: "user", content: prompt }]
});
const toolUse = response.content.find(b => b.type === "tool_use");
if (!toolUse) throw new Error("Claude did not call the tool");
console.log(`Schema: ${toolUse.input.slides.length} slides`);
const buffer = await generate(toolUse.input);
writeFileSync(filename, buffer);
console.log(`Saved: ${filename}`);
return filename;
}
// run from command line
const prompt = process.argv.slice(2).join(" ")
|| "Create a 3-slide project status update with a summary, timeline, and risks";
await createPresentation(prompt);
export ANTHROPIC_API_KEY=sk-ant-...
node agent.mjs "Create a quarterly business review deck with revenue chart, customer metrics table, and product roadmap"
Add multi-format support
The same agent produces PDF, DOCX, or XLSX by switching the generator. Add a format field to the tool schema or accept it as a command-line argument.
import { generate as toPptx } from "@paperjsx/json-to-pptx";
import { generate as toPdf } from "@paperjsx/json-to-pdf";
import { generate as toDocx } from "@paperjsx/json-to-docx";
import { generate as toXlsx } from "@paperjsx/json-to-xlsx";
const generators = { pptx: toPptx, pdf: toPdf, docx: toDocx, xlsx: toXlsx };
async function createDocument(prompt, format = "pptx") {
const schema = await generateSchema(prompt);
const gen = generators[format];
const buffer = await gen(schema);
const filename = `output.${format}`;
writeFileSync(filename, buffer);
return filename;
}
The AI produces the same JSON regardless of output format. The format selection is a downstream decision — not part of the LLM's job. This is the same separation of concerns described in the MCP server article: the AI handles content (JSON), the tool handles format compliance.
Custom agent or MCP server?
PaperJSX offers two paths for AI document generation:
- MCP server — install
@paperjsx/mcp-server, add it to Claude Desktop or Cursor, and ask the AI to generate documents directly. Zero custom code. Best for personal productivity and prototyping. See the MCP server setup guide. - Custom agent (this article) — call Claude's API with a tool definition, extract the JSON schema, pass to PaperJSX. Best for building document generation into your own SaaS product, internal tools, or automated workflows where the user never interacts with Claude directly.
The JSON schema is identical in both paths. The difference is who orchestrates the pipeline: the MCP client (Claude Desktop, Cursor) in the MCP path, or your code in the custom agent path.
Keep the workflow in your product.
Use local engines or local MCP to prove the artifact. When release controls matter, start Platform for authenticated hosted delivery or discuss Enterprise terms.