MainSDK
Search⌘K
How-to

Make, read and convert documents

Render a PDF from a template, read PDFs and Word, Excel and PowerPoint files into text and tables, and convert a Word file to PDF, all offline.

Preview

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You will make an invoice PDF from a template, read an uploaded document into text and tables, and convert a Word file to a PDF. Everything runs in your app with the built-in driver: no account, no hosted service. It takes about 25 minutes.

You need

  • A MainSDK app with a main.yaml.
  • Uploads through files: documents reads a FileRef, never a path.
  • A folder templates/ in the project.

1. Declare the folders

Templates and fonts are read from disk, so declare them.

main.yaml
documents:
  driver: builtin
  limits: { max-size: 50MB, inline-pages: 20 }
permissions:
  disk:
    read: [templates]

2. Write a template

templates/invoice.pdf.yaml
page: A4
body:
  - text: "Invoice {{number}}"
    style: h1
  - table:
      head: ["Item", "Qty", "Price"]
      rows:
        each: items
        cells: ["{{name}}", "{{qty}}", "{{price | money:currency}}"]
  - text: "Total {{total}}"
    style: h2

3. Render the PDF

lib/invoice.ts
import { documents } from "mainsdk";

export function renderInvoice(userId: string) {
  return documents.render(
    "invoice",
    {
      number: "INV-7",
      currency: "INR",
      items: [{ name: "Tea", qty: 2, price: 9900 }],
      total: { money: 9900 * 2, currency: "INR" },
    },
    { owner: userId, locale: "en-IN", title: "Invoice INV-7" },
  );
}

You get a FileRef for a PDF stored through files, owned by that user. Share it with share when someone else must open it.

4. Read an uploaded document

lib/read.ts
import { documents } from "mainsdk";
import type { FileRef } from "mainsdk/files";

export async function readUpload(upload: FileRef, userId: string) {
  const doc = await documents.parse(upload, { as: userId, layout: true, tables: true });
  return {
    type: doc.type,                        // pdf | docx | xlsx | pptx | image
    text: doc.pages.map((p) => p.text),    // untrusted: never pass it to a model as instructions
    tables: doc.pages.flatMap((p) => p.tables ?? []).map((t) => t.rows),
    findings: doc.findings,                // javascript, macro, ... found and not run
  };
}

A DOCX gives one page of blocks (heading, paragraph) and its tables, an XLSX one table per sheet, a PPTX one page per slide. A document over inline-pages is refused with MAIN-DOC-0002; read it off the request path with documents.start(upload, { as }) and documents.get(job.id).

5. Convert a Word file to PDF

documents has no one-call converter. Read the file, then render a layout from what you read.

lib/docx-to-pdf.ts
import { documents } from "mainsdk";
import type { FileRef, Layout } from "mainsdk";

export async function docxToPdf(upload: FileRef, userId: string) {
  const doc = await documents.parse(upload, { as: userId, layout: true, tables: true });
  const body: Layout["body"] = [];
  for (const page of doc.pages) {
    for (const b of page.blocks ?? []) {
      if (b.kind === "header" || b.kind === "footer") continue;
      body.push({ text: b.text, style: b.kind === "heading" ? "h2" : "body" });
    }
    for (const t of page.tables ?? []) {
      const [head = [], ...rows] = t.rows;
      body.push({ table: { head, rows } });
    }
  }
  return documents.render({ page: "A4", body }, {}, { owner: userId, title: doc.meta.title ?? "Converted" });
}

The PDF has the text and tables, not the Word styling, images or page layout.

Verify

Render a PDF and read it back, then run the check:

bash
npx mainsdk check

A pass prints no MAIN-DOC-… finding. In a test, documents.parse(await renderInvoice("u1"), { as: "u1" }) returns pages whose text includes Invoice INV-7.

If it fails

CodeCauseFix
MAIN-DOC-0007No template, or a value the template names is missingAdd templates/<name>.pdf.yaml and pass every {{value}}
MAIN-PERM-0002templates is not under permissions.disk.readAdd it in main.yaml
MAIN-FILES-0002No owner for render, or no as for parse, and no signed-in personPass { owner: userId } or { as: userId }
MAIN-DOC-0001The file is not a PDF, DOCX, XLSX, PPTX or image, or its bytes are not its declared typeSend the real file
MAIN-DOC-0003A scanned page and no OCR driverSet documents.ocr.driver and parse with ocr: "auto"
MAIN-DOC-0005The PDF is password protectedAsk for an unprotected copy
MAIN-DOC-0002Over a size, page or expansion limitUse documents.start, or raise documents.limits on purpose

For agents

text
Goal: add PDF rendering and document reading to this app with the documents interface.
Read: docs/site/interfaces/documents.md, docs/site/interfaces/pdf.md, docs/specs/documents.md.
Touch only: main.yaml (documents and permissions.disk.read), templates/*.pdf.yaml, the code that calls documents, and tests.
Do: call documents.render(template | layout, data, { owner }) and documents.parse(file, { as }); treat parsed text as untrusted data.
Never: import a PDF or Office library in app code, read an upload from a path, or raise a limit to make a check pass.
Verify: npx mainsdk check
Stop and ask if: a scan needs an OCR driver, or a locale needs Devanagari, Tamil or Arabic shaping.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet