office

Generate Microsoft Office documents and PDFs with pure JavaScript, no native dependencies. Create DOCX, XLSX, PDF, and PPTX files with TypeScript; all libraries run in Node.js, browsers, and Cloudflare Workers DOCX supports paragraphs, tables, images, and text formatting; XLSX handles formulas, multiple sheets, and JSON-to-sheet conversion PDF includes text, shapes, images, form filling, and page merging; PPTX covers slides, charts, tables, and master layouts Common pitfalls: remember PDF coordinates start at bottom-left, always await Packer.toBuffer() for DOCX, use base64 images in Workers instead of remote URLs

INSTALLATION
npx skills add https://github.com/jezweb/claude-skills --skill office
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

*PPTX in Workers: Works for local images/data. Remote image fetching needs workaround (uses https module).

Quick Start

Installation

# Install all four (or pick what you need)

npm install docx xlsx pdf-lib pptxgenjs

Create a Word Document (30 seconds)

import { Document, Packer, Paragraph, TextRun } from 'docx';

import { writeFileSync } from 'fs';

const doc = new Document({

  sections: [{

    children: [

      new Paragraph({

        children: [

          new TextRun({ text: 'Hello World', bold: true, size: 48 }),

        ],

      }),

    ],

  }],

});

// Node.js: Save to file

const buffer = await Packer.toBuffer(doc);

writeFileSync('hello.docx', buffer);

// Browser/Workers: Get as blob

const blob = await Packer.toBlob(doc);

Create an Excel Spreadsheet (30 seconds)

import * as XLSX from 'xlsx';

// Create workbook with data

const data = [

  ['Name', 'Amount', 'Date'],

  ['Invoice #1', 1500, '2026-01-12'],

  ['Invoice #2', 2300, '2026-01-13'],

];

const worksheet = XLSX.utils.aoa_to_sheet(data);

const workbook = XLSX.utils.book_new();

XLSX.utils.book_append_sheet(workbook, worksheet, 'Invoices');

// Node.js: Save to file

XLSX.writeFile(workbook, 'invoices.xlsx');

// Browser/Workers: Get as buffer

const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });

Create a PDF (30 seconds)

import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';

const pdfDoc = await PDFDocument.create();

const page = pdfDoc.addPage([612, 792]); // Letter size

const font = await pdfDoc.embedFont(StandardFonts.Helvetica);

page.drawText('Hello World', {

  x: 50,

  y: 700,

  size: 24,

  font,

  color: rgb(0, 0, 0),

});

// Get as bytes (works everywhere)

const pdfBytes = await pdfDoc.save();

// Node.js: Save to file

writeFileSync('hello.pdf', pdfBytes);

Create a PowerPoint (30 seconds)

import pptxgen from 'pptxgenjs';

const pptx = new pptxgen();

pptx.author = 'Your Name';

pptx.title = 'Sample Presentation';

// Add a slide

const slide = pptx.addSlide();

slide.addText('Hello World', {

  x: 1, y: 1, w: 8, h: 1.5,

  fontSize: 36, bold: true, color: '363636',

});

// Node.js: Save to file

await pptx.writeFile({ fileName: 'hello.pptx' });

// Browser: Trigger download

await pptx.writeFile({ fileName: 'hello.pptx' });

DOCX: Word Documents

Key Concepts

The docx package uses a builder pattern:

  • Document - The root container
  • Section - Contains pages (most docs have 1 section)
  • Paragraph - A line/block of text
  • TextRun - Formatted text within a paragraph
  • Table - Grid of TableRow → TableCell → Paragraph

Document with Headings and Paragraphs

import { Document, Packer, Paragraph, TextRun, HeadingLevel } from 'docx';

const doc = new Document({

  sections: [{

    children: [

      new Paragraph({

        text: 'Monthly Report',

        heading: HeadingLevel.HEADING_1,

      }),

      new Paragraph({

        children: [

          new TextRun('This is a '),

          new TextRun({ text: 'bold', bold: true }),

          new TextRun(' and '),

          new TextRun({ text: 'italic', italics: true }),

          new TextRun(' text example.'),

        ],

      }),

    ],

  }],

});

Tables

import { Document, Table, TableRow, TableCell, Paragraph, WidthType } from 'docx';

const table = new Table({

  width: { size: 100, type: WidthType.PERCENTAGE },

  rows: [

    new TableRow({

      children: [

        new TableCell({ children: [new Paragraph('Header 1')] }),

        new TableCell({ children: [new Paragraph('Header 2')] }),

      ],

    }),

    new TableRow({

      children: [

        new TableCell({ children: [new Paragraph('Data 1')] }),

        new TableCell({ children: [new Paragraph('Data 2')] }),

      ],

    }),

  ],

});

const doc = new Document({

  sections: [{ children: [table] }],

});

Images

import { Document, Paragraph, ImageRun } from 'docx';

import { readFileSync } from 'fs';

const imageBuffer = readFileSync('logo.png');

const doc = new Document({

  sections: [{

    children: [

      new Paragraph({

        children: [

          new ImageRun({

            data: imageBuffer,

            transformation: { width: 200, height: 100 },

            type: 'png',

          }),

        ],

      }),

    ],

  }],

});

Export Patterns

// Node.js - Save to file

import { writeFileSync } from 'fs';

const buffer = await Packer.toBuffer(doc);

writeFileSync('document.docx', buffer);

// Browser - Download

const blob = await Packer.toBlob(doc);

const url = URL.createObjectURL(blob);

const a = document.createElement('a');

a.href = url;

a.download = 'document.docx';

a.click();

// Cloudflare Workers - Return as Response

const buffer = await Packer.toBuffer(doc);

return new Response(buffer, {

  headers: {

    'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',

    'Content-Disposition': 'attachment; filename="document.docx"',

  },

});

XLSX: Excel Spreadsheets

Key Concepts

SheetJS (xlsx) uses utility functions:

  • Workbook - Container for sheets
  • Worksheet - Single sheet (grid of cells)
  • utils.aoa_to_sheet - Array of arrays → worksheet
  • utils.json_to_sheet - JSON array → worksheet
  • utils.book_new/book_append_sheet - Create/add sheets

From Array of Arrays

import * as XLSX from 'xlsx';

const data = [

  ['Product', 'Price', 'Quantity', 'Total'],

  ['Widget A', 10, 5, { f: 'B2*C2' }],  // Formula

  ['Widget B', 15, 3, { f: 'B3*C3' }],

  ['', '', 'Grand Total:', { f: 'SUM(D2:D3)' }],

];

const ws = XLSX.utils.aoa_to_sheet(data);

const wb = XLSX.utils.book_new();

XLSX.utils.book_append_sheet(wb, ws, 'Sales');

From JSON

const invoices = [

  { id: 1, customer: 'Acme Corp', amount: 1500, date: '2026-01-12' },

  { id: 2, customer: 'Globex', amount: 2300, date: '2026-01-13' },

];

const ws = XLSX.utils.json_to_sheet(invoices);

const wb = XLSX.utils.book_new();

XLSX.utils.book_append_sheet(wb, ws, 'Invoices');

Column Widths

// Set column widths (in characters)

ws['!cols'] = [

  { wch: 10 },  // Column A

  { wch: 20 },  // Column B

  { wch: 15 },  // Column C

];

Multiple Sheets

const wb = XLSX.utils.book_new();

const summaryData = [['Total Sales', 3800]];

const detailData = [['Item', 'Amount'], ['Item 1', 1500], ['Item 2', 2300]];

XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(summaryData), 'Summary');

XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(detailData), 'Details');

Export Patterns

// Node.js - Save to file

XLSX.writeFile(workbook, 'report.xlsx');

// Browser/Workers - Get buffer

const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });

// Cloudflare Workers - Return as Response

return new Response(buffer, {

  headers: {

    'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',

    'Content-Disposition': 'attachment; filename="report.xlsx"',

  },

});

PDF: PDF Documents

Key Concepts

pdf-lib uses a page-based approach:

  • PDFDocument - The PDF file
  • PDFPage - Individual page
  • Coordinates - Origin at BOTTOM-LEFT (not top-left!)
  • Units - Points (72 points = 1 inch)

Standard Page Sizes

// Common sizes in points [width, height]

const LETTER = [612, 792];    // 8.5" x 11"

const A4 = [595.28, 841.89];  // 210mm x 297mm

const LEGAL = [612, 1008];    // 8.5" x 14"

Text and Fonts

import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';

const pdfDoc = await PDFDocument.create();

const page = pdfDoc.addPage([612, 792]);

// Embed standard fonts

const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);

const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);

// Draw text (y=0 is BOTTOM of page)

page.drawText('Invoice #12345', {

  x: 50,

  y: 750,  // Near top of page

  size: 24,

  font: helveticaBold,

  color: rgb(0, 0, 0),

});

page.drawText('Thank you for your business!', {

  x: 50,

  y: 50,  // Near bottom of page

  size: 12,

  font: helvetica,

  color: rgb(0.5, 0.5, 0.5),

});

Drawing Shapes

// Rectangle

page.drawRectangle({

  x: 50,

  y: 600,

  width: 200,

  height: 100,

  borderColor: rgb(0, 0, 0),

  borderWidth: 1,

  color: rgb(0.95, 0.95, 0.95),  // Fill color

});

// Line

page.drawLine({

  start: { x: 50, y: 500 },

  end: { x: 550, y: 500 },

  thickness: 1,

  color: rgb(0, 0, 0),

});

Images

// From URL/fetch

const imageBytes = await fetch('https://example.com/logo.png').then(r => r.arrayBuffer());

const image = await pdfDoc.embedPng(imageBytes);

// Or embedJpg for JPEG

// const image = await pdfDoc.embedJpg(imageBytes);

page.drawImage(image, {

  x: 50,

  y: 700,

  width: 100,

  height: 50,

});

Fill PDF Forms

import { PDFDocument } from 'pdf-lib';

// Load existing PDF with form

const existingPdfBytes = await fetch('form.pdf').then(r => r.arrayBuffer());

const pdfDoc = await PDFDocument.load(existingPdfBytes);

// Get form and fields

const form = pdfDoc.getForm();

const nameField = form.getTextField('customer_name');

const dateField = form.getTextField('date');

// Fill fields

nameField.setText('John Doe');

dateField.setText('2026-01-12');

// Flatten form (make fields non-editable)

form.flatten();

const pdfBytes = await pdfDoc.save();

Merge PDFs

import { PDFDocument } from 'pdf-lib';

const pdf1Bytes = await fetch('doc1.pdf').then(r => r.arrayBuffer());

const pdf2Bytes = await fetch('doc2.pdf').then(r => r.arrayBuffer());

const mergedPdf = await PDFDocument.create();

const pdf1 = await PDFDocument.load(pdf1Bytes);

const pdf2 = await PDFDocument.load(pdf2Bytes);

// Copy all pages from both documents

const pages1 = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());

const pages2 = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());

pages1.forEach(page => mergedPdf.addPage(page));

pages2.forEach(page => mergedPdf.addPage(page));

const mergedBytes = await mergedPdf.save();

PPTX: PowerPoint Presentations

Key Concepts

pptxgenjs uses a slide-based approach:

  • Presentation - The PPTX file (new pptxgen())
  • Slide - Individual slide (addSlide())
  • Elements - Text, images, shapes, tables, charts
  • Units - Inches by default (can change to cm/points)

Basic Presentation

import pptxgen from 'pptxgenjs';

const pptx = new pptxgen();

pptx.author = 'Author Name';

pptx.title = 'Presentation Title';

pptx.subject = 'Subject';

pptx.company = 'Company';

// Title slide

const titleSlide = pptx.addSlide();

titleSlide.addText('Quarterly Report', {

  x: 0.5, y: 2, w: 9, h: 1,

  fontSize: 44, bold: true, color: '0066CC', align: 'center',

});

titleSlide.addText('Q1 2026', {

  x: 0.5, y: 3.5, w: 9, h: 0.5,

  fontSize: 24, color: '666666', align: 'center',

});

Slide with Bullet Points

const contentSlide = pptx.addSlide();

// Title

contentSlide.addText('Key Highlights', {

  x: 0.5, y: 0.5, w: 9, h: 0.8,

  fontSize: 32, bold: true, color: '333333',

});

// Bullet points

contentSlide.addText([

  { text: 'Revenue up 25% YoY', options: { bullet: true, fontSize: 20 } },

  { text: 'Customer base grew to 10,000', options: { bullet: true, fontSize: 20 } },

  { text: 'New product launch successful', options: { bullet: true, fontSize: 20 } },

  { text: 'Expanded to 5 new markets', options: { bullet: true, fontSize: 20 } },

], { x: 0.5, y: 1.5, w: 8, h: 4, valign: 'top' });

Tables

const tableSlide = pptx.addSlide();

tableSlide.addText('Sales Summary', {

  x: 0.5, y: 0.5, w: 9, h: 0.8,

  fontSize: 28, bold: true,

});

const tableData = [

  [{ text: 'Region', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },

   { text: 'Q1', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } },

   { text: 'Q2', options: { bold: true, fill: '0066CC', color: 'FFFFFF' } }],

  ['North America', '$2.5M', '$2.8M'],

  ['Europe', '$1.8M', '$2.1M'],

  ['Asia Pacific', '$1.2M', '$1.5M'],

];

tableSlide.addTable(tableData, {

  x: 0.5, y: 1.5, w: 9,

  border: { pt: 1, color: 'CCCCCC' },

  fontFace: 'Arial',

  fontSize: 14,

  align: 'center',

  valign: 'middle',

});

Charts

const chartSlide = pptx.addSlide();

chartSlide.addText('Revenue Trend', {

  x: 0.5, y: 0.5, w: 9, h: 0.6,

  fontSize: 28, bold: true,

});

chartSlide.addChart(pptx.ChartType.line, [

  { name: 'Revenue', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [100, 120, 150, 180] },

  { name: 'Expenses', labels: ['Jan', 'Feb', 'Mar', 'Apr'], values: [80, 85, 90, 95] },

], {

  x: 0.5, y: 1.2, w: 9, h: 4,

  showLegend: true,

  legendPos: 'b',

  showTitle: false,

});

Images

import { readFileSync } from 'fs';

const imageSlide = pptx.addSlide();

// From file (Node.js)

imageSlide.addImage({

  path: 'logo.png',

  x: 0.5, y: 0.5, w: 2, h: 1,

});

// From base64

const imageBase64 = readFileSync('chart.png').toString('base64');

imageSlide.addImage({

  data: `image/png;base64,${imageBase64}`,

  x: 0.5, y: 2, w: 4, h: 3,

});

// From URL (Node.js only - uses https module)

imageSlide.addImage({

  path: 'https://example.com/image.png',

  x: 5, y: 2, w: 4, h: 3,

});

Shapes

const shapeSlide = pptx.addSlide();

// Rectangle

shapeSlide.addShape(pptx.ShapeType.rect, {

  x: 0.5, y: 0.5, w: 3, h: 2,

  fill: { color: '0066CC' },

  line: { color: '004499', pt: 2 },

});

// Circle/Oval

shapeSlide.addShape(pptx.ShapeType.ellipse, {

  x: 4, y: 0.5, w: 2, h: 2,

  fill: { color: '00AA00' },

});

// Arrow

shapeSlide.addShape(pptx.ShapeType.rightArrow, {

  x: 1, y: 3, w: 3, h: 1,

  fill: { color: 'FF6600' },

});

Slide Layouts and Masters

// Define a master slide

pptx.defineSlideMaster({

  title: 'COMPANY_MASTER',

  background: { color: 'FFFFFF' },

  objects: [

    { text: { text: 'Company Name', options: { x: 0.5, y: 0.2, w: 4, h: 0.3, fontSize: 10, color: '999999' } } },

    { line: { x: 0.5, y: 0.6, w: 9, h: 0, line: { color: '0066CC', pt: 2 } } },

  ],

});

// Use the master

const slide = pptx.addSlide({ masterName: 'COMPANY_MASTER' });

Export Patterns

// Node.js - Save to file

await pptx.writeFile({ fileName: 'presentation.pptx' });

// Browser - Trigger download

await pptx.writeFile({ fileName: 'presentation.pptx' });

// Get as base64 (for email, API, etc.)

const base64 = await pptx.write({ outputType: 'base64' });

// Get as Blob (browser)

const blob = await pptx.write({ outputType: 'blob' });

// Get as ArrayBuffer

const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });

// Cloudflare Workers - Return as Response

const arrayBuffer = await pptx.write({ outputType: 'arraybuffer' });

return new Response(arrayBuffer, {

  headers: {

    'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',

    'Content-Disposition': 'attachment; filename="presentation.pptx"',

  },

});

Cloudflare Workers Integration

All four libraries work in Cloudflare Workers (PPTX with caveats for remote images).

DOCX Generation Endpoint

import { Hono } from 'hono';

import { Document, Packer, Paragraph, TextRun } from 'docx';

const app = new Hono();

app.get('/api/invoice/:id', async (c) => {

  const invoiceId = c.req.param('id');

  const doc = new Document({

    sections: [{

      children: [

        new Paragraph({

          children: [new TextRun({ text: `Invoice #${invoiceId}`, bold: true, size: 48 })],

        }),

      ],

    }],

  });

  const buffer = await Packer.toBuffer(doc);

  return new Response(buffer, {

    headers: {

      'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',

      'Content-Disposition': `attachment; filename="invoice-${invoiceId}.docx"`,

    },

  });

});

export default app;

HTML to PDF with Browser Rendering

For complex layouts with CSS, use Cloudflare Browser Rendering (paid feature):

import puppeteer from '@cloudflare/puppeteer';

export default {

  async fetch(request, env) {

    const browser = await puppeteer.launch(env.BROWSER);

    const page = await browser.newPage();

    // Set HTML content

    await page.setContent(`

      <html>

        <head>

          <style>

            body { font-family: Arial, sans-serif; padding: 40px; }

            h1 { color: #333; }

            table { width: 100%; border-collapse: collapse; }

            td, th { border: 1px solid #ddd; padding: 8px; }

          </style>

        </head>

        <body>

          <h1>Invoice #12345</h1>

          <table>

            <tr><th>Item</th><th>Amount</th></tr>

            <tr><td>Service</td><td>$500</td></tr>

          </table>

        </body>

      </html>

    `);

    const pdf = await page.pdf({ format: 'A4' });

    await browser.close();

    return new Response(pdf, {

      headers: {

        'Content-Type': 'application/pdf',

        'Content-Disposition': 'attachment; filename="invoice.pdf"',

      },

    });

  },

};

wrangler.jsonc binding:

{

  "browser": {

    "binding": "BROWSER"

  }

}

Critical Rules

Always Do

✅ Use await Packer.toBuffer() for DOCX (it's async)

✅ Remember PDF coordinates start at BOTTOM-LEFT

✅ Use type: 'buffer' for XLSX in Workers/browser

✅ Embed fonts in PDF before using them

✅ Set proper Content-Type headers for downloads

✅ Use await pptx.writeFile() or await pptx.write() for PPTX

✅ Use base64 images in PPTX for Workers (avoid remote URLs)

Never Do

❌ Use Packer.toBuffer() without await (returns Promise)

❌ Assume PDF y=0 is at top (it's at bottom)

❌ Use writeFile in Workers (use Response instead)

❌ Forget to set Content-Disposition for downloads

❌ Use Node.js fs module in browser/Workers

❌ Use PPTX path URLs in Workers (https module not available)

Known Issues Prevention

Issue #1: DOCX Packer Returns Promise

Error: [object Promise] in file or empty document

Why: Packer.toBuffer() is async but called without await

Prevention: Always await Packer.toBuffer(doc)

Issue #2: PDF Text at Wrong Position

Error: Text appears at bottom of page or off-page

Why: PDF coordinates have origin at bottom-left, not top-left

Prevention: For text near top, use high y values (e.g., y=750 for letter size)

Issue #3: XLSX Formula Not Calculating

Error: Cell shows formula text instead of result

Why: SheetJS doesn't execute formulas, Excel does on open

Prevention: This is expected - formulas calculate when opened in Excel

Issue #4: Workers DOCX/PDF Returns Empty

Error: Downloaded file is empty or corrupted

Why: Returning wrong type or missing Content-Type header

Prevention: Return buffer directly with proper headers

Issue #5: PPTX Remote Images Fail in Workers

Error: https is not defined or image not appearing

Why: pptxgenjs uses Node.js https module for remote images

Prevention: Use base64 data URIs or local images in Workers environment

Using Bundled Resources

Templates (templates/)

  • docx-basic.ts - Complete Word document with headings, tables, images
  • xlsx-basic.ts - Excel workbook with formulas and formatting
  • pdf-basic.ts - PDF with text, images, shapes
  • pptx-basic.ts - PowerPoint with slides, charts, tables
  • workers-pdf.ts - Cloudflare Workers PDF generation example

References (references/)

  • docx-api.md - Quick reference for docx npm package
  • xlsx-api.md - Quick reference for SheetJS functions
  • pdf-lib-api.md - Quick reference for pdf-lib methods
  • pptxgenjs-api.md - Quick reference for pptxgenjs

Scripts (scripts/)

  • verify-deps.sh - Check library versions are current

Limitations vs Anthropic's Official Skills

This skill focuses on document creation with portable TypeScript libraries:

Feature

This Skill

Anthropic's Skills

Create DOCX/XLSX/PDF/PPTX

Works in Cloudflare Workers

Plugin installable

TypeScript-first

❌ (Python)

Edit existing DOCX with tracked changes

Excel formula validation

OCR scanned PDFs

For advanced editing scenarios (tracked changes, formula validation), consider Anthropic's official skills with Python tooling.

Package Versions (Verified 2026-01-12)

{

  "dependencies": {

    "docx": "^9.5.0",

    "xlsx": "^0.18.5",

    "pdf-lib": "^1.17.1",

    "pptxgenjs": "^4.0.1"

  }

}

Official Documentation

Complete Setup Checklist

  • Installed required packages (npm install docx xlsx pdf-lib)
  • Verified import statements match runtime (Node.js vs browser)
  • Used proper export method for target environment
  • Set Content-Type headers for HTTP responses
  • Tested document opens correctly in target application
  • Confirmed Workers compatibility (if deploying to edge)
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card