Building a CRM on Google Sheets
I needed a CRM. Not a SaaS product with a learning curve and a monthly bill. Not a database that only I could read. I needed something that a nonprofit committee of five people could open, understand, and edit without calling me first. I needed a spreadsheet.
But I also needed that spreadsheet to be smart. I needed AI to pull dashboards, search contacts, log interactions, and flag overdue follow-ups. I needed the simplest possible human interface on one side, and a fully capable AI tool layer on the other.
So I built a CRM on Google Sheets. And it works.
Why a Spreadsheet
The project was a nonprofit sponsorship pipeline. Corporate sponsors, tiered packages, contact info, follow-up cadences, activity logs. Classic CRM stuff. But the committee managing it was a mix of volunteers and staff who already lived in Google Workspace. They already knew how to sort, filter, and edit a spreadsheet. That mattered more than any feature list.
The best database is the one people actually use. I've watched organizations buy Salesforce and let it rot. I've seen HubSpot instances with 200 contacts and zero logged activities. The tool doesn't fail because it lacks features. It fails because nobody opens it.
Google Sheets gave me three things no traditional CRM could match for this use case:
- Zero onboarding. Everyone already knows how to use a spreadsheet. No training sessions, no documentation, no support tickets.
- Real-time collaboration. Multiple people editing at once, seeing each other's changes, leaving comments. Built in. Free.
- Transparency. Sponsors and committee members can see the exact same data. No "let me pull that report for you" bottleneck.
The tradeoff is that spreadsheets aren't databases. They don't enforce data types. They don't have referential integrity. They don't scale to a million rows. But this pipeline would never have a million rows. It would have a few hundred contacts and a few thousand activity entries. That's a spreadsheet's sweet spot.
The Data Model
Four tabs. That's the entire schema.
Contacts is the main pipeline. Each row is a prospect or sponsor. Columns cover company name, contact person, email, phone, tier, status (prospect, contacted, negotiating, committed, declined), assigned committee member, and notes. Standard stuff, deliberately flat.
Tier Reference defines the sponsorship levels. Package name, price, what's included, how many are available, how many are sold. The availability column auto-calculates so nobody accidentally oversells a tier.
Activity Log is append-only. Every interaction gets a row: timestamp, contact, activity type (call, email, meeting, text), who did it, and notes. Append-only is a deliberate choice. You don't edit history. You don't delete a record of a conversation. You add to the log and move forward. This prevents the most common CRM data-loss pattern: someone "cleaning up" records and accidentally erasing context.
Dashboard is auto-calculated from the other three tabs. Total pipeline value, committed revenue, contacts by status, overdue follow-ups, tier availability. It's the tab you open first thing Monday morning to see where things stand.
The Architecture
Here's where it gets interesting. The spreadsheet is the data layer. But between the spreadsheet and AI, there are two more layers.
Google Sheets (Data)
|
v
Apps Script (Middleware / REST API)
|
v
Node.js MCP Server (AI Interface)
|
v
Claude (AI Agent)
Each layer has a single job. The spreadsheet stores data. Apps Script exposes it. The MCP server translates it. Claude uses it.
The Apps Script Layer
Google Apps Script is JavaScript that runs inside Google Workspace. You write functions, deploy them as a web app, and they become HTTP endpoints. It's middleware, basically.
There's one significant limitation: Apps Script web apps only accept GET requests. No POST, no PUT, no DELETE. Just GET. So every operation, including writes, goes through URL-encoded query parameters on a GET request. It's unconventional, but it works. You encode your payload in the URL, Apps Script parses it, performs the operation, and returns JSON.
The Apps Script layer handles data validation, formatting, and the translation between "spreadsheet rows" and "structured JSON." It checks for duplicate contacts before adding new ones. It enforces required fields. It auto-timestamps activity log entries. All the things a spreadsheet can't do on its own.
The MCP Server
The Node.js MCP server is the bridge between Claude and the REST API. MCP (Model Context Protocol) lets you define tools that an AI model can call. Each tool maps to a specific operation.
The server exposes nine tools:
dashboard— Pull current pipeline metricssearch— Find contacts by name, company, or statuslist_contacts— Return all contacts, optionally filteredget_contact— Get full detail on a single contactoverdue— List contacts past their follow-up datetiers— Show sponsorship levels and availabilityadd_contact— Create a new contact (with duplicate checking)update_contact— Modify an existing contact's fieldslog_activity— Append an interaction to the activity log
Each tool takes structured parameters, makes an HTTP request to the Apps Script endpoint, and returns the result. The MCP server doesn't contain business logic. It's a translator. All the rules live in the Apps Script layer, which keeps the stack easy to reason about.
Key Design Decisions
A few choices that made this work better than expected.
Append-only activity log. I mentioned this already, but it's worth emphasizing. The single most valuable property of this CRM is that interaction history is immutable. Nobody can rewrite what happened. Every call, email, and meeting is timestamped and permanent. When a committee member says "I talked to them last week," you can verify it. When handoffs happen, the full history travels with the contact.
Auto-calculated tier availability. Overbooking a sponsorship tier is a real problem. If you have five slots at a given level and you accidentally commit six, someone has to have an awkward conversation. The tier reference tab calculates remaining availability in real time from the contacts tab. The Apps Script layer checks availability before allowing a tier assignment. The problem simply can't happen.
Duplicate checking. Before adding a new contact, the system checks for existing entries by company name and contact email. Fuzzy matching catches common variations. This prevents the classic CRM problem of the same company appearing three times with slightly different names.
Status-driven follow-up cadence. Each status has an expected follow-up interval. Prospects get flagged after seven days of silence. Negotiating contacts get flagged after three. The overdue tool surfaces these automatically, so the Monday morning check-in is always actionable.
What I Learned
The biggest insight from this project is that AI doesn't need to replace human tools — it can wrap them. The spreadsheet stays familiar. Committee members open it, see rows and columns, and do their thing. They don't know or care that an AI agent is also reading and writing to the same sheet. The human interface and the AI interface coexist on the same data without conflict.
This pattern — human-readable data layer, middleware API, AI tool interface — is more broadly useful than I expected. It works for any situation where the data already lives somewhere people are comfortable. Inventory in a spreadsheet. Project plans in a doc. Contacts in an address book. You don't need to migrate to a "proper" database. You need to build an API on top of what already works.
The Apps Script GET-only limitation was annoying at first, but it forced a clean separation between the transport layer and the logic layer. Every operation is stateless. Every request is self-contained. The constraint became an advantage.
I also learned that the append-only log pattern should be the default for any system where accountability matters. Editable history is a liability. Immutable history is an asset. If you're building any kind of CRM or project tracker, make the activity log append-only from day one.
The whole thing took about a week to build. The spreadsheet existed already. The Apps Script layer was two days of work. The MCP server was another day. Testing and refinement filled the rest. A week of work for a CRM that a nonprofit committee actually uses every day. That's the kind of leverage AI tooling is supposed to provide.
You don't need Salesforce. You need a spreadsheet, a middleware layer, and an AI interface. The future of CRM isn't a better app. It's intelligence wrapped around the tools people already trust.