---
title: "Make, read and convert documents"
description: "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."
section: "guides"
status: "Preview"
spec: "documents"
canonical: "https://mainsdk.dev/docs/guides/documents"
updated: "2026-10-28"
---

# 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.

Status: Preview · Canonical: https://mainsdk.dev/docs/guides/documents · 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.

> **Note:** `documents` is built for 0.4 (spec `documents`, ADR-0058). It runs from the repository and is not published to npm yet. Scans need an OCR driver and are covered on [Document reading](https://mainsdk.dev/docs/interfaces/documents).

## You need

- A MainSDK app with a `main.yaml`.
- Uploads through [`files`](https://mainsdk.dev/docs/interfaces/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.

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

## 2. Write a template

```yaml title="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

```ts title="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`](https://mainsdk.dev/docs/interfaces/share) when someone else must open it.

## 4. Read an uploaded document

```ts title="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.

```ts title="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

| Code | Cause | Fix |
| --- | --- | --- |
| `MAIN-DOC-0007` | No template, or a value the template names is missing | Add `templates/<name>.pdf.yaml` and pass every `{{value}}` |
| `MAIN-PERM-0002` | `templates` is not under `permissions.disk.read` | Add it in `main.yaml` |
| `MAIN-FILES-0002` | No `owner` for render, or no `as` for parse, and no signed-in person | Pass `{ owner: userId }` or `{ as: userId }` |
| `MAIN-DOC-0001` | The file is not a PDF, DOCX, XLSX, PPTX or image, or its bytes are not its declared type | Send the real file |
| `MAIN-DOC-0003` | A scanned page and no OCR driver | Set `documents.ocr.driver` and parse with `ocr: "auto"` |
| `MAIN-DOC-0005` | The PDF is password protected | Ask for an unprotected copy |
| `MAIN-DOC-0002` | Over a size, page or expansion limit | Use `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

- [Document reading](https://mainsdk.dev/docs/interfaces/documents)
- [PDF documents](https://mainsdk.dev/docs/interfaces/pdf)
- [Upload files](https://mainsdk.dev/docs/guides/file-uploads)
