Part 2 of 7 · Feedback collector series ~4 min read

How a feedback request goes out

The system can only ask the people it knows about. So the first job is making sure the customer list actually reflects who walked in and who walked out happy or not. There are three ways a finished visit gets onto the list: somebody types a row in the Drive sheet, your till posts one the moment a sale closes, or a finished appointment gets pulled in from your booking calendar. Once a visit is on the list, a timing rule picks the right moment — and then it asks once.

Key takeaways

  • Three intake lanes feed one list: the Drive sheet, a point-of-sale webhook, and a booking import.
  • The point-of-sale lane posts a row to a Function URL the moment a sale closes — no typing.
  • Finished appointments tagged in your calendar get pulled hourly via the Google Calendar API.
  • A timing rule per visit type picks the right send moment — meal two hours later, product three days.
  • The list stays the canonical store. The other lanes are conveniences that write into it.

Three lanes into one list

Three intake lanes funnel into one customer list A diagram with three vertical lane columns at the top and a single unified row at the bottom. Lane one, Drive sheet: somebody types a row directly into the Google Sheet that holds the customer list; the drive-sync Lambda mirrors the sheet to S3 every 15 minutes, and the system reads from there. Lane two, POS webhook: your till or checkout tool calls a Function URL the moment a sale closes; a small Lambda validates the payload, then appends the row to the Drive sheet via the Sheets API, so a finished sale becomes a customer to ask with no typing. Lane three, Booking import: finished appointments on your Google Calendar get pulled hourly by a calendar-sync Lambda using the Calendar API; each finished booking becomes a row in the same sheet. All three lanes converge on the same Drive sheet, which the drive-sync Lambda keeps mirrored to S3 for the request builder to read. A note at the bottom: the Drive sheet stays the source of truth — the other lanes are conveniences that add rows to it. Lane 1 · manual Drive sheet • Somebody types a row directly • drive-sync mirrors to S3 every 15 min • Builder reads from S3 • Source of truth stays in Drive Lane 2 · Function URL POS webhook • Till calls a Function URL on sale • Lambda validates the payload • Row appended to sheet, no typing • A sale becomes a customer to ask Lane 3 · hourly poll Booking import • Finished bookings on your calendar • calendar-sync polls hourly via Calendar API • Each finished one becomes a row • Added to the same sheet Drive customer list (source of truth) name · contact · bought · visit type · date · staff · status drive-sync mirrors it to S3 every 15 min — builder reads from S3 to request builder The Drive sheet stays the source of truth — the other lanes are conveniences that add rows to it.
Fig 2. Three lanes converge on one Drive sheet. The sheet is the source of truth; the POS webhook and the booking import are conveniences that add rows. The drive-sync Lambda mirrors the sheet to S3 so the builder can read it without hitting Drive on every run.

Lane 1: the Drive sheet itself

The simplest lane. Open the customer list in Drive, add a row, save. The columns are short: name, contact (email or mobile), what they bought or booked, visit type, the date it happened, and the staff member who served them. A small Lambda — drive-sync — runs every fifteen minutes, exports the sheet as plain CSV via the Drive API, and writes it to s3://fc-customers-source/customers.csv if the sheet has changed since the last sync. The builder reads from S3, not Drive directly. That keeps Drive API calls predictable and gives you S3 versioning for free, so a bad bulk-edit can be rolled back in one click.

This lane covers the cases where you don’t have a till that talks to anything — a market stall, a one-off job, a referral you want to follow up. Most businesses use it for the first week and then lean on the other two.

Lane 2: the point-of-sale webhook (the lane most teams actually use)

Set up a small intake-webhook Lambda with a Function URL — a plain web address your till or online checkout can call. Most modern point-of-sale and e-commerce tools (Square, Shopify, your booking platform) can fire a webhook when a sale closes. Point it at the Function URL. When a sale completes, the till posts a small bundle of data: who bought, what they bought, when, and which staff member rang it up. The Lambda checks the payload is genuine (a shared secret in the header, verified against Secrets Manager), drops anything that’s a refund or a test, and appends a clean row to the Drive sheet via the Sheets API.

The point of this lane is that nobody has to remember to type anything. The moment Dwi pays for dinner, she’s on the list, and the timing clock starts. There’s no batch upload at the end of the day, no “oh, I forgot to add the Tuesday customers.” A finished sale becomes a finished-visit row in seconds.

One guardrail: the webhook never asks immediately. It only adds the row. The actual ask is scheduled for the right moment, which is the timing rule below. That keeps the “don’t pester someone who’s still standing at your counter” rule firmly in one place.

Lane 3: booking import

Some businesses run on appointments, not walk-in sales — a salon, a clinic, a repair shop, a consultant. For them the “finished visit” isn’t a till transaction, it’s an appointment that has happened. A small calendar-sync Lambda runs hourly, looks at the configured Google Calendars (using a service-account credential stored in Secrets Manager), and pulls any appointment whose end time has just passed and which is marked as completed (the simplest convention is a #done tag the staff add when the client leaves, or a calendar whose events are confirmed bookings). Each finished appointment becomes a row in the same customer list, with the visit type set to whatever the booking was for.

Booking import is the most opt-in of the three lanes. A shop that has no appointments loses nothing by skipping it; a clinic that lives on its calendar avoids retyping every visit by hand.

The timing rule: ask once, at the right moment

A row landing on the list does not send an ask. It schedules one. The request builder reads the visit type and looks up the wait time in the rules doc: “sit-down meal: ask 2 hours later. Takeaway: ask 1 hour later. Delivered product: ask 3 days later. Service appointment: ask the next morning.” The right moment is different for each because the question only works once the experience is fresh and complete — asking about a delivered product the second the order is placed is useless; the box hasn’t even arrived.

The builder computes the send time, checks it against quiet hours (default 8pm to 9am) and the holiday list, nudges it to the next allowed minute if needed, and creates a one-off EventBridge Scheduler rule that fires the ask at exactly that time. One rule, one ask. If the customer doesn’t reply, a daily sweep sends at most one gentle follow-up a few days later, then marks the row done for good. A customer who already replied is never asked again. The next post covers what happens when a reply actually comes back.

All posts