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

How a testimonial request goes out

The collector only asks customers who are on the list. So the first job is making sure the list actually reflects who’s been happy lately. There are three ways a happy moment gets in: somebody types a row in the Drive sheet, somebody forwards a glowing email to a dedicated address, or a 5-star rating in your review tool pings a small endpoint. The first one is obvious. The other two exist because in real life nobody types a row in a sheet for the kind email they just read.

Key takeaways

  • Three intake lanes feed one list: the Drive sheet, an inbox-forwarding lane, and a ratings webhook.
  • Forwarded praise is read by Bedrock Haiku 4.5, which proposes a row for one-tap approval.
  • The ratings lane is a Function URL: a 5-star rating posts a customer straight in as a candidate.
  • One warm ask goes out at the right moment, then at most one gentle reminder — never a third.
  • Asks respect quiet hours and a never-nag cool-down. No reply is treated as a polite no.

Three lanes into one list

Three intake lanes funnel into one 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 list of happy customers; the drive-sync Lambda mirrors the sheet to S3 every 15 minutes, and the collector reads from there. Lane two, Inbox forwarding: somebody forwards a glowing email to a dedicated address, kudos-at-your-company; SES writes the raw MIME to S3; a parser Lambda reads the email text and calls Bedrock Haiku 4.5 to extract the customer name, email, and a one-line summary of what they liked; the proposed row gets posted to the team's Slack with Approve and Edit buttons; on approve, the row is added to the Drive sheet via the Sheets API. Lane three, Ratings webhook: a 5-star rating in your review tool calls a small Lambda Function URL that adds the customer as a candidate with the rating and date; no email parsing is needed because the rating already carries the contact. All three lanes converge on the same Drive sheet, which the drive-sync Lambda keeps mirrored to S3 for the collector to read. A note at the bottom: the Drive sheet stays the source of truth, and the ask only goes out once a candidate clears the never-nag cool-down. Lane 1 · manual Drive sheet • Somebody types a row directly • drive-sync mirrors to S3 every 15 min • Collector reads from S3 • Source of truth stays in Drive Lane 2 · SES + Haiku Inbox forwarding • Forward praise to kudos-address • SES writes MIME to S3 • Haiku 4.5 reads it; proposes a row • Slack one-tap approve → sheet Lane 3 · webhook Ratings webhook • A 5-star rating lands in your tool • It posts to a Function URL • Customer added as a candidate • Rating + date come with it Drive list sheet (source of truth) name · email · moment · moment date · rating · reply · quote · permission · state drive-sync mirrors it to S3 every 15 min — collector reads from S3 to collector, daily tick The Drive sheet stays the source of truth — the ask only fires once a candidate clears the cool-down.
Fig 2. Three lanes converge on one Drive sheet. The sheet is the source of truth; the inbox lane and the ratings webhook are conveniences that propose or add candidates. The drive-sync Lambda mirrors the sheet to S3 so the collector can read it without hitting Drive on every tick.

Lane 1: the Drive sheet itself

The simplest lane. Open the list sheet in Drive, add a row, save. The columns are short: name, email, the moment that made them a candidate, the date of that moment, the rating (if any), and later the reply, the tidied quote, the permission state, and the publish state. A small Lambda — drive-sync — runs every fifteen minutes, exports the sheet as plain CSV via the Drive API, and writes it to s3://tc-list-source/list.csv if the sheet has changed since the last sync. The collector 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 already know a customer was happy — the call that ended well, the handshake at the end of a project — and you can spend thirty seconds typing it in.

Lane 2: inbox forwarding (the lane most teams actually use)

Set up a dedicated inbound address — something like kudos@your-company.com — via Amazon SES. Anyone on the team forwards a kind email to that address and the collector takes it from there. SES writes the raw MIME to s3://tc-raw-mime/. The S3 PUT triggers a parser Lambda. The Lambda walks the MIME tree to the message body and pulls out the plain text of the forwarded note. There’s no document parsing here — praise arrives as email, so no Textract and no per-page charges.

Then a Bedrock Haiku 4.5 call reads the text and emits a structured row: the customer name, their email (from the original sender line), the moment type (set to “glowing reply”), and a one-line summary of what they liked. The model prompt is short: “Pull out who said this and a one-line summary. Return JSON only. Do not invent an email that isn’t in the message.” The output goes to a small Slack interactive message that pings the person who forwarded the email: the proposed row and three buttons — approve, edit, discard. On approve, a Lambda writes the row to the Drive sheet via the Sheets API. On edit, they get a fillable modal pre-populated with the proposal. On discard, the message is logged and the email moved to a discarded prefix in S3 for audit.

The reason every parsed row goes to a human first is simple: asking the wrong person for a testimonial — or getting their name wrong — is a worse outcome than the email never making the list at all.

Lane 3: ratings webhook

Many teams already collect star ratings — on Google, on a booking tool, in a post-job survey. When a 5-star rating comes in, there’s no reason to retype it. Lane 3 is a small Lambda Function URL that your review tool calls on each new rating. The Lambda checks the score; a 5-star (or 4-and-above, if you set it that way) rating with a contact attached is written straight onto the list as a candidate, with the rating and the date already filled in. A lower rating is ignored — this system only ever asks the people who were genuinely happy.

The webhook is the most hands-off of the three lanes. Once it’s wired up, happy customers flow onto the list without anyone lifting a finger, and the never-nag cool-down (covered below) makes sure nobody gets asked twice in a short window.

How the ask actually goes out

Once a candidate is on the list and has cleared the cool-down, the daily tick decides on a move (the full move logic is the next post’s territory; here’s the short version). On the first ask, the collector reads the matching template from the voice doc, fills in the customer’s name and the moment, and sends one short, warm email through SES with a single link to a reply form. If there’s no reply after a set gap (default 5 days), it sends exactly one gentle reminder. If that also gets no reply, the candidate is closed quietly — no reply is treated as a polite no, and a decline buys a full year of silence. Every send respects quiet hours and the holiday calendar, so an ask never lands at 2am or on a public holiday.

The whole point of capping it at one reminder is trust. A business that asks once, kindly, and then stops is a business people are happy to hear from. A business that asks five times is one people mute.

Next post: how a customer’s reply gets tidied into a short, clean quote — without changing a word of what they meant.

All posts