Google Sheets Invoice Line Items: One PDF Per Invoice
By Josh ·
Invoice 1001 is three rows in your Google Sheets spreadsheet, one per line item. Run a traditional mail merge over that sheet and you get three invoices, each carrying a single line.
Nobody warns you about this before you start. You pick an add-on, tag up a template, hit run, and the output folder tells you what the tool thinks your data means.
Can a mail merge combine multiple rows into one document? Not by default. A merge walks records, a record is a row, so five line items become five documents. To get one invoice with five lines on it you need a tool that groups rows by a key column before it builds anything, or a script that does the grouping for you.
Where the one row, one document rule comes from
Word's mail merge was built to address envelopes. The data source is a list of people, each person is a record, and the merge steps through records one at a time, filling the same fields on each pass. That model is thirty years old and it has never had a notion of "these four records belong together."
Most tag-based add-ons inherited it, because they inherited the same shape: a template with placeholders, a sheet where a row means a document. The general survey of those tools is in the Google Sheets mail merge guide, and the Word-side equivalent is splitting a merge into separate PDFs. Neither of those posts solves the problem on this page, because the problem is not how files come out. It is what counts as one document going in.
Two neighboring jobs have their own posts. If your sheet is already one row per invoice, with a single amount and no itemization, start at batch invoices from a spreadsheet. If what you owe the client is a period summary of everything outstanding, that is client statements.
The data shape that triggers it
Here is the sheet almost everyone has, because it is the sheet that is easy to keep:
| invoice_no | client | item | qty | unit_price | amount |
|---|---|---|---|---|---|
| 1001 | Ridgeway Dental | Site visit | 2 | 150.00 | 300.00 |
| 1001 | Ridgeway Dental | Panel replacement | 1 | 480.00 | 480.00 |
| 1001 | Ridgeway Dental | Filter set | 3 | 22.50 | 67.50 |
| 1002 | Halloran Studios | Site visit | 1 | 150.00 | 150.00 |
| 1002 | Halloran Studios | Cable run, 40m | 1 | 320.00 | 320.00 |
Five rows, two invoices. The invoice_no column is the whole story: it repeats, and where it repeats, the rows belong on the same page. Nothing in a standard merge reads that column as an instruction.
Notice the second thing this shape costs you. client repeats too, and so would due_date and payment_terms if they were here. Those are invoice facts sitting on line-item rows, which means every tool downstream has to decide which row's copy of them to believe.
Option 1: reshape the sheet so a row is an invoice
The obvious move is to make the data fit the tool. One row per invoice, with the items spread sideways into numbered columns: item1, item1_qty, item1_price, item2, and onward. Then a plain merge works, and the free add-ons will do it.
You can build that tab without leaving Google Sheets. Call the raw tab Items, with the columns from the table above, and on a second tab:
- Put
=UNIQUE(Items!A2:A)in A2, so every invoice number appears exactly once. - In B2, pull that invoice's first description across, then drag the formula right for as many item slots as you are willing to allow:
=IFERROR(INDEX(FILTER(Items!C2:C, Items!A2:A = $A2), COLUMN() - COLUMN($B$1) + 1), "") - Repeat the block with
Items!D2:DandItems!E2:Efor quantities and prices, keeping each block's own anchor cell in theCOLUMN()arithmetic. - Total the invoice with
=SUMIF(Items!A2:A, $A2, Items!F2:F). - Point Autocrat at this tab instead of the raw one, and tag the template with
<<item1>>,<<item2>>, and the rest.
An alternative to the numbered slots is one multi-line cell per column, which reads better on short invoices:
=TEXTJOIN(CHAR(10), FALSE, FILTER(Items!C2:C, Items!A2:A = $A2))FALSE matters there. With blanks removed, a missing description shifts every price below it up one line, and the invoice is wrong in a way that still looks tidy.
Where it breaks:
- You pick a maximum item count in advance, and the one invoice with fourteen lines will arrive. When it does, you are widening the column set and re-tagging the template, then checking that nothing downstream referenced the old width.
- Unused slots print as empty table rows unless the template can hide them, and most tag-based templates cannot. An invoice with two lines and eleven blank rows under them looks like a mistake, because it is one.
- Even with the
TEXTJOINversion, descriptions and amounts live in separate cells, so they line up until a description wraps. Then line three's price sits beside line four's description, and the client is the one who notices.
The formulas are live, so this is not a rebuild every month. It is a standing structure to keep honest: slots to extend, a flattened tab to reconcile against the raw one, and a second version of the truth that exists only so a template can stay simple. If you are reading that list and recognizing your own Tuesday, the fourth option skips the flattened tab altogether.
Option 2: an add-on that groups rows for you
Some document add-ons do the grouping natively, so the sheet stays as it is and you tell the tool which column identifies the document.
Portant is the one I can point at specifically, and its documentation is worth reading before you evaluate anything else, because it describes the problem in the same words you would. Its data grouping feature (opens in a new tab) has you pick the column that identifies a group, then puts every matching row into one table inside the document. You can set header and footer rows that appear once per group with the repeating rows between them, which is exactly an invoice table with a totals line under it. Their free plan is 30 documents a month, and paid starts at $42 a month on Pro, billed annually, when I last checked. What I could not pin down is which tier grouping needs: the docs page names no plan, and whether the "line items and formulas" row on their pricing page is this feature or a HubSpot one is a question their site does not answer directly. Check their pricing page (opens in a new tab) and confirm the tier before you commit a billing run to it.
Autocrat (opens in a new tab) does not do this, and it is worth being exact about why, because its two modes both sound close. Multiple output gives you one file per row. Single output joins every record in the merge into one long file. Neither is an invoice with its own lines grouped under it. That is not a knock on Autocrat. For a merge where one row really is one document, it is still the free tool I reach for.
The rest of the Google Sheets merge add-ons sit somewhere between those two. Line-item support is the feature most likely to be described vaguely on a marketing page and missing from the product, so find the vendor's documentation page for grouping and read it before you pay. If they support it, they document it, because it is the hard part.
Option 3: Apps Script that groups the rows itself
If you can read JavaScript, the grouping is the easy part. Everything after it is Google Docs table plumbing.
This script reads the sheet from the table above, groups rows into a Map keyed on invoice number, copies a Docs template for each invoice, and clones a model row to build the line-item table:
function buildInvoices() {
// Sheet columns in this order: invoice_no, client, item, qty, unit_price, amount
const TEMPLATE_ID = 'PASTE_DOC_ID'; // Docs invoice with {{invoice_no}}, {{client}}, {{total}}
const FOLDER_ID = 'PASTE_FOLDER_ID'; // where the PDFs land
const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues().slice(1);
const invoices = new Map();
rows.forEach(([invoiceNo, client, item, qty, unitPrice, amount]) => {
if (!invoiceNo) return;
const key = String(invoiceNo);
if (!invoices.has(key)) invoices.set(key, { client: client, items: [] });
invoices.get(key).items.push([item, qty, unitPrice, amount]);
});
const folder = DriveApp.getFolderById(FOLDER_ID);
invoices.forEach((invoice, invoiceNo) => {
const copy = DriveApp.getFileById(TEMPLATE_ID).makeCopy('invoice-' + invoiceNo, folder);
const doc = DocumentApp.openById(copy.getId());
const body = doc.getBody();
// The template's first table has a header row plus one model row whose
// cells carry the formatting every line item should inherit.
const table = body.getTables()[0];
const model = table.getRow(1);
let total = 0;
invoice.items.forEach(cells => {
total += Number(cells[3]);
const row = model.copy();
cells.forEach((value, i) => row.getCell(i).setText(String(value)));
table.appendTableRow(row);
});
model.removeFromParent();
body.replaceText('{{invoice_no}}', invoiceNo);
body.replaceText('{{client}}', invoice.client);
body.replaceText('{{total}}', total.toFixed(2));
doc.saveAndClose();
folder.createFile(copy.getAs('application/pdf')).setName('invoice-' + invoiceNo + '.pdf');
copy.setTrashed(true);
});
}The grouping is the seven lines in the middle. Everything else is there because Google Docs has no repeating region, so you clone a row you formatted by hand and delete the original once the real lines are in.
Where it breaks:
- One execution is killed at six minutes on every plan. Each invoice costs a file copy, a set of table writes, a PDF export, and a trash call, so the ceiling arrives sooner than you would guess from the row count.
- A free Gmail account is capped at 250 created documents a day, and every invoice copies the template once. Trashing that copy afterwards does not give the quota back. Google Workspace accounts get more room; the six-minute limit does not move for anyone.
total.toFixed(2)prints1234.50, with no thousands separator and no currency symbol. Money formatting is yours to write.- If the table breaks across a page, the header repeats only when you pinned the header row in the template. Check that before you send anything long.
- Nothing tracks which invoices already exist. A run that dies at invoice 40 of 90 leaves you comparing a Drive folder against a spreadsheet, and running it again makes a second copy of the first 40.
That last one is the whole difference between a script that worked on your test data and one you can put on a schedule. I would not hand-roll that bookkeeping twice, which is most of why the tool below exists.
Option 4: a tool that groups rows for you, hosted
SheetRender is our product, so read this section knowing that.
It takes the sheet in the shape you already have:
- Upload the workbook, or connect the Google Sheets spreadsheet. CSV and XLSX both land the same way.
- Confirm the grouping column. We read the columns on upload and propose a candidate, going on name patterns like invoice or order and on how heavily values repeat, so the usual move is one click to accept. The detection exists because the billing sheets people send us nearly always have that one repeating column and nobody thinks to mention it.
- Upload one finished invoice as the example. Last month's bill, or the PDF your accountant sends. A model rebuilds it as a template with the fields mapped to your columns.
- Page through the preview on your own rows, then generate. Line items render as a table that breaks across pages with the header repeating, totals sum from the
amountcolumn at render time, and the batch comes back as separate PDFs or goes out as one email per invoice to the address carried on its rows.
Where it breaks:
- The wizard groups on one column. The API accepts a composite key of up to three, so documents identified by client plus month mean writing an API call.
- Line items render in sheet order. There is no sort control in the interface, so if you want them by amount or by date, sort the sheet first.
- Totals sum a column that exists. Nothing multiplies quantity by unit price for you, so a sheet holding only
qtyandunit_priceneeds anamountcolumn added before the totals are right. - Currency gets decided once, when the template is designed. A sheet mixing dollars and euros will not format each row in its own currency.
- The template is rebuilt by a model from your example, and the first pass is close rather than exact. Correcting it by chat is a step before your first real batch, not one you can skip.
- The email for each invoice is read from the group's first row. If the address column is not identical down the group, the other values are silently ignored.
- Your data goes to our servers. The merge Autocrat runs never leaves your Google account, and some finance teams stop reading there.
Free is 50 documents and 50 emails a month, on 2 active templates, with a "Made with SheetRender" line in the footer from the first document, and generated files are kept for 30 days. That covers a real billing month for a small book of clients, and it is enough to see your own invoice rebuilt before you decide anything. Scheduled runs are not in it; those start at $19 a month on Starter. The invoices page walks the billing job end to end.
The comparison
| Tool | Multiple rows into one document | Plan required | Price |
|---|---|---|---|
| Autocrat | No, one file per row or all records in one file | Free, its only plan | $0 |
| Reshaped sheet plus Autocrat | Yes, up to your fixed slot count | Free, its only plan | $0 |
| Apps Script you write | Yes, you write the grouping | None, any Google account | $0 plus a day of building |
| Portant | Yes, documented data grouping | Not stated on their grouping docs page | Free plan is 30 docs/mo, paid from $42/mo billed annually |
| SheetRender | Yes, grouped by a key column | Included on the free plan | $0 free with a footer, $19/mo paid |
Prices checked August 2026.
Quick answers
How do I create an invoice with multiple line items from a spreadsheet? Group the rows by invoice number first, then render one document per group. Either reshape the sheet so each invoice is a single row with numbered item columns, or use a tool that takes a grouping key: Portant, SheetRender, or a script you write.
Does Autocrat support line items? No. Autocrat writes one file per row, or joins every record into a single long document, and neither groups an invoice's rows under one header. It costs nothing, and one file per row is exactly what most merges want.
Can Google Docs repeat a table row from spreadsheet data? Not on its own. A Docs template has no repeating region, so anything filling a variable number of table rows is either doing it through Apps Script or doing it outside Google.
How do I group rows by invoice number in Google Sheets? =UNIQUE(Items!A2:A) gives you the invoice list and =SUMIF(Items!A2:A, $A2, Items!F2:F) totals each one from the amount column. That is the summary. Turning it into an itemized document still takes one of the routes above.
Which one should you pick?
- A short client list with item counts that never surprise you: reshape the sheet, merge it with Autocrat, spend nothing.
- One row per invoice already, no itemization: Autocrat on its own. Skip this page.
- You write code, the volume is small, and you want to own it: the Apps Script above, plus the resumability work it does not include.
- Already inside Portant or HubSpot: their grouping feature, once you have confirmed which plan carries it.
- Itemized invoices every month, from a sheet you would rather leave normalized: SheetRender. That is the case we built the grouping for, and if your sheet looks like the table at the top of this page, start there.
Underneath the tooling, this is a data-shape argument, and the sheet is winning it. One row per line item is how billing data should be stored, and every route here that begins by flattening it asks you to keep two versions of the truth so a template can stay simple. If you bill six clients and the flattened tab is five minutes of work, take that trade and get on with your day. Past that, pick the route that leaves the sheet alone.