lester noronha


Building a sales pipeline tool with Claude Code / Google Apps


Business users use spreadsheet for everything. It is what the .txt file is for engineers. Meeting notes, tasks, reports, most work starts with the Spreadsheet.

Every week, a dozen of our Sales team country leads would spend two hours through big opportunities, making notes and actions. Everything lived on a Spreadsheet. Not because we lacked tools. We had Salesforce. We had Google Sheets. We had a perfectly functional export button. The problem was the gap between those tools and the actual thing we needed: one page, by market, showing all active accounts, their opportunities, and the latest notes.

That gap got filled by a junior person with good clipboard skills and a lot of patience.


The real problem

Salesforce is built and optimized for individual reps, but not the team trying to run a meeting. So every week, someone would export the data, slice it per country, product and channels, and paste it into a sheet. Move last week's notes into a "previous" column, hunt for what changed, fix copy-paste errors, and rebuild the same view from scratch.

The dirty secret of operations: the tool exists, but the tool is for someone else. You end up paying a human to be a bridge between the tool and the actual job.

I wanted to close that gap.


The Smallest System That Solves the Problem

The first plan by claude code was a webapp. It could've been, but I kept coming back to what's the smallest thing that actually works?

I picked to build on Google Apps Script (GAS). Our team lives in Google Workspace. Salesforce already exports to Google Sheets. People were already adding notes in that sheet. Everyone was already signed in with their Google account. The only missing piece was a decent interface.

GAS is not glamorous or a stack to show technical prowess. But for an internal tool inside a our org, it's quietly perfect. It runs server-side code. It can serve a web page. Authentication is handled. No additional costs, or infrastructure to manage.

The constraint is that you give up a lot of flexibility. The advantage is that you don't need it.


How I build it

It works with 3 layers.

Data layer: Just a Google Sheet with the Salesforce pipeline, auto- updated. Additionally, an append-only comments log, one row per note.

The backend is a server-side script. It filters accounts by market, joins them with their opportunities, and handles reading and writing comments.

The frontend is a single, self-contained HTML file. It draws the sidebar of markets, the account tables, the nested opportunities, the comment panel. It handles all the interactivity. Minimal styling inspired by shadcn

The browser talks to the script through a Google bridge called google.script.run. You call a server function by name. No REST endpoints to design. No tokens to manage. Google hides all that plumbing.


Building in phases

To begin, it was making the loop work. Raw Salesforce data comes in as arrays, then converted to objects: Accounts, Opportunities, and comments. Once that translation existed, the rest was straightforward. Data went from Salesforce into Sheets, through the script, and onto the page without anyone touching it. The UI looked rough. That was fine.

Next was something boring, but necessary. I'd accidentally committed the spreadsheet ID into git. Nothing catastrophic, just not a good habit. Cleaned the repo, set up .gitignore, wrote a README explaining how it all fits together. Internal tools rot fast when you skip this. Nobody remembers why something works six months later.

Last, to make the tool useful. The team asked for something reasonable: a way to track accounts that weren't in Salesforce yet. Early conversations. Prospects being warmed up. Things too small or uncertain to justify a CRM record, but too important to leave in someone's notebook.

So I added a second account type. Custom accounts live in their own sheet. You can create, edit, and delete them — delete being a soft flag, not actual removal. IDs are timestamps. Nothing clever. Everything auditable.

The UI also got a proper overhaul: a sidebar for markets, accounts grouped by type (distributors, direct, utilities), expandable rows showing nested opportunities on click, a comment panel with full history. It stopped looking like a hack and started feeling like something someone built on purpose.


Performance: Making It Fast Enough

This isn't a consumer product. But nonetheless, slow internal tools shouldn't exist in this age. People may not complain but they just go back to the spreadsheet.

Early versions fetched fresh data from the server on every filter change. Each fetch was 3 to 5 seconds of waiting. That's too long when you're mid-meeting and someone asks to switch markets.

The fix: load one market at a time from the server. Everything else such as filtering by account type, filtering by owner runs in the browser on data that's already queried. Fewer server calls instantly make the tool feel snappy. The tool feels fast. The last selected market is saved to localStorage. Each user is interested in their own market. So when you reload, you drop straight back into the same view, and the sidebar renders before the data even arrives.

Tiny fixes, but big difference.


What Actually Changed


The Decisions Worth Repeating

Skip the framework. No React, no build steps, no dependencies. Vanilla JavaScript in a single HTML file. For something this size, it's not a compromise but the right call.

Not all filters are equal. Market selection happens server-side, because it controls how much data you send. Everything else filters client-side, on data that's already loaded. That split is what makes it feel fast without loading the server.

The lesson isn't really about Google Apps Script. It's about resisting the pull toward the more impressive-sounding solution. Often the answer is less satisfying to describe than to use. That's usually a good sign.