At a K-12 online learning provider, 2025 to now
Running a team on a spreadsheet-database without losing data
My team runs its daily work on a shared, spreadsheet-like database. These are the safety rules I use so that changing it never loses or breaks anyone's data, and so the emails it sends to schools are right.
- Role
- Built the automations and reports; I look after the database
- Time
- September 2026 to now
- Tools
- Airtable
- Outcome
- Each batch is written as one action that can be undone, with its before-and-after counts on record
Problem
I work at a K-12 online learning provider: schools in the US and abroad partner with us so their students can take courses online. My team runs tutoring and grading, and keeps its records in Airtable, a database that looks and feels like a spreadsheet. It holds tutoring requests, the shared calendar, staff details, which grading work each person has taken on, a library of answer keys and rubrics, and a copy of the platform’s course lists. Automations on top of it send emails, from alerts for staff to session reports for partner schools. I built the automations and reports described here, and I look after the database.
Constraints
- There is no test copy. The database is the real one, and colleagues are working in it while I change it.
- An automation that emails a school cannot be tested by running it, because a test email is a real email.
- Tables link to each other, so deleting a row in one place can quietly break a link somewhere else.
What I built
Five rules I follow for every change:
- Look before writing. Count the rows, check the new rows are not already there under a similar name, and save a copy of anything I am about to change.
- Small, labelled batches, each approved by a person who has seen exactly what will be written.
- Check afterwards. Read each new row back; the new row count must equal the old count plus the batch.
- Never delete. When something disappears from the source, its row gets a “last seen” date and stays, so no link breaks.
- Prove the source data is complete before marking anything as gone. An incomplete download changes nothing.
Automation code gets a stricter check. I make the edit offline first and take its fingerprint (a hash: a short code that changes if even one character changes). In the editor, the same edit is applied to the live code, and it is saved only if the result has the same fingerprint, so a typo cannot slip through.
Reports to schools went to me first, marked as a test, before any reached a school. If reading the data fails or comes back empty, I get an alert and the school gets nothing. Tutors’ notes are left out for group sessions. I test on saved data where names and email addresses are replaced by fingerprints, and notes by their length.
The diagram in words
- Source, Saved pull, then Pull proven complete?
- Dashed path from Pull proven complete?: "write nothing", then Alert, then Me.
- Solid path from Pull proven complete?: Proposed batch, Approver, Write, Count after equals before plus batch?, then Live base.
- Dashed path from Count after equals before plus batch?: Revert, then back to Live base.
- Live base to Linked bases: "linked counts after every batch".
- Dashed path from Pull proven complete? into Live base: "gone from source: last seen date, never delete".
How I proved it worked
- Predicted before building. Before adding a column that labels which language a course is taught in, I worked out every row’s answer offline, then compared row by row once the column was live. Every row matched. A catch-all value turns anything unexpected into a visible count instead of a blank.
- A tiny character, caught. A new tutor’s email address had an invisible tab character at the start, so their work shifts would never have matched them. It was fixed the same day, and a checklist script now tests every new tutor’s record before they can be booked.
- Looking past the obvious. When the guide linked from a staff alert moved to a new address, I checked every automation and formula for an old link. None was left, but a field sent reviewers to a section of the guide by its number, which a renumbering would have silently broken. The fix: new versions now replace the guide in place, so its address never changes.
- Closing gaps early. Building a second school report from the first showed two ways a student from another school could end up in a report: a tutor’s email typed in the student field, or a request naming a different school. Neither had happened. I closed both in the live report too, and that week’s real data listed the same students before and after.
Outcome
- Each batch is written as one action that can be undone, with its before-and-after counts on record.
- Partner schools receive daily and weekly session reports; the newer ones ran as tests to me before any reached a school.
What I would do next
- Keep all automation code in version control, with the same before-and-after fingerprints.
- Run the checklist script and the cross-table counts on a schedule that sends alerts.
The demo it pairs with
Safe-write sandbox (new). Stage a batch of changes on a made-up table, approve it, and watch the checks and the read-back. Try to delete a row and see it flagged instead.
A made-up table of tutors, a table of bookings that links to it, and a source to copy new rows from. Pick rows to put in a batch. Nothing is written until you approve it; then every written row is read back and the count is checked. Deleting is never allowed: try it.
Staging, approving and undoing need JavaScript. Without it, the sample batch reads below, staged but not written. A delete never runs: it is flagged, and the row gets a last seen date instead.
The table
| Name | Teaches | Sign-in | Status |
|---|---|---|---|
| Fenn | World history | fenn10 | In the source |
| Ada | French | ada79 | In the source |
| Bram | World history | bram31 | In the source |
| Dara | French | dara81 | In the source |
| Hollis | Geometry | hollis59 | In the source |
| Cleo | Spanish | cleo52 | In the source |
Bookings that link to it
- Tuesday 5 pmwith Hollis
- Thursday 4 pmwith Cleo
- Wednesday 3 pmwith Fenn
- Friday 2 pmwith Hollis
The source
The full download says it holds 9 rows, and 9 arrived. 4 of them are not in the table as written:
"Esme"Physicstabesme10"Kit"Geometrykit71"Noor"Frenchnoor83"bram "World historybram31
Mark rows gone that a download no longer holds
Batch 1
Staged, nothing written- New row: Kit, Geometry, sign-in kit71
- New row: Noor, French, sign-in noor83
- Last seen 12 May: Fenn. Asked to delete. Kept, with a last seen date.
Held or flagged
- Held:
"Esme". Sign-in"tabesme10". The sign-in name starts with an invisible tab character, shown here in words. It would never match the same sign-in name typed by hand. - Held:
"bram ". Already in the table as Bram. Only case and spaces differ. - Delete Fenn. Not deleted. The booking on Wednesday 3 pm links to this row and would break. Staged as last seen 12 May instead.
- Incomplete download. The download says it holds 9 rows, and 7 arrived. Nothing is marked gone until the source is complete. Had it run, it would have marked 2 rows gone, 1 of them still in the source.
The checks, one per rule
- Look. Flagged. Counted 6 rows. Checked 4 new rows against the table. Saved a copy of the 1 row that will change. Held 2 rows back.
- Never delete. Flagged. 1 delete asked for. No row was removed.
- Source complete. Flagged. The download is incomplete, so nothing is marked gone.
- Stage. Passed. Batch 1: 2 new rows, 1 change. Nothing written yet.
- Approve. Waiting. Waits for a person who has seen every change.
- Write. Waiting. Nothing written yet.
- Read back. Waiting. Each written row will be read back.
- Count. Waiting. The count before plus the new rows must equal the count after.
On record
No batch written yet.