This workflow corresponds to n8n.io template #17090 — we link there as the canonical source.
This workflow follows the Gmail → Google Drive recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Generate and email PDF invoices from new Google Sheets orders",
"nodes": [
{
"id": "5efb9ef6-33c8-4182-a579-5c58622eac8b",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 480,
"height": 1240,
"content": "## Generate and email PDF invoices from new Google Sheets orders\n\n### Who's it for\n\nSmall businesses and freelancers who track orders in a Google Sheet and still assemble invoices by hand.\n\n### How it works\n\nEvery new row in your Orders sheet becomes a styled PDF invoice with line items, tax, and totals. The workflow builds the invoice HTML, validates the row, renders the PDF with the Acrewity community node, emails it to the customer via Gmail, archives a copy in Google Drive, and updates the row to Status = Invoiced so nothing is billed twice.\n\nA row with bad data (broken Items JSON, a missing item name, no email address) never blocks the others \u2014 its Status cell receives a plain-English error message so you can fix it right in the sheet.\n\n### Setup steps\n\n1. Install the verified community node `@acrewity/n8n-nodes-acrewity` and add your Acrewity API credential (free key at acrewity.com, 100 free credits/month)\n2. Connect Google Sheets, Gmail, and Google Drive credentials\n3. Create an Orders sheet. Paste this line into cell A1 \u2014 it fills the whole header row:\n\nOrder Number\tCustomer Name\tCustomer Email\tCustomer Address\tItems\tDue Date\tNotes\tStatus\n\n4. Items holds a JSON array, e.g. [{\"name\":\"Widget\",\"quantity\":2,\"price\":9.99}]\n5. Pick your spreadsheet in the trigger and both Sheets update nodes, your archive folder in the Drive node, and edit Set Workflow Configuration (company name, logo, tax rate)\n\n### Customization\n\nAdjust the invoice look in Create Invoice HTML (the editable lines are marked CHANGE ME), change the email copy in Send Invoice via Email, or add a Slack notification after the Drive archive step."
},
"typeVersion": 1
},
{
"id": "426df318-81db-42c5-9bf6-a3827f217354",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
0
],
"parameters": {
"color": 7,
"width": 416,
"height": 400,
"content": "## Capture order settings\n\nStarts when a new order row is added in Google Sheets, then applies invoice-wide configuration such as company name, logo URL, tax rate, and currency symbol."
},
"typeVersion": 1
},
{
"id": "6a653f64-ca92-4f94-a239-3a900d5fbfe6",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1008,
0
],
"parameters": {
"color": 7,
"width": 640,
"height": 576,
"content": "## Build and validate invoice\n\nCreates the invoice HTML from the order data and checks whether the order is valid. Invalid orders are routed downward to update the sheet as failed."
},
"typeVersion": 1
},
{
"id": "557dc6f7-4004-4a7b-93d3-b11c48a9a418",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1680,
0
],
"parameters": {
"color": 7,
"width": 416,
"height": 400,
"content": "## Generate PDF file\n\nHandles the valid-order path by converting the generated invoice HTML into a PDF and then preparing that PDF as a file attachment for downstream steps."
},
"typeVersion": 1
},
{
"id": "e152a19b-0582-4d27-a98b-fc654f8be12f",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2128,
0
],
"parameters": {
"color": 7,
"width": 416,
"height": 560,
"content": "## Deliver and record invoice\n\nSends the invoice PDF to the customer, archives a copy in Google Drive, and updates the original Google Sheets order row to mark it as invoiced."
},
"typeVersion": 1
},
{
"id": "3bcf0c1f-4425-4bff-96b4-247cd8749c38",
"name": "When New Order Added",
"type": "n8n-nodes-base.googleSheetsTrigger",
"position": [
608,
192
],
"parameters": {
"event": "rowAdded",
"options": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 1
},
{
"id": "2a4555db-34c4-4649-ab83-9f6146ccd643",
"name": "Set Workflow Configuration",
"type": "n8n-nodes-base.set",
"position": [
832,
192
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-1",
"name": "companyName",
"type": "string",
"value": "Your Company Inc."
},
{
"id": "cfg-2",
"name": "companyLogoUrl",
"type": "string",
"value": ""
},
{
"id": "cfg-3",
"name": "taxRate",
"type": "number",
"value": 0.13
},
{
"id": "cfg-4",
"name": "currencySymbol",
"type": "string",
"value": "$"
},
{
"id": "cfg-5",
"name": "emailSubjectPrefix",
"type": "string",
"value": "Your invoice from Your Company Inc."
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "4f0af137-9b88-4a1c-b51a-7e7f94e8ebee",
"name": "Create Invoice HTML",
"type": "n8n-nodes-base.code",
"position": [
1056,
192
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// ================================================================\n// BUILD THE INVOICE HTML\n//\n// This node turns an order row from Google Sheets into the HTML\n// page that becomes the PDF invoice.\n//\n// The node is set to \"Run Once for Each Item\", so this code runs\n// separately for every new row \u2014 paste five rows into the sheet\n// and five invoices go out.\n//\n// It is safe to edit this code. The places you are most likely\n// to change are marked with: CHANGE ME\n// ================================================================\n\n\n// ---- STEP 1: Get the current row ---------------------------------\n// \"$json\" is this row from your sheet PLUS the settings from the\n// \"Set Workflow Configuration\" node (company name, tax rate, etc.)\nconst data = $json;\n\n\n// ---- STEP 2: Helper that keeps the HTML safe --------------------\n// This converts characters like < and & so that customer-entered\n// text cannot break the invoice layout.\n// Rule of thumb: wrap ANY text you print in the HTML in esc(...).\nfunction esc(text) {\n return String(text ?? '')\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''');\n}\n\n\n// ---- STEP 3: Read the settings ----------------------------------\n// These come from the \"Set Workflow Configuration\" node.\n// Edit them THERE (not here) so all your settings stay in one place.\nconst companyName = data.companyName || 'Your Company Inc.';\nconst logoUrl = data.companyLogoUrl || '';\nconst taxRate = Number(data.taxRate) || 0; // 0.13 means 13%\nconst currency = data.currencySymbol || '$';\n\n\n// ---- STEP 4: Read the order row ---------------------------------\n// The names in ['...'] must match your Google Sheets column\n// headers EXACTLY. If your sheet uses different column names,\n// change them here. CHANGE ME\nconst orderNumber = data['Order Number'] || 'INV-' + Date.now();\nconst customerName = data['Customer Name'] || 'Customer';\nconst customerEmail = data['Customer Email'] || '';\nconst customerAddress = data['Customer Address'] || '';\nconst dueDate = data['Due Date'] || '';\nconst notes = data['Notes'] || '';\n\n\n// ---- STEP 5: Validate the line items ----------------------------\n// The \"Items\" column must contain a JSON array, for example:\n// [{\"name\":\"Widget\",\"quantity\":2,\"price\":9.99}]\n//\n// IMPORTANT: a bad row must NOT stop the whole batch \u2014 other rows\n// still deserve their invoices. Instead of stopping, this section\n// marks the row as invalid; the If node after this one routes it\n// to 'Update Order as Failed', which writes the reason into the\n// Status column of your sheet.\nfunction invalidRow(reason) {\n return {\n json: {\n valid: false,\n orderNumber: data['Order Number'] || '',\n statusMessage: 'Error: ' + reason\n }\n };\n}\n\nif (data['Items'] === undefined) {\n return invalidRow('Items column not found \u2014 row 1 of the sheet must be the header row (see the sticky note)');\n}\n\nlet items;\ntry {\n items = JSON.parse(data['Items']);\n} catch (error) {\n return invalidRow('Items is not valid JSON \u2014 expected [{\"name\":...,\"quantity\":...,\"price\":...}]');\n}\nif (!Array.isArray(items) || items.length === 0) {\n return invalidRow('Items must be a non-empty JSON array');\n}\n\n// Every item needs a name, a quantity, and a price\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n if (!item || typeof item.name !== 'string' || item.name.trim() === '') {\n return invalidRow('item ' + (i + 1) + ' has no \"name\" field');\n }\n if (isNaN(Number(item.quantity)) || Number(item.quantity) <= 0) {\n return invalidRow('item ' + (i + 1) + ' needs a numeric quantity above zero');\n }\n if (isNaN(Number(item.price))) {\n return invalidRow('item ' + (i + 1) + ' needs a numeric price');\n }\n}\n\n// The email address is where the invoice goes \u2014 required\nif (!customerEmail || !customerEmail.includes('@')) {\n return invalidRow('Customer Email is missing or not an email address');\n}\n\n\n// ---- STEP 6: Build the items table and the totals ---------------\nlet itemsHtml = ''; // one <tr> table row per item\nlet subtotal = 0;\n\nfor (const item of items) {\n const quantity = Number(item.quantity) || 0;\n const price = Number(item.price) || 0;\n const lineTotal = quantity * price;\n subtotal = subtotal + lineTotal;\n\n itemsHtml = itemsHtml +\n '<tr>' +\n '<td>' + esc(item.name) + '</td>' +\n '<td>' + quantity + '</td>' +\n '<td>' + currency + price.toFixed(2) + '</td>' +\n '<td>' + currency + lineTotal.toFixed(2) + '</td>' +\n '</tr>';\n}\n\nconst tax = subtotal * taxRate;\nconst total = subtotal + tax;\n\n\n// ---- STEP 7: Build the optional sections ------------------------\n// Each of these is only shown when there is something to show.\n\n// Company logo in the header (only if a logo URL is configured)\nlet logoHtml = '';\nif (logoUrl) {\n logoHtml = '<img src=\"' + esc(logoUrl) + '\" style=\"max-height:60px;max-width:200px;\" alt=\"Logo\">';\n}\n\n// Tax line (only if the tax rate is above zero)\nlet taxHtml = '';\nif (taxRate > 0) {\n taxHtml = '<p class=\"right\">Tax (' + (taxRate * 100).toFixed(0) + '%): ' + currency + tax.toFixed(2) + '</p>';\n}\n\n// Due date, address, and notes (only if the row has them)\nlet dueHtml = '';\nif (dueDate) {\n dueHtml = '<p><strong>Due Date:</strong> ' + esc(dueDate) + '</p>';\n}\n\nlet addressHtml = '';\nif (customerAddress) {\n addressHtml = '<p><strong>Address:</strong> ' + esc(customerAddress) + '</p>';\n}\n\nlet notesHtml = '';\nif (notes) {\n notesHtml = '<div class=\"notes\"><strong>Notes:</strong><br>' + esc(notes) + '</div>';\n}\n\n\n// ---- STEP 8: The invoice itself ---------------------------------\n// This is normal HTML and CSS. Change colors, fonts, and layout\n// in the <style> block below. CHANGE ME\nconst html = `<!DOCTYPE html>\n<html>\n<head>\n<style>\n body { font-family: Arial, sans-serif; padding: 40px; color: #1e293b; }\n .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }\n h1 { margin: 0 0 4px 0; }\n h2 { color: #64748b; margin: 0; font-weight: 600; }\n table { width: 100%; border-collapse: collapse; margin: 20px 0; }\n th, td { padding: 10px; text-align: left; border-bottom: 1px solid #e2e8f0; }\n th { background: #f1f5f9; }\n .right { text-align: right; }\n .bold { font-weight: bold; font-size: 18px; }\n .notes { margin-top: 30px; padding: 15px; background: #f8fafc; border-left: 3px solid #cbd5e1; }\n</style>\n</head>\n<body>\n\n <div class=\"header\">\n <div>\n <h1>${esc(companyName)}</h1>\n <h2>INVOICE</h2>\n </div>\n ${logoHtml}\n </div>\n\n <p><strong>Invoice #:</strong> ${esc(orderNumber)}</p>\n <p><strong>Date:</strong> ${new Date().toISOString().slice(0, 10)}</p>\n ${dueHtml}\n <p><strong>Bill To:</strong> ${esc(customerName)}</p>\n ${addressHtml}\n <p><strong>Email:</strong> ${esc(customerEmail)}</p>\n\n <table>\n <tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>\n ${itemsHtml}\n </table>\n\n <p class=\"right\">Subtotal: ${currency}${subtotal.toFixed(2)}</p>\n ${taxHtml}\n <p class=\"right bold\">Total: ${currency}${total.toFixed(2)}</p>\n\n ${notesHtml}\n\n</body>\n</html>`;\n\n\n// ---- STEP 9: Hand everything to the next nodes -------------------\n// \"html\" becomes the PDF. The other fields are used by the\n// email, Drive, and sheet-update nodes further down the line.\n// (One item out per row in \u2014 the next nodes run once per invoice.)\nreturn {\n json: {\n valid: true,\n html: html,\n orderNumber: orderNumber,\n customerName: customerName,\n customerEmail: customerEmail,\n invoiceFilename: orderNumber + '.pdf',\n emailSubject: (data.emailSubjectPrefix || 'Your invoice') + ' \u2014 ' + orderNumber\n }\n};\n"
},
"typeVersion": 2
},
{
"id": "b5f32783-6da6-4fbd-b037-d23c12f9a8fc",
"name": "Convert HTML to PDF",
"type": "@acrewity/n8n-nodes-acrewity.acrewity",
"position": [
1728,
192
],
"parameters": {
"html": "={{ $json.html }}",
"resource": "html_to_pdf"
},
"retryOnFail": true,
"typeVersion": 1
},
{
"id": "f9cae9ac-c873-4e9c-adf9-36ed2e15b243",
"name": "Transform PDF to File",
"type": "n8n-nodes-base.convertToFile",
"position": [
1952,
192
],
"parameters": {
"options": {
"fileName": "={{ $('Create Invoice HTML').item.json.invoiceFilename }}",
"mimeType": "application/pdf"
},
"operation": "toBinary",
"sourceProperty": "result.data.content",
"binaryPropertyName": "data"
},
"typeVersion": 1.1
},
{
"id": "8b8057bf-5dc8-4943-a3f5-4a63c7690bc9",
"name": "Send Invoice via Email",
"type": "n8n-nodes-base.gmail",
"position": [
2176,
192
],
"parameters": {
"sendTo": "={{ $('Create Invoice HTML').item.json.customerEmail }}",
"message": "=Hi {{ $('Create Invoice HTML').item.json.customerName }},\n\nThank you for your order. Your invoice {{ $('Create Invoice HTML').item.json.orderNumber }} is attached as a PDF.\n\nIf you have any questions, just reply to this email.\n\nBest regards",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{
"property": "data"
}
]
}
},
"subject": "={{ $('Create Invoice HTML').item.json.emailSubject }}"
},
"typeVersion": 2.2
},
{
"id": "93d21bcb-0321-448a-9374-bbd15300b15f",
"name": "Save Invoice to Drive",
"type": "n8n-nodes-base.googleDrive",
"position": [
2176,
368
],
"parameters": {
"name": "={{ $('Create Invoice HTML').item.json.invoiceFilename }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive",
"cachedResultName": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "list",
"value": "root",
"cachedResultName": "/ (Root folder)"
}
},
"typeVersion": 3
},
{
"id": "a69b026f-5b01-4694-9ab8-ad394aeaf674",
"name": "Update Order as Invoiced",
"type": "n8n-nodes-base.googleSheets",
"position": [
2384,
368
],
"parameters": {
"columns": {
"value": {
"Status": "Invoiced",
"Order Number": "={{ $('Create Invoice HTML').item.json.orderNumber }}"
},
"schema": [],
"mappingMode": "defineBelow",
"matchingColumns": [
"Order Number"
]
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 4.5
},
{
"id": "9c10f7b4-1963-4cad-bd37-5936331f6a19",
"name": "Validate Order",
"type": "n8n-nodes-base.if",
"position": [
1232,
192
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "v1",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.valid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "fc5af09d-01ed-46a0-9969-ba8da4c12e94",
"name": "Update Order as Failed",
"type": "n8n-nodes-base.googleSheets",
"position": [
1488,
400
],
"parameters": {
"columns": {
"value": {
"Status": "={{ $json.statusMessage }}",
"Order Number": "={{ $json.orderNumber }}"
},
"schema": [],
"mappingMode": "defineBelow",
"matchingColumns": [
"Order Number"
]
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 4.5
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Validate Order": {
"main": [
[
{
"node": "Convert HTML to PDF",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Order as Failed",
"type": "main",
"index": 0
}
]
]
},
"Convert HTML to PDF": {
"main": [
[
{
"node": "Transform PDF to File",
"type": "main",
"index": 0
}
]
]
},
"Create Invoice HTML": {
"main": [
[
{
"node": "Validate Order",
"type": "main",
"index": 0
}
]
]
},
"When New Order Added": {
"main": [
[
{
"node": "Set Workflow Configuration",
"type": "main",
"index": 0
}
]
]
},
"Save Invoice to Drive": {
"main": [
[
{
"node": "Update Order as Invoiced",
"type": "main",
"index": 0
}
]
]
},
"Transform PDF to File": {
"main": [
[
{
"node": "Send Invoice via Email",
"type": "main",
"index": 0
},
{
"node": "Save Invoice to Drive",
"type": "main",
"index": 0
}
]
]
},
"Set Workflow Configuration": {
"main": [
[
{
"node": "Create Invoice HTML",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow watches for new order rows in Google Sheets, generates a styled HTML invoice, converts it to a PDF using Acrewity, emails the PDF via Gmail, archives it in Google Drive, and updates the order row status so successful and failed rows are clearly tracked. Triggers…
Source: https://n8n.io/workflows/17090/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
General use cases include: Property managers who manage multiple buildings or units. Building owners looking to centralize tenant repair communication. Automation builders who want to learn multi-trig
This workflow automates the full offer letter lifecycle, from generation to final candidate response tracking. When a new row with a Pending status is added to Google Sheets, it creates a personalized
This workflow watches Google Sheets for new rows, renders a personalized video with iLoveVideoEditor using the row data, uploads the finished MP4 to Google Drive, writes the Drive link back to the sam
Hiring teams often struggle with document follow-ups, offer letter generation, and stakeholder communication. Manual checks, email back-and-forth, and missing files slow down hiring and create chaos d
This workflow turns a Google Sheet into an automated certificate-issuing pipeline. The moment a new completion row is added — whether by your training team, a Zap, or a quiz platform — a branded PDF/A