Generate an invoice PDF from a Google Sheets row with n8n
Mark an invoice row Ready and the workflow builds the invoice from your Google Docs template, with its line items from a second tab, saves the Doc and a PDF to Google Drive, and writes both links back into the sheet.
- Source
- Google Sheets
- Output
- Google DocsPDF (single runs only)
- Delivered to
- Google Drive
n8n workflow
The importable workflow file for this recipe is being tested and will be added here. The steps below show exactly what it does, so you can build it now in a few minutes with the SourceToDocs n8n node.
How the workflow runs
- 01
Google Sheets Trigger: Row Added or Updated
Watches the Invoices tab and outputs the rows that were added or edited since the last check.
- 02
If: Status is equal to Ready
Only rows you have marked Ready continue, so a half-filled invoice never produces a file.
- 03
Loop Over Items: Batch Size 1
The SourceToDocs node starts one run for all the items it receives. The loop hands on one invoice at a time, so two rows marked Ready together still get one invoice each.
- 04
Google Sheets: Get Row(s)
Reads the Lines tab with a filter on Invoice number, so it returns only the line items of the invoice in the loop.
- 05
SourceToDocs: Document, Generate
Rows Come From is set to the Map Columns mode for the invoice header, and the line items go in as a JSON array under Options, Additional Data Sets. Execute Once is on, so the header is sent as one row. Because this is a single run, switching on Also generate PDF delivers a PDF beside the Google Doc.
- 06
Google Sheets: Update Row
Writes file_url and the PDF link into the invoice's row and changes Status, which also stops the same row from being generated twice.
| Google Sheets field | Template placeholder |
|---|---|
| Invoice number | {{ invoices.invoice_number }} |
| Client | {{ invoices.client }} |
| Client address | {{ invoices.client_address }} |
| Issue date | {{ invoices.issue_date }} |
| Due date | {{ invoices.due_date }} |
| Total | {{ invoices.total }} |
| Lines tab: Description, Qty, Unit price, Amount | {% for l in lines %} {{ l.description }} {{ l.qty }} {{ l.unit_price }} {{ l.amount }} {% endfor %} |
Or have Claude set it up
The Invoices and receipts skill
works through the SourceToDocs connector at https://app.sourcetodocs.com/api/mcp. Install the skill, connect, then paste:
Use the invoices-and-receipts skill. My invoices are in the Google Sheet "Invoicing": the tab "Invoices" has one row per invoice (Invoice number, Client, Client address, Issue date, Due date, Total) and the tab "Lines" has one row per line item (Invoice number, Description, Qty, Unit price, Amount). Create a Google Docs invoice document in SourceToDocs from them, with the data sets named Invoices and Lines and a PDF for each invoice. I will pick the spreadsheet, the Google Docs template and the Drive folder "Invoices 2026" when you send me the links. Show me the placeholder mapping, including the line item loop, before you run anything, then generate the one invoice number I give you.
To turn a Google Sheets row into an invoice PDF with n8n, trigger on added or updated rows, let only rows marked Ready through, read that invoice’s line items from a second tab, and pass both to the SourceToDocs node, which fills your Google Docs invoice template and saves the Doc and a PDF to Google Drive. The last node writes the links back to the row. One invoice is one run, which is what makes the PDF possible. The build is six nodes, described below.
What you need
- A Google Sheet with two tabs.
Invoiceshas one row per invoice and the columnsInvoice number,Client,Client address,Issue date,Due date,Total,Status,Invoice linkandPDF link.Lineshas one row per line item and the columnsInvoice number,Description,Qty,Unit priceandAmount. Invoice number must be unique on the Invoices tab. - An invoice template in Google Docs with the header placeholders and a table whose row repeats per line item (shown below). The invoice template is a fuller design with its own placeholder names: a design reference, not a drop-in file.
- A SourceToDocs workspace and an API token for it. A workspace owner or admin creates the token on the API Tokens page.
- On the Accounts page, a Google Sheets connection for the data, a Google Docs connection for the template and a Google Drive connection for delivery. Every tile is a separate connection.
- n8n, self-hosted or Cloud, with the SourceToDocs node installed. In n8n, open Settings, then Community nodes, and install
@twovmodules/n8n-nodes-sourcetodocs. On n8n Cloud, add it from the nodes panel, where it is listed as a verified node. - A Google Sheets credential in n8n.
Set up the document in SourceToDocs
The document needs two data sets, one for the invoice and one for its lines. Both tabs live in one spreadsheet, so connect that spreadsheet as the data source and n8n will send the rows of one invoice at a time.
- Create a Google Docs document and choose your Google Sheets account as its data source. Pick the invoicing spreadsheet.
- Add a data set named
Invoiceson the Invoices tab and tickInvoice number,Client,Client address,Issue date,Due dateandTotal. - Add a data set named
Lineson the Lines tab and tickDescription,Qty,Unit priceandAmount. - Each data set and each column gets an alias, a snake-case identifier derived from its name: data set
Invoicesisinvoices, columnUnit priceisunit_price. - On the Document step, pick your Google Doc. Its Variables panel lists the exact placeholders; click one to copy it.
- Add a Google Drive distribution and choose the folder.
- Switch on Also generate PDF in the Output card.
- Leave split off. A split document produces no PDFs.
In the template, the header fields are written with the data set’s alias, for example {{ invoices.client }} and {{ invoices.total }}. The line item table has one template row:
| Description | Qty | Unit price | Amount |
|---|---|---|---|
{% for l in lines %}{{ l.description }} | {{ l.qty }} | {{ l.unit_price }} | {{ l.amount }}{% endfor %} |
The {% for %} tag sits in the row’s first cell and {% endfor %} in its last, which repeats the whole row once per line.
Build it in n8n
Add the nodes in this order. Connect the true output of If to Loop Over Items, the loop output to the Get Row(s) node, that node to SourceToDocs, SourceToDocs to the Update Row node, and that node back into Loop Over Items. Rename the Get Row(s) node to Get line items, because an expression below refers to it by name.
| Node | Operation | Key settings |
|---|---|---|
| Google Sheets Trigger | Trigger On: Row Added or Updated | Document: your invoicing spreadsheet. Sheet: Invoices. Optional: under Options, set Columns to Watch to Status so edits to other cells do not start the workflow. |
| If | One condition | {{ $json.Status }} is equal to Ready. |
| Loop Over Items | Batch Size: 1 | No other settings. |
Google Sheets (Get line items) | Operation: Get Row(s) | Document: the same spreadsheet. Sheet: Lines. Under Filters, the column Invoice number with the value {{ $json['Invoice number'] }}. |
| SourceToDocs | Resource: Document. Operation: Generate | Document: your invoice document. Rows Come From: This Workflow — Map Columns. Data Set: Invoices. Columns: see below. Under Options, Additional Data Sets: one entry for Lines, see below. In the node’s Settings, switch on Execute Once. Wait for Completion stays on, which is the default. |
| Google Sheets | Operation: Update Row | Document: the same spreadsheet. Sheet: Invoices. Column to match on: Invoice number. Values: see below. |
The six column values in the SourceToDocs node:
| Column | Value |
|---|---|
| Invoice number | {{ $('Loop Over Items').item.json['Invoice number'] }} |
| Client | {{ $('Loop Over Items').item.json['Client'] }} |
| Client address | {{ $('Loop Over Items').item.json['Client address'] }} |
| Issue date | {{ $('Loop Over Items').item.json['Issue date'] }} |
| Due date | {{ $('Loop Over Items').item.json['Due date'] }} |
| Total | {{ $('Loop Over Items').item.json['Total'] }} |
The entry under Options, Additional Data Sets:
| Field | Value |
|---|---|
| Data Set Name or ID | Lines |
| Rows | {{ $('Get line items').all().map(i => ({ 'Description': i.json['Description'], 'Qty': i.json['Qty'], 'Unit price': i.json['Unit price'], 'Amount': i.json['Amount'] })) }} |
The values the last Google Sheets node writes:
| Column | Value |
|---|---|
| Invoice number | {{ $('Loop Over Items').item.json['Invoice number'] }} |
| Invoice link | {{ $json.file_url }} |
| PDF link | {{ $json.deliveries[0].result.pdf_url }} |
| Status | Generated |
Three details make this work:
- Execute Once. The Get Row(s) node hands SourceToDocs one item per line item, and the Map Columns mode sends one row per incoming item. With Execute Once on, the node sends the invoice header as a single row, and the lines travel in the Additional Data Sets entry.
- Both data sets are sent. A data set you do not send is read live from the sheet, every invoice included. Sending
InvoicesandLinesfrom the workflow keeps the run to this one invoice. - Names, not aliases. The Column names and the keys in Rows are the data sets’ column names, case included. The template placeholders use the aliases.
Mapped values are sent as they resolve. Look at the trigger’s output for the date and amount cells: if they do not read the way the invoice should show them, format the cells as plain text in the sheet or format the value in the expression.
An invoice needs at least one row on the Lines tab. With none, the Get Row(s) node returns nothing and the loop stops there, so add the lines before you mark the row Ready.
Which links end up in the sheet
The SourceToDocs node returns file_url and file_name at the top level of its output: that is the Google Doc. With Also generate PDF on, the deliveries list holds one entry per destination, and a Google Drive entry’s result carries drive_url for the Doc and pdf_url for the PDF. The expression above reads the first entry, which is correct when the document has one destination.
The files sit in your Drive folder and are visible to whoever that folder is shared with. If you forward a link to a client, the folder’s sharing decides whether they can open it.
One invoice per run, or all of them at once
This page starts one run per invoice because a PDF is produced on a single run only. The other shape is a split document: send every invoice and every line in one run and let SourceToDocs produce one file per invoice number. That run delivers Google Docs without PDFs, so it suits a monthly batch you review in Docs before exporting.
The Claude route
With the SourceToDocs connector added to Claude and the invoices-and-receipts skill installed, Claude can do the setup above for you: create the document, attach the spreadsheet (you choose it through a link Claude sends, because it cannot browse your Drive), define the two data sets, read the template’s placeholders, show you the mapping, set the Drive delivery with a PDF and start a run. You also pick the template and the Drive folder through links, and you edit the template itself in Google Docs. The MCP page explains how to connect it.
For a PDF per invoice, Claude starts one run per invoice and sends that invoice’s row and its lines with the run, so tell it which invoice numbers you want. It prints the totals the sheet holds and calculates nothing. The limit: it does not watch for a Status change and it does not write links back into the row. For an invoice that appears on its own whenever a row is marked Ready, use the n8n workflow on this page; Claude is the quicker way to get the document and its mapping in place first.
Where this fits
One row, its lines, one invoice, a person sending it: the simplest case. The document automation guide covers the wider pattern, and the n8n node page lists what else the node can do.
Related
- document automation: the full guide this recipe belongs to.
- Free invoice template for Google Docs: the template this workflow fills.
- The SourceToDocs n8n node: every action and setting.
- The SourceToDocs MCP server: what Claude can do through the connector.