AutomationFlowsEmail & Gmail › Generate and Email PDF Quotes From Airtable via Gmail and Google Drive

Generate and Email PDF Quotes From Airtable via Gmail and Google Drive

ByAnwar Bouilouta @anwar on n8n.io

Most service businesses and freelancers track their quotes in Airtable (or something similar), but when it comes to actually sending the quote, they're still manually copying data into a document, exporting a PDF, attaching it to an email, and then going back to update the…

Webhook trigger★★★★☆ complexity15 nodesAirtableHTTP RequestGoogle DriveGmail
Email & Gmail Trigger: Webhook Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #14174 — we link there as the canonical source.

This workflow follows the Airtable → Gmail 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 →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "fb92f0b5-ccbf-4b7c-89ec-313dfa46f64b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        864,
        -176
      ],
      "parameters": {
        "color": 3,
        "width": 460,
        "height": 720,
        "content": "### Generate and email PDF quotes from Airtable records\n\nIf you're managing quotes in Airtable and still copying data into a Google Doc or Word template every time, this saves you the hassle. Hit the webhook, and the workflow handles everything from there.\n\n### How it works\n1. You trigger the webhook with a record ID (either from an Airtable automation or a manual HTTP call)\n2. It pulls the quote record from your Airtable base\n3. A code node takes your business details and the line items, then builds a proper branded HTML quote with subtotals, tax, and a grand total\n4. That HTML gets converted to a PDF via pdf.co (free tier works fine)\n5. The PDF lands in your Google Drive folder and gets emailed to the client as an attachment\n6. Finally, the Airtable record gets marked as \"Sent\" with the Drive link attached\n\n### Setup\n1. You need an Airtable base with a \"Quotes\" table. Columns: Client Name, Client Email, Line Items (long text field, one item per line like `Website Design | 1 | 2500`), Tax Rate, Notes, Status\n2. Open the \"Configure Settings\" node and fill in your business name, email, address, and Drive folder ID\n3. Connect your Airtable, Gmail, and Google Drive credentials\n4. Grab a free API key from pdf.co and add it as an HTTP Header Auth credential\n5. Activate and test with a real Airtable record ID"
      },
      "typeVersion": 1
    },
    {
      "id": "109c5ea5-36e0-4873-b097-d552e3c32e27",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        -16
      ],
      "parameters": {
        "width": 260,
        "height": 140,
        "content": "## 1. Trigger\nWebhook receives a POST with the Airtable record ID. You can call this from an Airtable automation or any HTTP client."
      },
      "typeVersion": 1
    },
    {
      "id": "d00df63f-5a6b-46f2-a35d-89d7ef437000",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1632,
        320
      ],
      "parameters": {
        "width": 280,
        "height": 140,
        "content": "## 2. Your settings\nAll the stuff you need to change is here. Business name, email, brand color, Drive folder, etc."
      },
      "typeVersion": 1
    },
    {
      "id": "01d21184-8b87-40c2-885a-20a0d870128b",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1952,
        -32
      ],
      "parameters": {
        "width": 260,
        "height": 140,
        "content": "## 3. Pull the quote\nGrabs the record from Airtable using the ID from the webhook."
      },
      "typeVersion": 1
    },
    {
      "id": "4edd844f-4dfa-4ce8-9ea0-4f5b1bab1ded",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2256,
        336
      ],
      "parameters": {
        "width": 280,
        "height": 140,
        "content": "## 4. Build the quote\nThis is where the magic happens. Parses line items, calculates totals, and builds a clean HTML document you can customize."
      },
      "typeVersion": 1
    },
    {
      "id": "0886b69d-3aca-44f3-827f-16f30160f58f",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2576,
        -16
      ],
      "parameters": {
        "width": 964,
        "height": 524,
        "content": "## 5. PDF, save, and send\nConverts the HTML to PDF, uploads it to your Drive folder, and emails it to the client. The Airtable record gets updated with the link so you know it went out."
      },
      "typeVersion": 1
    },
    {
      "id": "56aceb31-a772-4d06-930e-e9a0ecc0589c",
      "name": "Quote Generation Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1424,
        144
      ],
      "parameters": {
        "path": "generate-quote",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "922a11d6-8e04-46a6-8543-a95503462e3e",
      "name": "Configure Settings",
      "type": "n8n-nodes-base.set",
      "position": [
        1712,
        144
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "config-1",
              "name": "airtableBaseId",
              "type": "string",
              "value": "YOUR_AIRTABLE_BASE_ID"
            },
            {
              "id": "config-2",
              "name": "airtableTableName",
              "type": "string",
              "value": "Quotes"
            },
            {
              "id": "config-3",
              "name": "businessName",
              "type": "string",
              "value": "Your Business Name"
            },
            {
              "id": "config-4",
              "name": "businessEmail",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "config-5",
              "name": "businessAddress",
              "type": "string",
              "value": "123 Main Street, City, State 12345"
            },
            {
              "id": "config-6",
              "name": "businessPhone",
              "type": "string",
              "value": "+1234567890"
            },
            {
              "id": "config-7",
              "name": "brandColor",
              "type": "string",
              "value": "#2563eb"
            },
            {
              "id": "config-8",
              "name": "googleDriveFolderId",
              "type": "string",
              "value": "YOUR_GOOGLE_DRIVE_FOLDER_ID"
            },
            {
              "id": "config-9",
              "name": "recordId",
              "type": "string",
              "value": "={{ $json.body.recordId || $json.query.recordId }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "c94a4cbd-8729-455c-902a-12140c5b2a71",
      "name": "Read Quote from Airtable",
      "type": "n8n-nodes-base.airtable",
      "position": [
        2032,
        144
      ],
      "parameters": {
        "id": "={{ $json.recordId }}",
        "base": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.airtableTableName }}"
        },
        "options": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "c8ddfdd5-3799-431c-a8ad-935956bc6c29",
      "name": "Build HTML Quote",
      "type": "n8n-nodes-base.code",
      "position": [
        2336,
        144
      ],
      "parameters": {
        "jsCode": "const config = $('Configure Settings').first().json;\nconst record = $input.first().json;\n\nconst clientName = record['Client Name'] || 'Client';\nconst clientEmail = record['Client Email'] || '';\nconst lineItemsRaw = record['Line Items'] || '';\nconst taxRate = parseFloat(record['Tax Rate'] || '0') / 100;\nconst notes = record['Notes'] || '';\nconst quoteRef = `Q-${Date.now().toString(36).toUpperCase()}`;\nconst today = new Date().toISOString().split('T')[0];\nconst validDays = 30;\nconst validUntil = new Date(Date.now() + validDays * 86400000).toISOString().split('T')[0];\n\n// Parse line items: \"Description | Qty | Price\" per line\nconst lineItems = lineItemsRaw.split('\\n').filter(l => l.trim()).map(line => {\n  const parts = line.split('|').map(p => p.trim());\n  const description = parts[0] || 'Item';\n  const qty = parseInt(parts[1] || '1');\n  const price = parseFloat(parts[2] || '0');\n  return { description, qty, price, total: qty * price };\n});\n\nconst subtotal = lineItems.reduce((sum, item) => sum + item.total, 0);\nconst tax = subtotal * taxRate;\nconst grandTotal = subtotal + tax;\n\nconst itemRows = lineItems.map((item, i) =>\n  `<tr>\n    <td style=\"padding:12px 16px;border-bottom:1px solid #e5e7eb;\">${i + 1}</td>\n    <td style=\"padding:12px 16px;border-bottom:1px solid #e5e7eb;\">${item.description}</td>\n    <td style=\"padding:12px 16px;border-bottom:1px solid #e5e7eb;text-align:center;\">${item.qty}</td>\n    <td style=\"padding:12px 16px;border-bottom:1px solid #e5e7eb;text-align:right;\">$${item.price.toFixed(2)}</td>\n    <td style=\"padding:12px 16px;border-bottom:1px solid #e5e7eb;text-align:right;font-weight:600;\">$${item.total.toFixed(2)}</td>\n  </tr>`\n).join('');\n\nconst html = `<!DOCTYPE html>\n<html>\n<head><meta charset=\"UTF-8\"><style>\n  body { font-family: 'Helvetica Neue', Arial, sans-serif; color: #1f2937; margin: 0; padding: 40px; }\n  .header { display: flex; justify-content: space-between; margin-bottom: 40px; }\n  .brand { font-size: 24px; font-weight: 800; color: ${config.brandColor}; }\n  table { width: 100%; border-collapse: collapse; }\n  th { background: ${config.brandColor}; color: white; padding: 12px 16px; text-align: left; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; }\n  th:nth-child(3), th:nth-child(4), th:nth-child(5) { text-align: center; }\n  th:last-child { text-align: right; }\n</style></head>\n<body>\n  <div class=\"header\">\n    <div>\n      <div class=\"brand\">${config.businessName}</div>\n      <p style=\"color:#6b7280;font-size:13px;margin:4px 0;\">${config.businessAddress}</p>\n      <p style=\"color:#6b7280;font-size:13px;margin:4px 0;\">${config.businessPhone} | ${config.businessEmail}</p>\n    </div>\n    <div style=\"text-align:right;\">\n      <h1 style=\"font-size:32px;color:${config.brandColor};margin:0;\">QUOTE</h1>\n      <p style=\"color:#6b7280;font-size:14px;margin:8px 0;\">Ref: ${quoteRef}</p>\n      <p style=\"color:#6b7280;font-size:14px;margin:4px 0;\">Date: ${today}</p>\n      <p style=\"color:#6b7280;font-size:14px;margin:4px 0;\">Valid until: ${validUntil}</p>\n    </div>\n  </div>\n\n  <div style=\"background:#f9fafb;border-radius:8px;padding:20px;margin-bottom:32px;\">\n    <p style=\"font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#9ca3af;margin:0 0 8px 0;\">Prepared for</p>\n    <p style=\"font-size:18px;font-weight:700;margin:0;\">${clientName}</p>\n    <p style=\"color:#6b7280;font-size:14px;margin:4px 0;\">${clientEmail}</p>\n  </div>\n\n  <table>\n    <thead>\n      <tr><th>#</th><th>Description</th><th>Qty</th><th>Unit Price</th><th style=\"text-align:right;\">Total</th></tr>\n    </thead>\n    <tbody>${itemRows}</tbody>\n  </table>\n\n  <div style=\"display:flex;justify-content:flex-end;margin-top:24px;\">\n    <div style=\"width:280px;\">\n      <div style=\"display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #e5e7eb;\">\n        <span style=\"color:#6b7280;\">Subtotal</span>\n        <span>$${subtotal.toFixed(2)}</span>\n      </div>\n      <div style=\"display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #e5e7eb;\">\n        <span style=\"color:#6b7280;\">Tax (${(taxRate * 100).toFixed(0)}%)</span>\n        <span>$${tax.toFixed(2)}</span>\n      </div>\n      <div style=\"display:flex;justify-content:space-between;padding:12px 0;font-size:18px;font-weight:800;\">\n        <span>Total</span>\n        <span style=\"color:${config.brandColor};\">$${grandTotal.toFixed(2)}</span>\n      </div>\n    </div>\n  </div>\n\n  ${notes ? `<div style=\"margin-top:32px;padding:20px;background:#f9fafb;border-radius:8px;border-left:4px solid ${config.brandColor};\"><p style=\"font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#9ca3af;margin:0 0 8px 0;\">Notes</p><p style=\"margin:0;color:#4b5563;\">${notes}</p></div>` : ''}\n\n  <div style=\"margin-top:48px;text-align:center;color:#9ca3af;font-size:12px;\">\n    <p>Thank you for considering ${config.businessName}.</p>\n  </div>\n</body>\n</html>`;\n\nreturn [{\n  json: {\n    html,\n    clientName,\n    clientEmail,\n    quoteRef,\n    grandTotal: grandTotal.toFixed(2),\n    today,\n    recordId: config.recordId,\n    airtableBaseId: config.airtableBaseId,\n    airtableTableName: config.airtableTableName,\n    googleDriveFolderId: config.googleDriveFolderId,\n    businessName: config.businessName\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c1c7d91e-fba6-4889-9c59-9f6ebea9fbe2",
      "name": "Convert HTML to PDF",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2656,
        144
      ],
      "parameters": {
        "url": "https://api.pdf.co/v1/pdf/convert/from/html",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={{ JSON.stringify({ html: $json.html, name: $json.quoteRef + '.pdf', margins: '20px', paperSize: 'A4' }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "595e2868-a63c-4b14-abe1-24f145570ac6",
      "name": "Download PDF File",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2880,
        144
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6faf7eea-642f-4895-9006-4eae25dd3b86",
      "name": "Save PDF to Google Drive",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        3104,
        80
      ],
      "parameters": {
        "name": "={{ $('Build HTML Quote').first().json.quoteRef }}.pdf",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Build HTML Quote').first().json.googleDriveFolderId }}"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "3750061f-b38b-4f3f-894a-ae1ffa1bc385",
      "name": "Email Quote to Client",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3104,
        288
      ],
      "parameters": {
        "sendTo": "={{ $('Build HTML Quote').first().json.clientEmail }}",
        "message": "=<p>Hi {{ $('Build HTML Quote').first().json.clientName }},</p><p>Please find your quote ({{ $('Build HTML Quote').first().json.quoteRef }}) attached. The total is ${{ $('Build HTML Quote').first().json.grandTotal }}.</p><p>This quote is valid for 30 days. Please let us know if you have any questions.</p><p>Best regards,<br>{{ $('Build HTML Quote').first().json.businessName }}</p>",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "=Quote {{ $('Build HTML Quote').first().json.quoteRef }} from {{ $('Build HTML Quote').first().json.businessName }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ef828708-bc98-4797-a8a8-5fc14336d654",
      "name": "Update Airtable Status",
      "type": "n8n-nodes-base.airtable",
      "position": [
        3344,
        144
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Build HTML Quote').first().json.airtableTableName }}"
        },
        "columns": {
          "value": {
            "Status": "Sent",
            "PDF Link": "={{ $('Save PDF to Google Drive').first().json.webViewLink || '' }}",
            "Sent Date": "={{ $('Build HTML Quote').first().json.today }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "update"
      },
      "typeVersion": 2.1
    }
  ],
  "connections": {
    "Build HTML Quote": {
      "main": [
        [
          {
            "node": "Convert HTML to PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download PDF File": {
      "main": [
        [
          {
            "node": "Save PDF to Google Drive",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Quote to Client",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Settings": {
      "main": [
        [
          {
            "node": "Read Quote from Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert HTML to PDF": {
      "main": [
        [
          {
            "node": "Download PDF File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quote Generation Webhook": {
      "main": [
        [
          {
            "node": "Configure Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Quote from Airtable": {
      "main": [
        [
          {
            "node": "Build HTML Quote",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save PDF to Google Drive": {
      "main": [
        [
          {
            "node": "Update Airtable Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Most service businesses and freelancers track their quotes in Airtable (or something similar), but when it comes to actually sending the quote, they're still manually copying data into a document, exporting a PDF, attaching it to an email, and then going back to update the…

Source: https://n8n.io/workflows/14174/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

Automate short-term trading research by generating high-quality trade ideas using MCP (Market Context Protocol) signals and AI-powered analysis. 📈🤖 This workflow evaluates market context, catalysts, m

Slack, Asana, HTTP Request +4
Email & Gmail

Receive booking requests via webhook with automatic validation, duplicate detection, availability checking, confirmation emails, Google Calendar sync, and Slack notifications.

HTTP Request, Gmail, Google Calendar +2
Email & Gmail

This template is built to be customized for your specific needs. This template has the core logic and n8n node specific references sorted to work with dynamic file names throughout the workflow. Store

Gmail, Slack, Gmail Trigger +3
Email & Gmail

What This Does: Search your past trips using natural language queries like: "Show my Goa hotel from last year" "Where did I stay in Paris?" "Find my beach trips from 2024" "Show all Italy restaurants

HTTP Request, Google Drive
Email & Gmail

Automate building visitor management with secure verification, digital entry passes, and real-time security notifications.

N8N Nodes Verifiemail, HTTP Request, N8N Nodes Htmlcsstopdf +2