This workflow corresponds to n8n.io template #14982 — we link there as the canonical source.
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 →
{
"id": "E5QvVu7b1SGiH1EF",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Pricing Proposal Generator",
"tags": [],
"nodes": [
{
"id": "de5e1a55-89b9-4d5f-a066-790a84ed7b4b",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 420,
"height": 1012,
"content": "## \ud83d\udcc4 Pricing Proposal Generator\n\nGenerates a branded PDF pricing proposal on demand and delivers it by email.\n\n### How it works\n1. **Webhook Trigger** \u2014 A web form POSTs prospect details, selected services, discount level, and requestor info to the webhook endpoint.\n2. **Fetch Pricing Sheet** \u2014 Retrieves all rows from your `pricing_request.xlsx` Excel table in OneDrive/SharePoint.\nDownload template: https://iportgpt.com/n8n_assets/pricing_request.xlsx\n3. **Process & Price** \u2014 Filters the pricing rows to the selected services, applies the correct Discount / Retail / Premium price tier, and calculates the total.\n4. **Build HTML** \u2014 Renders a fully branded HTML proposal document with cover info, line-item pricing table, terms, and a signature block.\n5. **Convert to PDF** \u2014 Sends the HTML to a self-hosted [Gotenberg](https://gotenberg.dev) instance, which returns a print-ready PDF.\n6. **Email Proposal** \u2014 Delivers the PDF as an attachment via Gmail OAuth2 to the requester and any internal recipients you configure.\n\n### Setup\n1. **Webhook** \u2014 Set the `path` field to a value that matches your form POST URL (e.g. `pricing-request`).\n2. **Fetch Pricing Sheet** \u2014 Connect a Microsoft Excel credential, then select your OneDrive/SharePoint workbook, worksheet, and table.\n3. **Process & Price** \u2014 Confirm the column names in the Code node match your Excel table (`Title`, `Description`, `Cost`, `Discount`, `Retail`, `Premium`, `Unit`).\n4. **Build HTML Proposal** \u2014 Replace `YOUR_LOGO_URL` and brand color variables with your own values.\n5. **Gotenberg \u2192 PDF** \u2014 Update the URL to point to your Gotenberg instance and attach a Basic Auth credential.\n6. **Email Proposal** \u2014 Connect a Gmail OAuth2 credential. Update `YOUR_INTERNAL_EMAIL` in the `sendTo` field.\n\n### Customization\n- Add new price tiers by extending the `switch` block in **Process & Price**.\n- Adjust proposal branding in **Build HTML Proposal** by updating the CSS color variables and logo URL.\n- Validity period defaults to 30 days \u2014 change the `30` multiplier in **Build HTML Proposal** and the email body text."
},
"typeVersion": 1
},
{
"id": "b81eea6c-155c-46c1-a311-5a5882db2f92",
"name": "Section \u2013 Intake",
"type": "n8n-nodes-base.stickyNote",
"position": [
448,
0
],
"parameters": {
"color": 7,
"width": 180,
"height": 448,
"content": "## 1 \u00b7 Intake\nReceives the form POST from your web form."
},
"typeVersion": 1
},
{
"id": "d0a5d8c9-bb5b-4888-b5c0-718268f8e7cd",
"name": "Section \u2013 Data",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
0
],
"parameters": {
"color": 7,
"width": 180,
"height": 448,
"content": "## 2 \u00b7 Data\nPulls all rows from the pricing Excel table in OneDrive."
},
"typeVersion": 1
},
{
"id": "db0e3bcc-1e53-4fdd-b9f8-1346713b1f0d",
"name": "Section \u2013 Processing",
"type": "n8n-nodes-base.stickyNote",
"position": [
832,
0
],
"parameters": {
"color": 7,
"width": 196,
"height": 448,
"content": "## 3 \u00b7 Processing\nFilters services, applies price tier (Discount / Retail / Premium), and calculates total."
},
"typeVersion": 1
},
{
"id": "c3957feb-4194-4e72-84f2-70f16bc5f91a",
"name": "Section \u2013 Build",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
0
],
"parameters": {
"color": 7,
"width": 180,
"height": 448,
"content": "## 4 \u00b7 Proposal Build\nRenders the branded HTML document for PDF conversion.\n\nUpdate `YOUR_LOGO_URL` and the brand color variables to match your identity."
},
"typeVersion": 1
},
{
"id": "ae3f4da8-02bd-4bdf-816d-5cfd87365a2b",
"name": "Section \u2013 PDF",
"type": "n8n-nodes-base.stickyNote",
"position": [
1232,
0
],
"parameters": {
"color": 7,
"width": 180,
"height": 448,
"content": "## 5 \u00b7 PDF Conversion\nSelf-hosted [Gotenberg](https://gotenberg.dev) converts HTML \u2192 PDF.\n\nUpdate the URL to your Gotenberg instance and attach a **Basic Auth** credential."
},
"typeVersion": 1
},
{
"id": "af29b910-ddde-40ab-b3a1-541895ebdf75",
"name": "Section \u2013 Email",
"type": "n8n-nodes-base.stickyNote",
"position": [
1424,
0
],
"parameters": {
"color": 7,
"width": 200,
"height": 448,
"content": "## 6 \u00b7 Email Delivery\nSends the PDF to the requester via Gmail OAuth2.\n\nReplace `YOUR_INTERNAL_EMAIL` in the **Email Proposal** node with your own address."
},
"typeVersion": 1
},
{
"id": "b155c9f1-3352-4ebe-8b25-46397e9a6a52",
"name": "Pricing Request Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
480,
256
],
"parameters": {
"path": "pricing-request",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2
},
{
"id": "8cdce527-7d8d-4aeb-97b0-8b2d7fc2b6ee",
"name": "Fetch Pricing Sheet",
"type": "n8n-nodes-base.microsoftExcel",
"position": [
688,
256
],
"parameters": {
"table": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultName": "Table1"
},
"filters": {},
"resource": "table",
"workbook": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultName": "pricing_request.xlsx"
},
"operation": "getRows",
"returnAll": true,
"worksheet": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultName": "Sheet1"
}
},
"credentials": {
"microsoftExcelOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2.2
},
{
"id": "711c31c7-eb06-4c01-90d6-86637325d70f",
"name": "Process & Price",
"type": "n8n-nodes-base.code",
"position": [
880,
256
],
"parameters": {
"jsCode": "// Extract form data\n// Expected fields: prospectName, date, services (array or string),\n// discountLevel ('Discount' | 'Retail' | 'Premium'),\n// requestorName (format: \"Full Name (user@example.com)\")\nconst formData = $('Pricing Request Webhook').item.json.body;\nconst prospectName = formData.prospectName;\nconst date = formData.date;\nconst selectedServices = Array.isArray(formData.services) ? formData.services : [formData.services];\nconst discountLevel = formData.discountLevel; // 'Discount', 'Retail', or 'Premium'\nconst requestorInfo = formData.requestorName; // e.g. \"Jane Smith (user@example.com)\"\n\n// Extract email from requestor info\nconst emailMatch = requestorInfo.match(/\\(([^)]+)\\)/);\nconst requestorEmail = emailMatch ? emailMatch[1] : '';\nconst requestorName = requestorInfo.replace(/\\s*\\([^)]*\\)/, '').trim();\n\n// Get pricing data from Excel node\n// Expected columns: Title, Description, Cost, Discount, Retail, Premium, Unit\nconst pricingData = $('Fetch Pricing Sheet').all();\n\n// Filter pricing data for selected services (match on 'Title' column)\nconst filteredPricing = pricingData.filter(item => {\n const service = item.json.Title;\n return selectedServices.includes(service);\n});\n\n// Map pricing based on discount level\nconst processedPricing = filteredPricing.map(item => {\n const data = item.json;\n let price;\n switch (discountLevel) {\n case 'Discount': price = data.Discount; break;\n case 'Premium': price = data.Premium; break;\n default: price = data.Retail;\n }\n return {\n service: data.Title,\n description: data.Description,\n cost: data.Cost,\n price: price,\n unit: data.Unit\n };\n});\n\n// Calculate total\nconst total = processedPricing.reduce((sum, item) => sum + (parseFloat(item.price) || 0), 0);\n\nreturn [{\n json: {\n prospectName,\n date,\n requestorName,\n requestorEmail,\n discountLevel,\n selectedServices,\n pricingItems: processedPricing,\n total: total.toFixed(2)\n }\n}];"
},
"typeVersion": 2
},
{
"id": "ce1c5eb0-8bc9-432f-954e-079bae0b95b3",
"name": "Build HTML Proposal",
"type": "n8n-nodes-base.code",
"position": [
1088,
256
],
"parameters": {
"jsCode": "/**\n * Pricing Proposal \u2013 HTML Builder\n * Replace the variables below with your own branding before activating.\n */\n\nconst data = $json;\n\n// \u2500\u2500 BRANDING \u2013 update these \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst logoUrl = 'YOUR_LOGO_URL'; // e.g. https://example.com/logo.png\nconst companyName = 'Your Company Name';\nconst tagline = 'A division of Your Parent Company';\nconst colorNavy = '#002657';\nconst colorBlue = '#0021a5';\nconst colorOrange = '#fa4616';\nconst colorGold = '#f2a900';\nconst colorDark = '#343741';\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst displayDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });\nconst validUntil = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });\n\nconst pricingRows = data.pricingItems.map((item, index) =>\n '<tr style=\"' + (index % 2 === 0 ? '' : 'background-color:#f9f9f9;') + '\">'\n + '<td style=\"padding:12px;border-bottom:1px solid #eee;font-weight:bold;color:' + colorNavy + ';\">' + item.service + '</td>'\n + '<td style=\"padding:12px;border-bottom:1px solid #eee;font-size:11px;\">' + item.description + '</td>'\n + '<td style=\"padding:12px;border-bottom:1px solid #eee;text-align:center;\">' + item.unit + '</td>'\n + '<td style=\"padding:12px;border-bottom:1px solid #eee;text-align:right;font-weight:bold;color:' + colorBlue + ';\">$' + parseFloat(item.price).toFixed(2) + '</td>'\n + '</tr>'\n).join('');\n\nconst htmlContent = '<!DOCTYPE html>'\n+ '<html lang=\"en\"><head><meta charset=\"UTF-8\">'\n+ '<title>Pricing Proposal - ' + data.prospectName + '</title>'\n+ '<style>'\n+ '@page{size:letter;margin:15mm;}'\n+ 'body{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;line-height:1.5;color:' + colorDark + ';margin:0;padding:0;}'\n+ '.header{display:flex;justify-content:space-between;align-items:center;border-bottom:4px solid ' + colorOrange + ';padding-bottom:20px;margin-bottom:30px;}'\n+ '.logo{height:70px;width:auto;}'\n+ '.proposal-title{font-size:32px;font-weight:900;color:' + colorNavy + ';margin:10px 0;text-transform:uppercase;letter-spacing:-1px;}'\n+ '.cover-info{background-color:#fcfcfc;padding:25px;border-radius:4px;border:1px solid #eee;margin:20px 0;}'\n+ '.info-row{display:flex;margin:8px 0;font-size:14px;}'\n+ '.label{font-weight:bold;color:' + colorBlue + ';width:150px;text-transform:uppercase;font-size:11px;}'\n+ '.pricing-table{width:100%;border-collapse:collapse;margin:25px 0;}'\n+ '.pricing-table th{background-color:' + colorNavy + ';color:white;padding:15px 12px;text-align:left;text-transform:uppercase;font-size:11px;}'\n+ '.legal-section{margin-top:40px;padding:20px;background-color:#fffcf5;border-left:5px solid ' + colorGold + ';font-size:12px;line-height:1.7;}'\n+ '.signature-section{margin-top:40px;padding:25px;background-color:#f9f9f9;border-radius:4px;}'\n+ '.sig-line{border-bottom:2px solid ' + colorDark + ';display:inline-block;width:100%;margin-top:30px;}'\n+ '</style></head><body>'\n+ '<div class=\"header\">'\n+ '<img src=\"' + logoUrl + '\" class=\"logo\" alt=\"' + companyName + '\">'\n+ '<div style=\"text-align:right;color:' + colorNavy + ';font-size:10px;font-weight:bold;\">' + tagline + '</div>'\n+ '</div>'\n+ '<div class=\"proposal-title\">Pricing Proposal</div>'\n+ '<div class=\"cover-info\">'\n+ '<div class=\"info-row\"><span class=\"label\">Prepared For:</span><span><strong>' + data.prospectName + '</strong></span></div>'\n+ '<div class=\"info-row\"><span class=\"label\">Date:</span><span>' + displayDate + '</span></div>'\n+ '<div class=\"info-row\"><span class=\"label\">Prepared By:</span><span>' + data.requestorName + '</span></div>'\n+ '<div class=\"info-row\"><span class=\"label\">Valid Until:</span><span>' + validUntil + '</span></div>'\n+ '</div>'\n+ '<div>'\n+ '<h2 style=\"color:' + colorNavy + ';border-bottom:2px solid ' + colorOrange + ';padding-bottom:10px;font-size:18px;\">Proposed Services & Pricing</h2>'\n+ '<table class=\"pricing-table\"><thead><tr>'\n+ '<th>Service</th><th>Description</th><th style=\"text-align:center;\">Unit</th><th style=\"text-align:right;\">Price</th>'\n+ '</tr></thead><tbody>' + pricingRows + '</tbody></table>'\n+ '</div>'\n+ '<div class=\"legal-section\">'\n+ '<h3 style=\"color:' + colorNavy + ';margin-top:0;font-size:14px;\">Terms & Conditions</h3>'\n+ '<p>This pricing proposal is valid for <strong>thirty (30) days</strong>. After this period, all prices are subject to change. A binding contract is only created upon execution of a separate Services Agreement.</p>'\n+ '</div>'\n+ '<div class=\"signature-section\">'\n+ '<h3 style=\"color:' + colorNavy + ';margin-top:0;font-size:14px;\">Authorization & Acceptance</h3>'\n+ '<table style=\"width:100%;margin-top:20px;\"><tr>'\n+ '<td style=\"width:60%;padding-right:20px;\"><div class=\"sig-line\"></div><div style=\"font-size:10px;color:' + colorDark + ';margin-top:5px;\">Client Authorized Signature</div></td>'\n+ '<td style=\"width:30%;\"><div class=\"sig-line\"></div><div style=\"font-size:10px;color:' + colorDark + ';margin-top:5px;\">Date</div></td>'\n+ '</tr><tr><td colspan=\"2\" style=\"padding-top:20px;\">'\n+ '<div class=\"sig-line\" style=\"width:60%;\"></div>'\n+ '<div style=\"font-size:10px;color:' + colorDark + ';margin-top:5px;\">Printed Name & Title</div>'\n+ '</td></tr></table></div>'\n+ '</body></html>';\n\nconst htmlBuffer = Buffer.from(htmlContent, 'utf-8');\n\nreturn [{\n binary: {\n data: {\n data: htmlBuffer.toString('base64'),\n mimeType: 'text/html',\n fileName: 'index.html',\n fileExtension: 'html'\n }\n },\n json: {\n htmlContent: htmlContent,\n fileName: data.prospectName.replace(/[^a-zA-Z0-9]/g, '-') + '-Pricing-Proposal-' + new Date().toISOString().split('T')[0] + '.pdf',\n prospectName: data.prospectName,\n requestorEmail: data.requestorEmail,\n requestorName: data.requestorName\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a491e6b8-873f-4fb0-88a4-9d1d6b49c7d0",
"name": "Gotenberg \u2192 PDF",
"type": "n8n-nodes-base.httpRequest",
"position": [
1280,
256
],
"parameters": {
"url": "https://YOUR_GOTENBERG_INSTANCE/forms/chromium/convert/html",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"sendBody": true,
"contentType": "multipart-form-data",
"sendHeaders": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "index.html",
"parameterType": "formBinaryData",
"inputDataFieldName": "=data"
}
]
},
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Gotenberg-Output-Filename",
"value": "={{ $('Build HTML Proposal').item.json.fileName }}"
}
]
}
},
"typeVersion": 4.4
},
{
"id": "01262f19-5231-446e-8ea3-4d2bdf0ea0e4",
"name": "Send a message",
"type": "n8n-nodes-base.microsoftOutlook",
"position": [
1472,
256
],
"parameters": {
"subject": "=Pricing Proposal prepared for {{ $('Build HTML Proposal').item.json.prospectName }}\n",
"bodyContent": "=Please find the attached pricing proposal for {{ $('Build HTML Proposal').item.json.prospectName }}, requested by {{ $('Build HTML Proposal').item.json.requestorName }}. The attached pricing is valid for 30 days.\n",
"toRecipients": "=YOUR_INTERNAL_EMAIL;{{ $('Build HTML Proposal').item.json.requestorEmail }}\n",
"additionalFields": {
"attachments": {
"attachments": [
{
"binaryPropertyName": "data"
}
]
}
}
},
"credentials": {
"microsoftOutlookOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "69b14a64-11a4-40a0-8f23-4bebf29fb3c3",
"connections": {
"Process & Price": {
"main": [
[
{
"node": "Build HTML Proposal",
"type": "main",
"index": 0
}
]
]
},
"Gotenberg \u2192 PDF": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
},
"Build HTML Proposal": {
"main": [
[
{
"node": "Gotenberg \u2192 PDF",
"type": "main",
"index": 0
}
]
]
},
"Fetch Pricing Sheet": {
"main": [
[
{
"node": "Process & Price",
"type": "main",
"index": 0
}
]
]
},
"Pricing Request Webhook": {
"main": [
[
{
"node": "Fetch Pricing Sheet",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
microsoftExcelOAuth2ApimicrosoftOutlookOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This template is built for consultants, agencies, and service businesses that quote custom engagements and want to eliminate the manual work of building, formatting, and delivering pricing proposals. If you have a SharePoint or OneDrive pricing sheet, a self-hosted Gotenberg…
Source: https://n8n.io/workflows/14982/ — 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.
This workflow connects Salesforce and Geotab to streamline fleet tracking for field service jobs (Work Orders). When a new Work Order is created in Salesforce (with a 'New' status and valid coordinate
Receives meeting data via a webform, cleans/structures it, fills a Word docx template, uploads the file to SharePoint, appends a row to Excel 365, and sends an Outlook email with the document attached
Automate WhatsApp communication for recruitment agencies with an interactive, structured customer experience. This workflow handles pricing inquiries, request submissions, tracking, complaints, and hu
This template turns Podium's conversation inbox into a full sales CRM with a custom funnel, AI message classification, automated drip follow-ups, daily admin reports, and a live Kanban dashboard. Six
Suspicious_login_detection. Uses postgres, httpRequest, noOp, html. Webhook trigger; 43 nodes.