AutomationFlowsAI & RAG › Send Milestone Billing Invoices with Google Sheets, Gpt-4o Mini, Gmail and Slack

Send Milestone Billing Invoices with Google Sheets, Gpt-4o Mini, Gmail and Slack

ByRahul Joshi @rahul08 on n8n.io

This workflow polls a Google Sheets milestone tracker every 15 minutes, calculates milestone-based invoice amounts, uses OpenAI (gpt-4o-mini) to draft invoice messaging, emails the invoice via Gmail with an approval link, then records approval in Google Sheets and notifies an…

Event trigger★★★★☆ complexityAI-powered24 nodesError TriggerSlackGoogle SheetsOpenAIGmail
AI & RAG Trigger: Event Nodes: 24 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Error Trigger → 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
{
  "id": "1bk3zMNyH5s1ennY",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Milestone Billing & Client Invoice Approval Workflow",
  "tags": [],
  "nodes": [
    {
      "id": "c6d62313-62ed-4362-b37a-3005b84515fd",
      "name": "On Workflow Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -1024,
        2368
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "42071f58-5d27-46ae-9043-7aecd0a3ab78",
      "name": "Slack \u2013 Send Error Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        -768,
        2368
      ],
      "parameters": {
        "text": "=\u26a0\ufe0f Prescription workflow error: {{ $json.error.message }}\nNode: {{ $json.execution.lastNodeExecuted }}\nExecution ID: {{ $json.execution.id }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "pharmacy-alerts"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "fb7fd0c5-4322-4920-a48d-33b67e449a4a",
      "name": "Section: Error Handler",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1120,
        2208
      ],
      "parameters": {
        "color": 7,
        "width": 556,
        "height": 368,
        "content": "## \u26a0\ufe0f Error Handler\nCatches any failure in the workflow and posts a Slack alert with the error message, failing node name, and execution ID. Wire the error output of any critical node here to prevent silent failures going unnoticed."
      },
      "typeVersion": 1
    },
    {
      "id": "a60aa55f-e6d7-4748-a430-742687861418",
      "name": "Overview \u2013 Invoice Bridge",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2224,
        912
      ],
      "parameters": {
        "width": 684,
        "height": 660,
        "content": "## \ud83e\uddfe Invoice Bridge \u2013 Automated Milestone Billing\n\n### How it works\nThis workflow monitors a Google Sheets milestone tracker every 15 minutes. When a project milestone reaches its invoice trigger threshold (e.g. 50% complete), it automatically calculates the billable amount, uses GPT-4o mini to write a professional invoice narrative, generates a formatted HTML invoice, and emails it to the client with a one-click approval button.\n\nOnce the client approves, the workflow logs the invoice, updates the milestone status in Sheets, and notifies your accounts team in Slack \u2014 all without manual intervention.\n\n### Setup steps\n1. **Google Sheets** \u2013 Connect your OAuth2 credentials and update the `documentId` in both Sheets nodes to point to your own milestone tracker.\n2. **OpenAI** \u2013 Add your OpenAI API key under credentials. The workflow uses `gpt-4o-mini` by default.\n3. **Gmail** \u2013 Connect Gmail via OAuth2. Update the `sendTo` field in the Gmail node with your client-facing email address.\n4. **Slack** \u2013 Connect your Slack workspace and update the `channelId` in both Slack nodes to your target channels.\n5. **Approval Webhook** \u2013 Replace `YOUR_N8N_WEBHOOK_BASE_URL` in the merge node with your live n8n instance URL.\n6. **Sheet structure** \u2013 Your Milestones sheet must include columns: `Milestone ID`, `Contract ID`, `Client Name`, `% Complete`, `Invoice Trigger %`, `Contract Value ($)`, `Invoice Status`, `Currency`.\n7. Activate the workflow and run a manual test with a milestone row set to `Invoice Status: Pending` and `% Complete` \u2265 `Invoice Trigger %`."
      },
      "typeVersion": 1
    },
    {
      "id": "a41c7929-58b5-4122-ac4e-495d3c7ee7ba",
      "name": "Section: Trigger & Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1264,
        1536
      ],
      "parameters": {
        "color": 7,
        "width": 928,
        "height": 556,
        "content": "## \u23f1\ufe0f Trigger & Milestone Filter\nPolls the Milestones sheet every 15 minutes. Rows pass through only when `% Complete` meets or exceeds the `Invoice Trigger %` and the status is still `Pending`. Everything else is silently skipped \u2014 no duplicate invoices."
      },
      "typeVersion": 1
    },
    {
      "id": "404f7bad-3b83-4baa-b644-4c674fa8740a",
      "name": "Section: Billing & AI Narrative",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        1536
      ],
      "parameters": {
        "color": 7,
        "width": 832,
        "height": 556,
        "content": "## \ud83e\uddee Billing Calculation & AI Narrative\nComputes the exact billable amount from the contract value and trigger percentage, generates unique invoice IDs, and calls GPT-4o mini to produce a professional invoice narrative, payment terms, email subject, and HTML email body \u2014 all returned as structured JSON."
      },
      "typeVersion": 1
    },
    {
      "id": "08553b31-bbe1-45a0-81b8-4ea0836c666a",
      "name": "Section: Invoice Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        1536
      ],
      "parameters": {
        "color": 7,
        "width": 540,
        "height": 536,
        "content": "## \ud83d\udce4 Invoice Generation & Delivery\nBuilds a styled HTML invoice document and sends it to the client via Gmail. The email includes an inline **Approve Invoice** button linked to a unique approval token \u2014 so clients can confirm with one click, no login required."
      },
      "typeVersion": 1
    },
    {
      "id": "3829a702-45db-40ca-a794-55163f161c46",
      "name": "Section: Approval & Logging",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        1280
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 936,
        "content": "## \ud83d\udccb Approval Routing & Sheet Logging\nAfter the client approves, the workflow simultaneously updates the milestone status to `Sent`, appends a full record to the Invoice Log sheet, and stamps the approval timestamp. All three Sheets operations run in parallel via the `If` branch."
      },
      "typeVersion": 1
    },
    {
      "id": "4006d213-decb-4d74-862e-caaba5a26fc1",
      "name": "Section: Slack Notification",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1904,
        1536
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 504,
        "content": "## \ud83d\udd14 Accounts Team Notification\nOnce approval is recorded, a formatted Slack block message alerts the accounts team with the Milestone ID and approval timestamp. This is the handoff point \u2014 the team knows to monitor for incoming payment and mark the row as `Paid` once it clears."
      },
      "typeVersion": 1
    },
    {
      "id": "69378d63-93dc-4a4e-9b95-f538919033e8",
      "name": "Credentials & Security",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2000,
        2272
      ],
      "parameters": {
        "color": 3,
        "width": 336,
        "height": 212,
        "content": "## \ud83d\udd10 Credentials & Security\nUse OAuth2 for Google Sheets and Gmail. Add OpenAI and Slack via their respective credential types. Never hardcode API keys in Code nodes. Replace all sample emails, sheet IDs, and channel IDs with your own values before sharing this template."
      },
      "typeVersion": 1
    },
    {
      "id": "dca6c3a0-4022-408d-96e0-c281f9982041",
      "name": "Schedule \u2013 Poll Milestones1",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1216,
        1792
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8c475fb9-687e-4127-b376-f6914a004ef0",
      "name": "Sheets \u2013 Read Milestones1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -976,
        1792
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1612767382,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=1612767382",
          "cachedResultName": "Milestones"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit",
          "cachedResultName": "Invoice Tracker"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "7d54fda3-94b8-43ad-9c74-39e095f4c4a1",
      "name": "Filter \u2013 Ready to Invoice1",
      "type": "n8n-nodes-base.code",
      "position": [
        -736,
        1792
      ],
      "parameters": {
        "jsCode": "// Filter rows where % Complete >= Invoice Trigger % AND status is Pending\nconst rows = $input.all().map(i => i.json);\n\nconst ready = rows.filter(r => {\n  const pct     = parseFloat(r['% Complete']  ?? r.percent_complete ?? 0);\n  const trigger = parseFloat(r['Invoice Trigger %'] ?? r.invoice_trigger ?? 0);\n  const status  = (r['Invoice Status'] ?? r.invoice_status ?? '').toLowerCase().trim();\n  return pct >= trigger && trigger > 0 && status === 'pending';\n});\n\nif (ready.length === 0) {\n  return [{ json: { _skip: true, message: 'No milestones ready for invoicing.' } }];\n}\n\nreturn ready.map(r => ({ json: r }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6df74bf1-c806-41f1-ad69-f8fdc74dd17a",
      "name": "IF \u2013 Skip if None Ready1",
      "type": "n8n-nodes-base.if",
      "position": [
        -496,
        1792
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "d607d67e-9405-4f1a-a45b-fd1d552c1341",
              "operator": {
                "type": "boolean",
                "operation": "equal"
              },
              "leftValue": "={{ $json._skip }}",
              "rightValue": true
            },
            {
              "id": "3c98d950-5957-418a-bfa7-cd7b0cf717ea",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json[\"Invoice Status\"] }}",
              "rightValue": "completed"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "7b4b43bd-142b-4173-90df-27725aeef7a1",
      "name": "Code \u2013 Calculate Billable Amount1",
      "type": "n8n-nodes-base.code",
      "position": [
        -192,
        1808
      ],
      "parameters": {
        "jsCode": "// Calculate billable amount and build context for GPT\nconst r = $input.item.json;\n\nconst contractValue = parseFloat(r['Contract Value ($)'] ?? r.contract_value ?? 0);\nconst triggerPct    = parseFloat(r['Invoice Trigger %']  ?? r.invoice_trigger ?? 0);\nconst billable      = contractValue * (triggerPct / 100);\n\nconst now = new Date();\nconst invoiceDate = now.toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' });\nconst dueDate = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000)\n  .toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' });\n\nconst invoiceId = 'INV-' + now.getFullYear() + '-' + String(Math.floor(Math.random()*9000)+1000);\n\nreturn [{ json: {\n  ...r,\n  billableAmount:  billable,\n  contractValue:   contractValue,\n  triggerPct:      triggerPct,\n  invoiceId:       invoiceId,\n  invoiceDate:     invoiceDate,\n  dueDate:         dueDate,\n  currency:        r.Currency ?? 'USD'\n}}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d757a33c-9b14-41f7-b589-17c8c545988e",
      "name": "GPT-4o \u2013 Write Invoice Narrative1",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        48,
        1808
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=You are a professional construction project billing specialist. Write a formal, concise invoice narrative for the following milestone payment.\n\nReturn ONLY a JSON object with these exact fields, no markdown, no code fences:\n{\n  \"invoiceNarrative\": \"<2-3 sentence professional description of work completed and basis for this invoice>\",\n  \"paymentTerms\": \"<one sentence payment terms>\",\n  \"subject\": \"<professional email subject line for sending this invoice>\",\n  \"emailBody\": \"<professional 3-paragraph HTML email body to send to the client, referencing the invoice details below>\"\n}\n\nInvoice Details:\n- Invoice ID: {{ $json.invoiceId }}\n- Client: {{ $json['Client Name'] ?? $json.client_name ?? 'Client' }}\n- Project: {{ $json['Milestone Name'] ?? $json.milestone_name }}\n- Contract ID: {{ $json['Contract ID'] ?? $json.contract_id }}\n- Milestone: {{ $json['Milestone Name'] ?? $json.milestone_name }}\n- % Complete: {{ $json['% Complete'] ?? $json.percent_complete }}%\n- Invoice Trigger: {{ $json.triggerPct }}% of contract\n- Contract Value: {{ $json.currency }} {{ $json.contractValue.toLocaleString() }}\n- Amount Due: {{ $json.currency }} {{ $json.billableAmount.toLocaleString() }}\n- Invoice Date: {{ $json.invoiceDate }}\n- Payment Due: {{ $json.dueDate }}\n\nReturn ONLY the JSON object."
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c97cf2c6-03e0-4ff8-ae84-2be77337c6af",
      "name": "Code \u2013 Merge GPT + Build Approval URL1",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        1808
      ],
      "parameters": {
        "jsCode": "// Parse GPT response and merge with invoice data\nconst raw = $input.item.json.message?.content ?? $input.item.json.choices?.[0]?.message?.content ?? '{}';\nlet gpt;\ntry {\n  gpt = JSON.parse(raw);\n} catch(e) {\n  const match = raw.match(/\\{[\\s\\S]*\\}/);\n  gpt = match ? JSON.parse(match[0]) : {\n    invoiceNarrative: 'Payment due for milestone completion as per contract.',\n    paymentTerms: 'Payment due within 14 days of invoice date.',\n    subject: 'Invoice ' + $('Code \u2013 Calculate Billable Amount1').item.json.invoiceId,\n    emailBody: '<p>Please find attached your invoice.</p>'\n  };\n}\n\nconst inv = $('Code \u2013 Calculate Billable Amount1').item.json;\n\n// Build approval webhook URL (n8n webhook path)\nconst approvalToken = Buffer.from(inv.invoiceId + ':' + Date.now()).toString('base64').replace(/=/g,'');\nconst approvalUrl = 'YOUR_N8N_WEBHOOK_BASE_URL/webhook/invoice-approve?token=' + approvalToken + '&invoiceId=' + encodeURIComponent(inv.invoiceId) + '&milestoneId=' + encodeURIComponent(inv['Milestone ID'] ?? '');\n\nreturn [{ json: { ...inv, ...gpt, approvalToken, approvalUrl } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "cd36bc8c-10bf-4ed1-b966-35d54c45fb12",
      "name": "Code \u2013 Generate HTML Invoice1",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        1808
      ],
      "parameters": {
        "jsCode": "// Generate a clean HTML invoice document\nconst d = $input.item.json;\n\nconst fmtMoney = (n) => {\n  const num = parseFloat(n) || 0;\n  return num.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n};\n\nconst html = `<!DOCTYPE html>\n<html>\n<head>\n<meta charset='UTF-8'>\n<style>\n  body { font-family: Arial, sans-serif; color: #222; margin: 0; padding: 0; background: #f5f5f5; }\n  .page { max-width: 780px; margin: 40px auto; background: #fff; padding: 48px; border-radius: 8px; box-shadow: 0 2px 16px rgba(0,0,0,0.10); }\n  .logo-row { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 32px; }\n  .company-name { font-size: 22px; font-weight: bold; color: #1F4E79; }\n  .invoice-label { font-size: 32px; font-weight: bold; color: #1F4E79; letter-spacing: 2px; }\n  .invoice-meta { text-align: right; font-size: 13px; color: #555; margin-top: 6px; }\n  .divider { border: none; border-top: 2px solid #1F4E79; margin: 24px 0; }\n  .two-col { display: flex; justify-content: space-between; margin-bottom: 24px; }\n  .two-col div { width: 48%; }\n  .label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 2px; }\n  .value { font-size: 14px; color: #222; font-weight: 600; }\n  table { width: 100%; border-collapse: collapse; margin: 24px 0; }\n  th { background: #1F4E79; color: #fff; padding: 10px 14px; text-align: left; font-size: 13px; }\n  td { padding: 10px 14px; border-bottom: 1px solid #e8e8e8; font-size: 13px; }\n  tr:nth-child(even) td { background: #f0f5fa; }\n  .total-row td { font-weight: bold; font-size: 15px; background: #DEEAF1 !important; color: #1F4E79; }\n  .narrative { background: #f8f8f8; border-left: 4px solid #1F4E79; padding: 14px 18px; border-radius: 4px; font-size: 13px; color: #444; margin: 20px 0; }\n  .approve-btn { display: inline-block; margin-top: 28px; padding: 14px 36px; background: #1F4E79; color: #fff; text-decoration: none; border-radius: 6px; font-size: 15px; font-weight: bold; letter-spacing: 0.5px; }\n  .footer { margin-top: 40px; font-size: 11px; color: #aaa; text-align: center; }\n</style>\n</head>\n<body>\n<div class='page'>\n  <div class='logo-row'>\n    <div>\n      <div class='company-name'>YOUR COMPANY NAME</div>\n      <div style='font-size:12px;color:#888;margin-top:4px;'>123 Business Street, City, State 00000<br>accounts@yourcompany.com | +1 (000) 000-0000</div>\n    </div>\n    <div>\n      <div class='invoice-label'>INVOICE</div>\n      <div class='invoice-meta'><b>${d.invoiceId}</b><br>Date: ${d.invoiceDate}<br>Due: ${d.dueDate}</div>\n    </div>\n  </div>\n  <hr class='divider'>\n  <div class='two-col'>\n    <div>\n      <div class='label'>Bill To</div>\n      <div class='value'>${d['Client Name'] ?? d.client_name ?? 'Client'}</div>\n    </div>\n    <div>\n      <div class='label'>Contract Reference</div>\n      <div class='value'>${d['Contract ID'] ?? d.contract_id ?? ''}</div>\n    </div>\n  </div>\n  <table>\n    <thead>\n      <tr><th>Description</th><th>Milestone</th><th>% Billed</th><th>Amount (${d.currency})</th></tr>\n    </thead>\n    <tbody>\n      <tr>\n        <td>${d.invoiceNarrative ?? 'Milestone payment as per contract.'}</td>\n        <td>${d['Milestone Name'] ?? d.milestone_name ?? ''}</td>\n        <td>${d.triggerPct}%</td>\n        <td>${fmtMoney(d.billableAmount)}</td>\n      </tr>\n      <tr class='total-row'>\n        <td colspan='3'>TOTAL DUE</td>\n        <td>${d.currency} ${fmtMoney(d.billableAmount)}</td>\n      </tr>\n    </tbody>\n  </table>\n  <div class='narrative'>\n    <b>Payment Terms:</b> ${d.paymentTerms ?? 'Payment due within 14 days of invoice date.'}\n  </div>\n  <div style='text-align:center;margin-top:32px;'>\n    <p style='font-size:14px;color:#444;'>To approve this invoice, please click the button below:</p>\n    <a href='${d.approvalUrl}' class='approve-btn'>&#10003; Approve Invoice</a>\n  </div>\n  <div class='footer'>This is a system-generated invoice. Please retain for your records.</div>\n</div>\n</body>\n</html>`;\n\nreturn [{ json: { ...d, invoiceHtml: html } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e8dfd049-a57c-4e0b-b76d-77f3bc51a32c",
      "name": "Gmail \u2013 Send Invoice to Client1",
      "type": "n8n-nodes-base.gmail",
      "position": [
        960,
        1808
      ],
      "parameters": {
        "sendTo": "=CLIENT_EMAIL@example.com",
        "message": "={{ $json.emailBody + '<br><br><hr style=\"border:none;border-top:1px solid #eee;margin:24px 0;\"><p style=\"font-size:12px;color:#888;\">Invoice ID: ' + $json.invoiceId + ' | Due: ' + $json.dueDate + '</p><div style=\"text-align:center;margin-top:20px;\"><a href=\"' + $json.approvalUrl + '\" style=\"display:inline-block;padding:12px 32px;background:#1F4E79;color:#fff;text-decoration:none;border-radius:6px;font-size:14px;font-weight:bold;\">&#10003; Approve Invoice</a></div>' }}",
        "options": {},
        "subject": "={{ $json.subject ?? ('Invoice ' + $json.invoiceId + ' \u2013 ' + ($json['Milestone Name'] ?? '')) }}",
        "operation": "sendAndWait"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "740f35f7-ce0b-45e5-8456-b559e6886b3a",
      "name": "Sheets \u2013 Mark Invoice as Sent1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1536,
        1536
      ],
      "parameters": {
        "columns": {
          "value": {
            "Milestone ID": "={{ $('Code \u2013 Merge GPT + Build Approval URL1').item.json[\"Milestone ID\"] }}",
            "Invoice Status": "Sent"
          },
          "schema": [
            {
              "id": "Milestone ID",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "Milestone ID",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "Invoice Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Invoice Status",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Milestone ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1612767382,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=1612767382",
          "cachedResultName": "Milestones"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit",
          "cachedResultName": "Invoice Tracker"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "60977b41-bb89-4c0c-8bac-2f00111430eb",
      "name": "Sheets \u2013 Log Invoice1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1536,
        1968
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "Invoice ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Invoice ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Milestone ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Milestone ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Contract ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Contract ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Client Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Client Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Invoice Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Invoice Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Amount ($)",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Amount ($)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "PDF Link",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "PDF Link",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Sent To",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Sent To",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Approval Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Approval Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Payment Due Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Payment Due Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1218770209,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=1218770209",
          "cachedResultName": "Invoice Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit",
          "cachedResultName": "Invoice Tracker"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "99f2a568-5b8c-4b62-8d5e-4c27143067a7",
      "name": "Sheets \u2013 Update Approval Status1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1712,
        1792
      ],
      "parameters": {
        "columns": {
          "value": {
            "Approved On": "={{ new Date().toISOString() }}",
            "Milestone ID": "={{ $('Code \u2013 Calculate Billable Amount1').item.json[\"Milestone ID\"] }}"
          },
          "schema": [
            {
              "id": "Milestone ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Milestone ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Contract ID",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Contract ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Milestone Name",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Milestone Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "% Complete",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "% Complete",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Invoice Trigger %",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Invoice Trigger %",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Contract Value ($)",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Contract Value ($)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Billable Amount ($)",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Billable Amount ($)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Invoice Status",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Invoice Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Approved On",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Approved On",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Notes",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Milestone ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1612767382,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=1612767382",
          "cachedResultName": "Milestones"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit",
          "cachedResultName": "Invoice Tracker"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "be84450a-8bcd-4d5e-8837-f4ef4bb0e671",
      "name": "Slack \u2013 Notify Accounts Team1",
      "type": "n8n-nodes-base.slack",
      "position": [
        2016,
        1792
      ],
      "parameters": {
        "text": "\ud83d\udcb0 Invoice Approved \u2013 Action Required",
        "select": "channel",
        "blocksUi": "=[\n  {\n    \"type\": \"header\",\n    \"text\": {\n      \"type\": \"plain_text\",\n      \"text\": \"\ud83d\udcb0 Invoice Approved \u2013 Action Required\"\n    }\n  },\n  {\n    \"type\": \"section\",\n    \"fields\": [\n      { \"type\": \"mrkdwn\", \"text\": \"*Milestone ID:*\\n{{ $json['Milestone ID'] }}\" },\n      { \"type\": \"mrkdwn\", \"text\": \"*Approved On:*\\n{{ $json['Approved On'] }}\" }\n    ]\n  },\n  {\n    \"type\": \"section\",\n    \"text\": {\n      \"type\": \"mrkdwn\",\n      \"text\": \"\u2705 The client has *approved* this invoice. Please log the payment due date in the tracker and issue a receipt once payment is received.\"\n    }\n  },\n  {\n    \"type\": \"divider\"\n  },\n  {\n    \"type\": \"context\",\n    \"elements\": [\n      {\n        \"type\": \"mrkdwn\",\n        \"text\": \"\ud83e\udd16 Sent by Invoice Bridge automation | Update status to *Paid* in Google Sheets when payment clears.\"\n      }\n    ]\n  }\n]",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "accounts-team"
        },
        "messageType": "block",
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.2
    },
    {
      "id": "d2fb22a9-cefe-4c52-be06-c3541c027981",
      "name": "If1",
      "type": "n8n-nodes-base.if",
      "position": [
        1168,
        1808
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "63c53833-aab1-4cda-bed2-642b9de95359",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.data.approved }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "636fe91a-b310-4178-bb3c-744688fbbf34",
  "connections": {
    "If1": {
      "main": [
        [
          {
            "node": "Sheets \u2013 Mark Invoice as Sent1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Sheets \u2013 Log Invoice1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Sheets \u2013 Update Approval Status1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On Workflow Error": {
      "main": [
        [
          {
            "node": "Slack \u2013 Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF \u2013 Skip if None Ready1": {
      "main": [
        [],
        [
          {
            "node": "Code \u2013 Calculate Billable Amount1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets \u2013 Read Milestones1": {
      "main": [
        [
          {
            "node": "Filter \u2013 Ready to Invoice1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter \u2013 Ready to Invoice1": {
      "main": [
        [
          {
            "node": "IF \u2013 Skip if None Ready1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule \u2013 Poll Milestones1": {
      "main": [
        [
          {
            "node": "Sheets \u2013 Read Milestones1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2013 Generate HTML Invoice1": {
      "main": [
        [
          {
            "node": "Gmail \u2013 Send Invoice to Client1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail \u2013 Send Invoice to Client1": {
      "main": [
        [
          {
            "node": "If1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets \u2013 Update Approval Status1": {
      "main": [
        [
          {
            "node": "Slack \u2013 Notify Accounts Team1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2013 Calculate Billable Amount1": {
      "main": [
        [
          {
            "node": "GPT-4o \u2013 Write Invoice Narrative1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4o \u2013 Write Invoice Narrative1": {
      "main": [
        [
          {
            "node": "Code \u2013 Merge GPT + Build Approval URL1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2013 Merge GPT + Build Approval URL1": {
      "main": [
        [
          {
            "node": "Code \u2013 Generate HTML Invoice1",
            "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.

Pro

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

About this workflow

This workflow polls a Google Sheets milestone tracker every 15 minutes, calculates milestone-based invoice amounts, uses OpenAI (gpt-4o-mini) to draft invoice messaging, emails the invoice via Gmail with an approval link, then records approval in Google Sheets and notifies an…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Complete AI-powered sales system Automates lead capture, qualification, and follow-up from multiple channels. AI INTELLIGENCE:

Gmail Trigger, Google Sheets, OpenAI +3
AI & RAG

This workflow accepts construction work permit requests via webhook, logs them to Google Sheets, uses OpenAI (gpt-4o-mini) to detect conflicts against active permits, routes the request to a superviso

Error Trigger, Slack, Google Sheets +2
AI & RAG

Consultants, agencies, freelancers, and professional service firms who need to create customized proposals and contracts quickly and efficiently.

Google Sheets Trigger, OpenAI, Google Docs +5
AI & RAG

This workflow runs every Friday at 5 PM, reads project metrics from three Google Sheets tabs, calculates a Red/Amber/Green health status, uses OpenAI (GPT-4o mini) to generate executive summaries, and

Error Trigger, Slack, Google Sheets +2
AI & RAG

LeadInboxTriageBot_GT. Uses gmailTrigger, openAi, googleSheets, gmail. Event-driven trigger; 36 nodes.

Gmail Trigger, OpenAI, Google Sheets +2