Part 2 of 7 · Onboarding guide series ~4 min read

How a new customer gets enrolled

The guide only guides the customers who are on the list. So the first job is making sure a new customer lands on the list the moment they sign up — without anyone having to remember to add them. There are three ways a customer gets in: your app calls a webhook when someone signs up, somebody types a row in the Drive sheet, or somebody forwards the welcome email to a dedicated address. The first one carries almost everyone. The other two exist for the customers your app can’t tell the guide about on its own.

Key takeaways

  • Three enrollment lanes feed one list: a signup webhook, the Drive sheet, and an inbox-forwarding lane.
  • The webhook is a Lambda Function URL — your app posts a new customer and the row is enrolled at once.
  • Forwarded welcome emails are read by Bedrock Haiku 4.5, which proposes a row.
  • Every webhook and parsed row is checked before the sequence starts at day 0.
  • The Drive sheet stays the canonical store. The other lanes are conveniences that write into it.

Three lanes into one list

Three enrollment 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, Signup webhook: when someone signs up, your app posts the new customer to a Lambda Function URL; the enroll Lambda validates the payload, normalizes the row, and adds it to the Google Sheet that holds the onboarding list; the drive-sync Lambda mirrors the sheet to S3 every 15 minutes, and the guide reads from there. Lane two, Drive sheet: somebody types a customer row directly into the sheet; the same drive-sync mirror picks it up; the source of truth stays in Drive. Lane three, Inbox forwarding: somebody forwards a welcome email to a dedicated address, welcome-at-your-company; SES writes the raw MIME to S3; a parser Lambda calls Bedrock Haiku 4.5 to read the name, email, and plan; the proposed row gets posted to a rep's Slack with Approve and Edit buttons; on approve, the row is added to the Drive sheet via the Sheets API. All three lanes converge on the same Drive sheet, which the drive-sync Lambda keeps mirrored to S3 for the guide to read. A note at the bottom: the Drive sheet stays the source of truth — the webhook and inbox lanes are conveniences that propose rows for it. Lane 1 · Function URL Signup webhook • App posts new customer on signup • enroll Lambda validates payload • Row added to the sheet • Sequence starts at day 0 Lane 2 · manual Drive sheet • Somebody types a customer row • drive-sync mirrors to S3 every 15 min • Guide reads from S3 • Source of truth stays in Drive Lane 3 · SES + Haiku Inbox forwarding • Forward welcome to welcome-address • SES writes MIME to S3 • Haiku 4.5 reads name, email, plan • Slack one-tap approve → sheet Drive onboarding sheet (source of truth) name · email · plan · signup date · current step · steps done · paused drive-sync mirrors it to S3 every 15 min — guide reads from S3 to guide, daily tick The Drive sheet stays the source of truth — the webhook and inbox lanes propose rows for it.
Fig 2. Three lanes converge on one Drive sheet. The sheet is the source of truth; the signup webhook and the inbox lane write into it. The drive-sync Lambda mirrors the sheet to S3 so the guide can read it without hitting Drive on every tick.

Lane 1: the signup webhook (the lane that carries everyone)

The main lane. Your app already knows the instant someone signs up — that’s the moment to enroll them. Set up a Lambda Function URL (a plain HTTPS endpoint on a Lambda, no API Gateway needed) and have your app POST a small JSON body to it whenever a new customer is created: name, email, plan, and a signup timestamp. The enroll Lambda validates the body (checks the email looks real, the plan is one you know, the signup date isn’t in the future), normalizes it into the sheet’s column shape, and writes the row via the Sheets API. The customer is on the list within a second of signing up, and the sequence starts at day 0 on the next tick.

The endpoint is protected with a shared secret your app sends in a header, checked against a value in Secrets Manager — so a random POST from the internet can’t enroll a fake customer. This is the lane that should carry almost every real signup; the other two exist for the edges.

Lane 2: the Drive sheet itself

The simplest lane. Open the onboarding sheet in Drive, add a row, save. The columns are short: name, email, plan, signup date, the current step, which steps are done, and a paused flag. A small Lambda — drive-sync — runs every fifteen minutes, exports the sheet as plain CSV via the Drive API, and writes it to s3://og-list-source/onboarding.csv if the sheet has changed since the last sync. The guide 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 customers your app can’t webhook for you — the enterprise account your sales team set up by hand, the customer you migrated from an old system, the one who signed a contract over email. Type the row and the guide picks them up on the next tick.

Lane 3: inbox forwarding

Some signups arrive as an email, not an app event. A reseller sends over a new account. A customer replies to your sales thread to confirm they want to start. The welcome message your billing tool sends lands in a shared inbox. Forcing someone to retype those into a sheet is a small chore that gets skipped exactly when things are busy.

Set up a dedicated inbound address — something like welcome@your-company.com — via Amazon SES. Anyone forwards the welcome email there. SES writes the raw MIME to s3://og-raw-mime/. The S3 PUT triggers a parser Lambda that calls Bedrock Haiku 4.5 to read the email and emit a structured row: name, email, plan (if it can tell), and a signup date. The prompt is short: “Extract a customer row. Return JSON only. Mark each field with a confidence score. Do not invent a plan that isn’t in the text.” The proposal goes to a small Slack message that pings the rep who forwarded it — the proposed row, the confidence per field, and three buttons: approve, edit, discard. On approve, the row is written to the sheet via the Sheets API.

The reason every parsed row goes to a human first is simple: a customer enrolled on the wrong plan gets the wrong step plan, and a customer enrolled with a typo in their email never gets a single message. Both are worse than a row that needed ten seconds of human confirmation.

Why the list stays the source of truth

Three lanes in, but only one place where the guide actually looks. That’s a deliberate constraint. If two lanes both wrote directly to the guide’s state, every “why did this customer get this message?” question would mean checking three places. Funneling everything through the Drive sheet means there is exactly one row per customer, and any rep can read or edit any of it without learning a new tool. The convenience lanes are first-class for getting customers in, but they always pass through the sheet on the way.

Next post: how the guide actually reads the list, computes days-since-signup, looks at which steps are done, and picks one of four moves.

All posts