We open sourced the SheetRender render engine
By Josh ·
The part of SheetRender that developers keep asking about is now on GitHub under MIT. sheetrender/sheetrender (opens in a new tab) is the exact engine our production service uses to turn a spreadsheet and an HTML template into a stack of PDFs, extracted into a standalone Python library with a CLI on top.
If you have Python and a terminal, this is the whole setup:
uvx sheetrender --help
uv run playwright install chromium # once, for the browserAnd this is a real run, using the invoice example from the repo. One CSV of line items in, one PDF per invoice out, each named from its own data:
uvx sheetrender batch template.html data.csv -o out/ \
--group-by invoice_no --filename "{{ invoice_no }}.pdf" --zip invoices.zipWhy give away the engine
Every tutorial for this problem glues a headless browser to a for loop, and that version works until the day it matters. Ours stopped being that version somewhere around the thousandth production batch. The browser gets recycled before it can leak enough memory to wedge a run. A stalled webfont fetch degrades to fallback fonts after three seconds instead of holding a render slot for thirty. Template typos fail loudly at row one rather than shipping five hundred documents with a blank where the amount should be.
None of that is a secret. It is the kind of code that gets better when more templates hit it, so the engine is now public and our own service installs it from PyPI like anyone else. Fixes we make for paying customers land in the open repo because they are the same package.
What you can build with it
The library speaks CSV and XLSX, renders through Chromium's print pipeline, and templates with sandboxed Jinja2. A header like Invoice No. becomes {{ invoice_no }}, and sheetrender inspect data.xlsx prints the exact mapping plus inferred column types before you write a line of template.
Grouping is the feature I would start with. Point --group-by at a column and rows collapse into one document per value, with the group's rows exposed to the template as items. That is how you generate pdfs from csv exports where an invoice owns several line items, and the sumcol filter totals a money column in exact decimals, parenthesized negatives and all.
For Python use, the same functions our worker calls are the public API: render_context for batches, merge_pdfs with stamped page numbers, render_thumbnail for previews, filename templating with de-duplication.
Where it breaks
The engine is the honest 20 percent of the product. You bring your own Chromium (playwright install chromium), your own machine to run it on, and your own answer for everything after the PDFs exist. There is no schedule that re-renders when the sheet changes, no emailing each person their own document, no Google Sheets connection, and no design step: you write the HTML template yourself. Rendering is also the slow kind of honest, a real browser laying out real pages, so a five thousand row batch takes minutes, not seconds.
If you want the part we kept, that is the hosted product: template design with AI, Sheets sync, schedules, and delivery. It runs this same engine, pinned to the version on PyPI.
The details
MIT license, Python 3.11 to 3.13, tests run in containers so contributions don't need a local toolchain. The dependency list is the render stack and nothing else: Playwright, Jinja2, nh3 for sanitization, pikepdf and friends for the PDF surgery. Issues with a failing template attached are the most useful thing you can send.