{
  "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
          }
        ]
      ]
    }
  }
}