SMB Digital Way – Digital Solutions for Your Business - The spreadsheet was the right answer until it wasn't
Rebuilding a volunteer career guidance platform from Google Apps Script to Frappe A community led career navigation programme gives free career guidance to school students.
Version one: Google Sheets and about a thousand lines of Apps Script
The first build used nothing but Google Workspace. Forms for registration and the two assessments, Sheets as the datastore, and Apps Script to hold it together.
The flow worked like this. A student submits the registration form. A trigger fires, builds two assessment URLs with the student's name, class and email already encoded as query parameters, and sends a welcome email containing both links. The student completes the Multiple Intelligences inventory and the RIASEC interest inventory, in either order, on whatever schedule suits them.
When both submissions land, another trigger notices the match, sums the item scores into eight intelligence totals and six interest totals, sorts them, derives a three letter Holland code, generates an HTML report, converts it to PDF through a temporary Drive file, emails the PDF to the student, and alerts the coordinator that a navigator is needed. The coordinator picks one. A third trigger sends that navigator a full briefing with the student's profile attached, and simultaneously tells the student who they will be working with.
Some of it was carefully built. Emails were deduplicated through a log tab and a LockService wrapper that wrote the log entry inside the lock and sent the email outside it, so a slow Gmail call could never hold the lock open or cause a double send. Every identifier written to the execution log was masked, so a developer reading logs saw aar***@g***.com rather than a student's actual address. Time based sweeps ran every thirty minutes as a backup for anything a form submit trigger dropped.
It ran. It served [X] students. Infrastructure cost was zero, which for a programme that charges nothing is not a minor detail.
For that stage, it was the right call. I would build it the same way again.
Where it broke
Systems built on spreadsheets do not degrade gracefully. They work, and then they hit a wall, and the wall is usually not where you expected.
Gmail's send quota. Every completed student triggers three or four emails, several carrying PDF attachments. Apps Script sends run against a fixed daily quota that you cannot buy your way past from inside the platform. This is not a performance problem you optimise. It is a ceiling.
The execution time limit. Apps Script kills a function after six minutes on consumer accounts, thirty on Workspace. The completion check loops every row in the MI sheet and, for each one, runs a linear search through the RIASEC sheet looking for a matching email. That is quadratic. Layer on a Drive round trip per report, which means creating a temporary HTML file, converting it, and trashing it, and execution time grows faster than the student count. Reports started timing out.
Column indices as schema. The register tab's columns were mapped in code as constants: student ID is 0, name is 1, assignment status is 17. Insert one column anywhere to the left of 17 and every downstream function silently reads the wrong field. No exception, no alert. Just a navigator briefing that goes to a coordinator's phone number. Silent failure is worse than a crash, because you only find it in production and usually after someone has been emailed.
Hand rolled concurrency. The lock and log pattern was, in retrospect, a careful workaround for something a database gives you free. What we were building was a unique constraint on an email log. It also papered over a design issue: two triggers fire on the same form submit event, and a sweep runs every thirty minutes, so the same function can execute three times for one submission. That was why the deduplication had to be so defensive in the first place.
Configuration in source code. Navigator email addresses lived in a JavaScript object literal. Adding a volunteer meant editing the script and redeploying it. A non technical coordinator could not onboard a mentor without a developer.
No permission model. This is the one that ended the argument. The spreadsheet was simultaneously the database, the admin interface and the access control layer. Anyone with edit access to it could see every student's name, email, class, school, parent's name and phone number, and full psychometric profile.
We hold data on minors. India's Digital Personal Data Protection Act treats children's personal data as a distinct category with specific obligations around consent and processing. Under that framing, "everyone with the sheet link sees everything" is not technical debt to schedule. It is a governance failure to fix.
There was a quieter seventh problem too. We could not answer operational questions. How many Class 9 students in Bengaluru are waiting on a navigator? What is our median time from completion to first session? Which navigators are at capacity? Every one of those required manual filtering, which meant nobody asked them, which meant we were flying on instinct.
What we actually needed
| The constraint | The requirement it implied |
|---|---|
| Send quota as a hard ceiling | A real mail queue with retries and delivery logging |
| Six minute execution limit | Background jobs that run outside a request cycle |
| Column position as schema | Named fields, typed, with validation |
| Locks to prevent double sends | Database level uniqueness, handled atomically |
| Navigators hardcoded in source | Configuration that a coordinator edits, not a developer |
| Everyone sees all student PII | Role based permissions, down to the field |
| No answer to operational questions | Queryable data and a reporting layer |
| Email as the only interface | Portals for students and navigators |
Written out like that, the answer stops being "a better script." It is an application.
Why Frappe
We rebuilt on Frappe, the open source low code framework underneath ERPNext.
The fit was close enough to feel almost unfair. In Frappe you define a DocType, and from that single definition you get a database table, a form UI, a REST API, and a permission model. The things we had spent months hand building were framework primitives:
- The workflow engine replaced "type Assigned into column R." States, transitions, and who is allowed to move between them, defined declaratively.
- Role and field level permissions replaced everyone seeing everything. A navigator sees the students assigned to them, and only the fields relevant to a briefing.
- Background jobs replaced the execution ceiling. Report generation runs in a worker, not in a trigger racing a stopwatch.
- The built in email queue replaced our locking logic, with retries and a delivery record we did not have to design.
- Jinja print formats replaced the temporary Drive file PDF hack, with real control over layout and pagination.
- Document versioning replaced a log tab. Every change to every record, with author and timestamp, automatically.
- Portal pages gave students and navigators somewhere to log in, so a deleted email no longer means a lost report.
- The report builder answered the operational questions in a query rather than an afternoon.
And it is open source and self-hosted. Student data stays on infrastructure we control, which matters for the DPDP question, and there is no per seat licence on a programme that is free for the people using it.
It is worth being honest about the cost. You now run a server. Backups, updates and monitoring become your problem rather than Google's. Frappe is opinionated, and working with the grain of its conventions takes some learning. That is a real trade, and for a programme at fifty students it would have been a bad one.
Prototyping in days rather than sprints
The build ran as a tight loop: model something, generate it, put it in front of a counsellor, watch them react, change it, rebuild.
This mattered more than it sounds, because the domain resists specification. Ask a career counsellor in the abstract what belongs in a student briefing and you get a reasonable sounding list. Show them an actual briefing for an actual student and they will tell you within thirty seconds that the intelligence scores are less useful than the gap between a student's top interest and their top intelligence, and that they want to see the raw item responses for anything scoring near a band boundary.
You cannot get that from a requirements conversation. You get it from putting something concrete in front of someone who knows what they are looking at. The faster the loop, the more of those corrections you collect before launch rather than after.
What AI did, and what it did not
We used AI heavily. That phrase has been worn smooth by overuse, so here is the specific version.
Where it did the work:
- Generated the DocType schemas from a plain English description of the domain
- Translated several hundred lines of Apps Script scoring logic into Python controllers
- Built the Jinja print format for the report PDF
- Wrote the migration script that moved existing records out of Sheets
- Drafted the descriptive content bank: the explanatory text for eight intelligence types and six interest categories, plus band descriptions
- Generated realistic test data, which is tedious enough by hand that it usually gets skipped
Weeks of work, compressed into days. The prototyping loop above only worked because rebuilding after each counsellor conversation was cheap.
Where it did nothing useful:
- Deciding what a navigator actually needs to know before meeting a student
- Deciding what a thirteen-year-old should read about their own results, and what should be held back for a conversation with a human
- Judging which steps needed a person in the loop and which were safe to automate end to end
- Deciding whether these instruments are appropriate for this age group at all
There is one specific failure mode worth naming, because it cost us real editing time. The generated descriptive content drifted consistently toward fixed trait, flattering language: you are a natural leader, you are a visual thinker. That is exactly wrong for an adolescent. Assessment results describe current preference and self perception, not permanent capacity, and the difference is not cosmetic. A student who reads "you are not a maths person" at fourteen can carry that for a decade.
Every line of that content had to be rewritten toward developmental framing. Our report now says in as many words that it is a compass, not a destination. No model produced that instinct. It came from counsellors who have watched what happens when a report is written carelessly.
That is the honest split. AI compressed the build loop enormously. It compressed the thinking loop not at all.
Before and after
| Area | Version one | Version two | ||
|---|---|---|---|---|
| Datastore | Google Sheets | Relational, typed, validated | ||
| Access control | Whoever has the sheet link | Role and field level permissions | ||
| Assignment | Type a status into a column | Workflow with states and transitions | ||
| Report generation | Blocking, inside a timed trigger | Background job | ||
| Deduplication | Hand rolled lock plus a log tab | Database constraint plus mail queue | ||
| Adding a navigator | Edit source, redeploy | Coordinator adds a record | ||
| Audit trail | A log tab, by convention | Automatic, on every record | ||
| Student interface | Email only | Portal plus email | ||
| Reporting | Manual filtering | Query and dashboard | ||
| Infrastructure cost | Zero | Small VPS |
The pattern is not about careers
Here is why this write up exists rather than staying in an internal wiki.
Strip the career guidance vocabulary out and what remains is a shape that a lot of organisations have:
Structured intake → automated scoring → a personalised document → routing to the right specialist → both parties briefed → with an audit trail.
Swap the question bank and that is a financial advisor's risk profiling, where the audit trail is a regulatory requirement rather than good hygiene. It is a clinic's patient intake. It is a skilling NGO's beneficiary assessment, where the funder demands per person documentation. It is a college placement cell, a mentorship programme, a study abroad consultancy's profile evaluation, a recruitment agency's candidate screening.
Every one of those has the same seven manual steps and, very often, the same spreadsheet holding them together.
What I would do differently
Nothing about version one. Building it on Sheets got the programme running in weeks with no budget, and gave us real students and real counsellors to learn from. A properly architected system built first, in the abstract, would have been wrong in ways we could not have predicted.
Move on permissions before scale forces it. Everything else on the failure list was an inconvenience. The access control gap was the only one with a person on the other end of it, and it should not have taken a capacity ceiling to trigger the rebuild.
Separate content from code from day one. Report text lived inside source files, which meant a counsellor could not fix a badly worded sentence without a developer.
Write down the questions you cannot answer. The operational questions we kept failing to answer were the clearest signal that the data model was wrong, and we treated them as an annoyance rather than as evidence for months.
If your organisation has this shape and you are still running it on spreadsheets and manual email, the tooling to do it properly is cheaper and faster than it was even a year ago. Most people doing this work have not noticed yet.
Happy to compare notes.
No comments yet. Login to start a new discussion Start a new discussion