This autumn I used Tape to build a booking system for student - mentor meetings at my kidsâ school. On the surface it is âjustâ a calendar with names and time slots. Under the hood it turned into a fun little Tape architecture exercise with JSON, calculation fields, automations and some unexpected platform constraints.
This article walks through the structure of the system, the legacy Google Sheet we replaced, some technical implementation details, and a couple of dead ends that we hit along the way.
The old system: a shared Google Sheet
Before Tape, everything ran via a single Google spreadsheet that parents and staff all edited by hand:
The sheet had:
- one big grid for all days and mentors
- time slots as rows
- columns per mentor
- parents typing their learnerâs name directly into the cell they wanted
It worked, but it had all the classic shared spreadsheet problems:
- no real constraints - two parents could type into the same slot at roughly the same time
- easy to accidentally overwrite or move names
- no automatic way to see which learners were already booked on another day
- no easy export or derived views (for example, per mentor summary, per learner overview, room based lists)
The goal of the Tape project was to keep the simplicity of that sheet for parents, while giving staff a proper data model, automations and exports behind the scenes.
The problem we wanted to solve
The school runs 360 style mentor conversations over three days. Constraints:
- fixed dates - for example 15th, 16th and 17th of December
- multiple mentors, each in a specific room
- parents should pick a free slot under the right mentor
- once a learner is booked with a mentor on one day, they should disappear from the âpossible learnersâ list on other days
- staff wanted to get away from the fragile shared spreadsheet and have one reliable, queryable source of truth in Tape
So the target architecture was:
- Tape as backend (objects, records, automations, exports)
- a simple booking page that feels like picking a cell in a spreadsheet, but actually does proper validation and record creation in the background
Data model in Tape
I ended up with three main record types.
1. Availability
One record per mentor per day. The âcanonicalâ representation of that record is a calculation field that outputs JSON as a string, for example:
{
"day": "2025-12-15T00:00:00.000Z",
"mentor": "Suzanne",
"room": "Barceloneta",
"possibleLearners": [
"Lea", "Pau M", "Renee", "Kedem", "Scarlett",
"Adele L", "Keanu", "Val", "Carmen", "Mia"
],
"availableTimeSlots": [
"14:00",
"14:30",
"15:00"
]
}
The JSON is not edited by hand. The calculation field aggregates the raw fields (day, mentor, room, possible learners, time slots) into one JSON structure via JavaScript and returns JSON.stringify(...). Other calculation fields and automations then JSON.parse that string to transform or display it.
2. Bookings
One record per confirmed booking:
daymentorlearnertime- backlink to the related availability record
These are created by Tape when a parent clicks a booking link.
3. Potential Bookings
This is the slightly crazy one: for every possible combination of
- day
- mentor
- time slot
- learner
there is a Potential Booking record.
Each of these records has:
- the parameters (day, mentor, time, learner, etc) saved in fields
- a Tape weblink in a JSON object in a text field that runs the booking logic for exactly that combination
In our real setup there are 3 days, 15 mentors, up to 8 slots per mentor per day and 175 learners.
Each learner is preassigned to exactly one mentor. For every combination of
- day
- time slot with that mentor
- learner for that mentor
we create a Potential Booking record.
In the worst case (all mentors using all 8 slots) that is up to:
3 days Ă 8 slots Ă 175 learners â 4,200 Potential Booking records
The actual number is a bit lower because not every mentor uses all 8 slots, but it is still thousands of records for what is conceptually a single âbook this slot for this learnerâ action.
Core logic - keeping availability and bookings in sync
The core logic is implemented in calculation fields and automations.
Aggregating availability and bookings
An earlier step aggregates the data into a single JSON blob that looks roughly like this:
[
{
"availability": {
"day": "2025-12-15T00:00:00.000Z",
"mentor": "Suzanne",
"room": "Barceloneta",
"possibleLearners": ["Lea", "Pau M", "Renee", "..."],
"availableTimeSlots": ["10:00", "11:00", "12:00"]
},
"bookings": [
{ "learner": "Lisa", "time": "14:00" },
{ "learner": "Mia", "time": "15:00" }
]
},
{
"availability": { "... second day ..." },
"bookings": [ /* ... */ ]
}
]
Removing learners who are booked on any day
In one calculation field I run JavaScript that:
- collects all booked learners across all days
- filters
possibleLearnersof each day against that set
Conceptually:
const allBookedLearners = new Set();
data.forEach(entry => {
entry.bookings.forEach(b => {
if (Array.isArray(b.learner)) {
b.learner.forEach(name => allBookedLearners.add(name));
} else if (b.learner) {
allBookedLearners.add(b.learner);
}
});
});
const normalized = data.map(entry => {
const avail = entry.availability;
const cleanedLearners = avail.possibleLearners.filter(
name => !allBookedLearners.has(name)
);
return {
...entry,
availability: {
...avail,
possibleLearners: cleanedLearners
}
};
});
JSON.stringify(normalized);
This is something the original spreadsheet could not do reliably: as soon as someone is booked on one day, they simply disappear from the list of clickable learner links on the other days, so parents only ever see valid options.
Human friendly views
From this JSON we generate:
- HTML tables for the internal overview
- parent friendly HTML with a logo, a note to parents and color coded availability
- CSV / XLSX exports for backup and special cases
All of that is done in calculation fields that use JSON.parse and then format strings.
Frontend - a lightweight booking page with links
Instead of building a complex app, the booking frontend is a simple HTML page generated from Tape.
- Tape is the backend and source of truth
- a calculation field builds an HTML page that includes
- a logo and a note to parents
- a table of mentors (with the room in brackets)
- for each time slot, a list of remaining available learners, each shown as a clickable link
Each learner name is not a generic âbook nowâ endpoint. It is a direct link to the weblink of the corresponding Potential Booking record, something like:
https://weblink.tapeapp.com/trigger/6d07d7fb-...-45e7-9a93-b03...
When a parent clicks that link:
- Tape runs the weblink in the context of that Potential Booking record
- The script reads the parameters (day, mentor, time, learner) from the record
- It shows the parent a Booking Confirmation with all the details
- It creates a Booking record
- It triggers the downstream automations to refresh availability and update the HTML
The user experience is still âclick a name in a gridâ, but behind that click there is a fairly elaborate record and weblink setup.
Dead ends and platform limitations
On paper this sounded simple. In practice we hit a few Tape platform constraints that are worth mentioning.
1. (Abandoned) webhook approach - no GET support
Early in the project I experimented with a different architecture:
-
use a Tape âcatchâ webhook as a generic API endpoint
-
call it from the frontend like:
GET https://.../api/catch/<token>?learner=Lisa&slot=169588399-169588460
What you actually get back with a GET is a 404:
{
"status_code": 404,
"endpoint": "/api/catch/<token>",
"error_code": "not_found",
"error_message": "Cannot GET /api/catch/<token>"
}
Webhooks only accept POST. I briefly had a version where the frontend sent POST requests with JSON bodies and the server side script read everything from payload.body. That prototype worked, but in the end I dropped webhooks entirely in favor of only using weblinks plus records, because:
- the rest of the system was already very record centric
- access control and context in weblinks fit better with how Tape is structured
- mixing two different entry points (weblinks and webhooks) felt unnecessary for this use case
So webhooks are a real limitation worth knowing about, but they are not part of the final implementation.
2. Weblinks do not accept parameters
The second roadblock was with Tape weblinks themselves:
- I wanted to use one weblink as a generic âbook this slotâ endpoint
- idea: call it with a query like
.../weblink?slot=...&learner=...
That does not work. Weblinks in Tape do not see query parameters in a way that the script can easily consume. They execute in the context of a record and are not designed as parameterized HTTP endpoints.
That limitation is the reason for the next pain point.
3. Record explosion - one Potential Booking record per possible booking
Because weblinks do not accept parameters, the only way to have a âbook this specific slot for this specific learnerâ action is:
- create a record that stores all those parameters
- attach a weblink to that record
- link to that weblink from the parent facing page
This is why the system needs a separate Potential Booking record for every possible combination of:
- day
- mentor
- time slot
- learner
In a clean backend world you would have a single endpoint like:
POST /book { day, mentor, time, learner }
In Tape, the equivalent is:
- precreate N Potential Booking records
- generate N unique weblink URLs in the page
- let parents click those URLs
For this project I accepted the tradeoff and:
- created a dedicated app for Potential Bookings
- hid it from normal users
- plan to clean it up periodically after the event
It works reliably, but it is a lot of moving parts for what is conceptually a stateless booking call.
What worked well
Despite the constraints, using Tape as the booking engine had a lot of upsides compared to the old Google Sheet.
-
Single source of truth
Availability, bookings, exports and communication all live in one place and are modeled explicitly as objects and records, not inferred from arbitrary cell contents. -
JSON + calculation fields
Storing flexible JSON blobs in calculation fields and using JavaScript to transform them worked really well for- removing booked learners from other days
- generating different views (internal vs parent facing) from the same data
-
Automations as glue
Automations connected everything:- on booking creation they update availability
- they generate HTML snippets for emails and pages
- they produce CSV / XLSX exports staff can download if needed
-
Quick iteration
Most changes were a matter of editing a calculation field or a JS action and reloading the page, without breaking the parent facing interface.
Wish list for Tape after this project
If I translate the dead ends into feature ideas, I end up with three things that would make systems like this easier to build:
-
GET support for catch webhooks
Even if POST remains the default, having basic GET support would be handy for simple integrations and debugging. -
Parameter aware weblinks
A way to read query parameters inside a weblink context would remove a lot of boilerplate Potential Booking records that only exist to carry parameters. -
Headless action endpoints
Some kind of âaction without recordâ endpoint (that is not âreceive a webhookâ) that can receive a JSON payload and run a script would make Tape an even more powerful backend for lightweight apps, especially when migrating from simple tools like shared Google Sheets.
If you are thinking about using Tape for school events, bookings or any other scheduling problem, this kind of architecture is very doable. Just be prepared to think in terms of records and automations first, and HTTP endpoints second.

