Lead CRM
Status dropdowns with an enforced pipeline, automatic timestamps, duplicate detection (email, phone, name + company) and business-day follow-up reminders.
Google Sheets + Apps Script automation kit
A lead pipeline with reminders, one-click invoice PDFs with Gmail drafts, weekly KPIs and form routing, all inside the Google Sheet the team already uses. The business logic is in typed, tested TypeScript.
Four workflows that come up in most small businesses. Each one can be used on its own.
Status dropdowns with an enforced pipeline, automatic timestamps, duplicate detection (email, phone, name + company) and business-day follow-up reminders.
Groups orders per customer, numbers invoices (INV-YYYY-NNNN), handles discounts and tax, renders a PDF from a Docs template and creates a Gmail draft.
Live formulas (not pasted values) for new, won and lost leads, invoiced revenue and win rate, plus a chart that refreshes every Monday.
Google Form answers are routed to the right tab by category and cleaned up; sales enquiries become leads automatically.
Coloured status chips, overdue follow-ups in red and flagged duplicates. Everything in the menu is plain TypeScript compiled to Apps Script.
| AName | BCompany | CEmail | DSource | EStatus | FLast Contact | GNext Follow-up | HDuplicate Of | |
|---|---|---|---|---|---|---|---|---|
| 2 | Jon Berg | Pine Street Bakery | jon.berg@pinestreetbakery.example | Walk-in | Lost | 2026-11-04 | ||
| 3 | Quinn Tanaka | Oak & Iron Gym | quinn.tanaka@oakirongym.example | Qualified | 2026-11-04 | 2026-11-06 | ||
| 4 | Frank Reyes | Pine Street Bakery | frank.reyes@pinestreetbakery.example | Qualified | 2026-11-06 | 2026-11-10 | ||
| 5 | Frank Moreau | Cedar Books | frank.moreau@cedarbooks.example | Referral | Proposal | 2026-11-01 | 2026-11-05 | |
| 6 | Marco Tanaka | Cedar Books | marco.tanaka@cedarbooks.example | Website | Proposal | 2026-11-04 | 2026-11-10 | |
| 7 | Omar Patel | Riverside HOA | omar.patel@riversidehoa.example | Website | Qualified | 2026-11-07 | 2026-11-10 | |
| 8 | Omar Tanaka | Summit Realty | omar.tanaka@summitrealty.example | Website | New | 2026-11-05 | 2026-11-06 | |
| 9 | Grace Berg | Summit Realty | grace.berg@summitrealty.example | Website | New | 2026-11-06 | 2026-11-09 | |
| 10 | Marco Costa | Maple Cafe | marco.costa@maplecafe.example | Google Ads | Contacted | 2026-11-11 | 2026-11-16 | |
| 11 | Frank Patel | Oak & Iron Gym | frank.patel@oakirongym.example | Referral | Contacted | 2026-11-11 | 2026-11-16 | |
| 12 | Omar Patel | Riverside HOA | omar.patel+web@riversidehoa.example | Form | New | 2026-11-11 | 2026-11-12 | Row 35 (email) |
| 13 | Jon Walsh | Birch Dental | - | New | 2026-11-11 | 2026-11-12 | Row 29 (phone) |
Invalid moves (for example New to Won) are reverted by onEdit, with a toast that explains why. Follow-ups skip weekends and holidays.
| Status | Can move to | Follow-up after (business days) |
|---|---|---|
| New | Contacted Lost | 1 |
| Contacted | Qualified Lost | 3 |
| Qualified | Proposal Lost | 2 |
| Proposal | Won Lost Qualified | 4 |
| Won | final | - |
| Lost | Contacted | - |
"Invoices > Generate from Orders" groups un-invoiced rows by customer, numbers them, renders the PDF and writes the invoice number back to each order. Emails are created as drafts, so nothing is sent without a human check.
| AOrder ID | BDate | CCustomer | DItem | EQty | FUnit Price | GDiscount | HTaxable | IInvoice # | |
|---|---|---|---|---|---|---|---|---|---|
| 2 | ORD-101 | 2026-10-30 | Maple Cafe | Weekly lawn mowing (4 visits) | 1 | $180.00 | Yes | INV-2026-0041 | |
| 3 | ORD-102 | 2026-10-30 | Maple Cafe | Hedge trimming | 1 | $95.00 | Yes | INV-2026-0041 | |
| 4 | ORD-103 | 2026-11-02 | Birch Dental | Spring cleanup package | 1 | $320.00 | Yes | INV-2026-0042 | |
| 5 | ORD-104 | 2026-11-02 | Birch Dental | Mulch, premium (cubic yard) | 6 | $48.50 | Yes | INV-2026-0042 | |
| 6 | ORD-105 | 2026-11-03 | Birch Dental | Tree pruning (per tree) | 2 | $135.00 | Yes | INV-2026-0042 | |
| 7 | ORD-106 | 2026-11-04 | Riverside HOA | Weekly lawn mowing (4 visits) | 3 | $180.00 | 10% | Yes | INV-2026-0043 |
| 8 | ORD-107 | 2026-11-05 | Riverside HOA | Fall leaf removal | 2 | $240.00 | 10% | Yes | INV-2026-0043 |
| 9 | ORD-108 | 2026-11-05 | Riverside HOA | Irrigation repair, labor (hours) | 3.5 | $72.00 | No | INV-2026-0043 | |
| 10 | ORD-109 | 2026-11-07 | Summit Realty | Hedge trimming | 2 | $95.00 | Yes | INV-2026-0044 | |
| 11 | ORD-110 | 2026-11-07 | Summit Realty | Mulch, premium (cubic yard) | 4 | $48.50 | Yes | INV-2026-0044 | |
| 12 | ORD-111 | 2026-11-09 | Harbor Yoga | Fall leaf removal | 1 | $240.00 | Yes | INV-2026-0045 |
| Description | Qty | Unit | Disc. | Amount |
|---|---|---|---|---|
| Weekly lawn mowing (4 visits) | 3 | $180.00 | 10% | $486.00 |
| Fall leaf removal | 2 | $240.00 | 10% | $432.00 |
| Irrigation repair, labor (hours) * | 3.5 | $72.00 | - | $252.00 |
* Labor is not taxable. Line amounts are rounded per line; tax is computed once on the taxable subtotal (half-up), so the document always adds up.
The KPI tab holds formulas such as =COUNTIFS(Leads!F:F,"Won",Leads!I:I,">="&A2, ...), so it stays correct when someone edits data by hand. A time-driven trigger rebuilds the chart every Monday.
| AWeek of | BNew | CWon | DLost | EInvoiced | FWin rate | |
|---|---|---|---|---|---|---|
| 2 | 2026-09-21 | 6 | 1 | 0 | $0.00 | 100% |
| 3 | 2026-09-28 | 4 | 3 | 1 | $412.80 | 75% |
| 4 | 2026-10-05 | 5 | 1 | 3 | $0.00 | 25% |
| 5 | 2026-10-12 | 4 | 1 | 2 | $259.80 | 33.3% |
| 6 | 2026-10-19 | 6 | 3 | 0 | $0.00 | 100% |
| 7 | 2026-10-26 | 6 | 1 | 1 | $0.00 | 50% |
| 8 | 2026-11-02 | 4 | 0 | 1 | $297.69 | 0% |
| 9 | 2026-11-09 | 3 | 0 | 0 | $2,874.90 | 0% |
Pipeline today: New 4 · Contacted 2 · Qualified 6 · Proposal 10 · Won 10 · Lost 8
An installable onFormSubmit trigger sends each response to a tab by the answer to "What do you need help with?". Answers are trimmed, capped, and protected against formula injection.
| Category answer | Destination tab |
|---|---|
| New project, Quote request, Estimate | Intake - Sales |
| Existing project, Support, Warranty | Intake - Support |
| Billing, Invoice question, Payment | Intake - Billing |
| anything else | Intake - Other |
Business rules never touch Google globals (a lint rule enforces this), so they run and are unit-tested in Node. Thin adapters do the Sheets, Docs, Drive and Gmail calls.
src/**/*.tstsc --noEmitesbuild (IIFE + global wrappers)dist/Code.js + appsscript.jsonclasp push| Trigger | Runs | Does |
|---|---|---|
onOpen (simple) | When the sheet opens | Adds the SheetFlow menu |
onEdit (simple) | On every edit | Pipeline rules and timestamps on the Leads tab |
dailyFollowUpDigest | Daily, 8am | Updates follow-up dates and drafts a digest email |
refreshKpis | Mondays, 7am | Rebuilds KPI formulas and the chart |
onFormSubmitRoute | On form submit | Routes answers and creates leads |
Scopes are kept narrow: spreadsheets.currentonly, documents, drive.file (only files SheetFlow creates), gmail.compose (drafts, not send), script.scriptapp and script.container.ui.
We map your current sheet, statuses, tax rules, invoice look and who needs reminders.
Columns, dropdowns and pipeline rules live in one schema file, so I change them there and the tests confirm nothing else broke.
I push the script to a copy of your sheet with clasp. You approve the permissions; I never need your password.
Logo, fonts and payment details go in the Docs template. We generate a test batch together and check every number.
A short video walkthrough, a README for your team, and triggers switched on. Typical turnaround: 3 to 5 days.
// src/core/invoice.ts: pure, tested, no Google APIs
export function computeTotals(lines: InvoiceLine[], taxRatePct: number): InvoiceTotals {
let subtotal = 0, net = 0, taxableBase = 0;
for (const line of lines) {
const { gross, amount } = lineAmount(line.quantity, line.unitPrice, line.discountPct);
subtotal += gross;
net += amount;
if (line.taxable) taxableBase += amount;
}
const tax = roundHalfUp((taxableBase * taxRatePct) / 100);
return { subtotal, discount: subtotal - net, taxableBase, tax, total: net + tax };
}