Small Biz Workflows
FORM SBW-002 · REV B · 5-FILE KITFollow-Up

Automated Review Requests

Ask every customer for a Google review automatically after the job closes — with an LLM reading the replies, catching complaints before they go public, and personalizing every ask.

TIME TO VALUE
1 afternoon (basic) · 1 weekend (with the LLM layer)
RUNNING COST
$5–15 SMS + $2–8 LLM usage
STACK
Twilio (or your CRM's SMS) · Google Business Profile review link · A 'job completed' trigger (invoice, calendar, or CRM) · An LLM (Claude) for reply triage and personalization
Download the kit (.zip)Free · Field-Tested

PROVENANCE: Production review workflow run for Alberta service businesses. Generalized for any small business — no vendor lock-in, no secrets, adapt freely. The kit's INSTALL.md is a one-shot Claude Code build: unzip, paste one prompt, answer ~5 questions.

FILE: WORKFLOW.mdDOWNLOAD THIS FILE

TL;DR

Every finished job triggers one short, personal text with your Google review link, sent when satisfaction peaks. An LLM reads whatever the customer texts back: happy replies get a thank-you, complaints get caught privately and escalated to you before they become 1-star reviews. The asking runs itself; you keep the judgment.

The problem this solves

Reviews are the local-search ranking factor you control most directly, and the trust signal customers check first. But asking is awkward, easy to forget, and always loses to the next job. So the businesses with the best work often have the thinnest review profiles — and lose map-pack position to mediocre competitors who simply ask.

The fix: take asking off your plate entirely, and put an AI safety net under the replies.

The full picture

                          THE END-TO-END FLOW
═══════════════════════════════════════════════════════════════════════

 ┌──────────────┐  marked   ┌──────────────────┐   wait    ┌─────────────────┐
 │   JOB DONE   │ ────────▶ │  TRIGGER FIRES   │ ────────▶ │  REVIEW ASK SMS │
 │ (invoice paid│           │ (webhook from    │  1–3 hrs  │ named, personal,│
 │  or calendar │           │  invoice/CRM/cal)│           │ one link        │
 │  event ends) │           └──────────────────┘           └────────┬────────┘
 └──────────────┘                    │                              │
                                     ▼                              ▼
                          ┌──────────────────┐            ┌──────────────────┐
                          │  SENT-LIST CHECK │            │     CUSTOMER     │
                          │ one ask per      │            │  taps link → ⭐⭐⭐⭐⭐ │
                          │ customer, EVER   │            │  or texts back   │
                          └──────────────────┘            └────────┬─────────┘
                                                                   │ reply
                                                                   ▼
                                                       ┌──────────────────────┐
                                                       │      LLM TRIAGE      │
                                                       │ reads the reply:     │
                                                       │  happy? → thank them │
                                                       │  problem? → ALERT    │
                                                       │  question? → forward │
                                                       └──────────┬───────────┘
                                                                  │
                                              ┌───────────────────┴──────────┐
                                              ▼                              ▼
                                    ┌──────────────────┐          ┌──────────────────┐
                                    │   YOUR PHONE     │          │   REVIEW LOG     │
                                    │ "Dana has a leak │          │ asks, replies,   │
                                    │  complaint —     │          │ reviews, saves   │
                                    │  call her first" │          │ (the ROI table)  │
                                    └──────────────────┘          └──────────────────┘
═══════════════════════════════════════════════════════════════════════

The quiet genius: the reply-to-fix line in your ask message is a satisfaction check and a review ask in one — and the LLM is the net that catches problems privately, before they go public.

What you need

Piece What it does Pick
Direct review link Opens the Google review box in one tap Google Business Profile → "Ask for reviews" → copy the g.page short link
Completion trigger Tells the system a job finished Invoice marked paid, calendar event ended, or CRM status change — whatever already exists
SMS sender Delivers the ask Twilio, or the texting in tools you already pay for
LLM Triages replies, personalizes asks Claude (Haiku tier — pennies per reply)
Review log Tracks asks → reviews → saves One table or sheet

Phase 1 — The automated ask

  JOB CLOSES ──▶ wait 1–3 hrs ──▶ checks ──▶ personalized SMS ──▶ log row
                                    │
                                    ├─ already asked this customer, ever? → skip
                                    ├─ job flagged "do not ask"?          → skip
                                    └─ customer opted out?                → skip
  1. Pick the trigger that already exists. Don't add a crew step. Invoice-paid is best — nobody forgets to invoice.
  2. Wait 1–3 hours after completion. Fresh enough to remember, settled enough to not feel robotic.
  3. Send one short, named, personal text (template below).
  4. Suppress repeats forever. One ask per customer, ever. The sent-list check runs before every send.
  5. Route replies into the LLM triage (Phase 2).

The ask (template):

Hi {{first_name}}, thanks for having {{business_name}} out for the {{job_type}} today. If you were happy with the work, a quick Google review helps us a ton: {{review_link}} — and if anything's not right, just reply here and we'll fix it.

LLM-personalized variant: feed the job notes to the LLM and let it name something real:

You write SMS review requests for {{business_name}}. Using the job notes below, write ONE text under 300 characters that: greets {{first_name}} by name, references one specific detail of the work (from the notes), includes {{review_link}}, and ends with "if anything's not right, just reply here and we'll fix it." Warm, plain, no exclamation overload, no emojis.

JOB NOTES: {{job_notes}}

"Thanks for letting us replace the hot-water tank in the Larch St basement" out-converts "thanks for your business" every single week.

Phase 2 — The LLM reply net

Some customers reply to the text instead of (or before) leaving a review. Every one of those replies is gold — route them well.

  CUSTOMER REPLY arrives
        │
        ▼
  ┌──────────────────────────────────────────┐
  │ LLM TRIAGE — returns strict JSON:        │
  │ {                                        │
  │   "sentiment": "happy | unhappy |        │
  │                 question | unsubscribe", │
  │   "issue_summary": "tap still drips",    │
  │   "needs_human": true/false,             │
  │   "draft_reply": "…",                    │
  │   "confidence": 0.94                     │
  │ }                                        │
  └────────────────────┬─────────────────────┘
                       │
     ┌─────────────────┼──────────────────┬──────────────────┐
     ▼                 ▼                  ▼                  ▼
   happy            unhappy            question         unsubscribe
 ┌──────────┐   ┌───────────────┐   ┌─────────────┐   ┌─────────────┐
 │ auto     │   │ ALERT OWNER   │   │ forward to  │   │ add to      │
 │ thank-you│   │ NOW + draft   │   │ your inbox  │   │ suppression │
 │ reply    │   │ apology, hold │   │ with draft  │   │ list, reply │
 └──────────┘   │ all further   │   │ answer      │   │ confirm     │
                │ asks          │   └─────────────┘   └─────────────┘
                └───────────────┘

The triage prompt:

You read SMS replies to a review request from {{business_name}}. Classify the reply below. Return ONLY JSON: sentiment ("happy", "unhappy", "question", "unsubscribe"), issue_summary (under 10 words, or null), needs_human (true if unhappy or any repair issue is mentioned), draft_reply (under 300 chars, warm, never defensive, never offers discounts, never mentions reviews if sentiment is unhappy), confidence (0–1). If the reply mentions ANY problem with the work, sentiment is "unhappy" even if polite.

REPLY: {{reply_text}}

The rule that makes this safe: an "unhappy" classification stops everything. No thank-you, no further asks, owner alerted with the issue summary. A complaint you catch in a private text thread is a 1-star review that never happened.

Data model

 review_asks
 ┌────────────────┬──────────────────────────────────────────┐
 │ id             │ uuid                                     │
 │ customer_phone │ +1403…  (unique — enforces one-ask-ever) │
 │ customer_name  │ Dana                                     │
 │ job_type       │ hot water tank replacement               │
 │ job_closed_at  │ timestamp                                │
 │ asked_at       │ timestamp                                │
 │ reply_text     │ text or null                             │
 │ sentiment      │ happy | unhappy | question | unsub | null│
 │ issue_summary  │ "tap still drips" or null                │
 │ review_left    │ boolean (manual weekly check)            │
 │ saved_job      │ boolean (complaint caught + fixed)       │
 └────────────────┴──────────────────────────────────────────┘

review_left and saved_job are your two ROI columns: reviews gained, disasters dodged.

Folder structure

 review-engine/
 ├── CLAUDE.md            ← the AI-builder brief (ships in this kit)
 ├── api/
 │   ├── job-closed.ts    ← trigger webhook → schedule the ask
 │   └── inbound-sms.ts   ← replies → LLM triage → route
 ├── lib/
 │   ├── ask.ts           ← template/LLM personalization + suppression
 │   ├── triage.ts        ← the triage prompt + JSON validation
 │   └── log.ts           ← review_asks read/write
 └── prompts/
     ├── personalize.md
     └── triage.md

Compliance notes (US + Canada + Google)

  • US — TCPA + 10DLC: texting an existing customer about the job they just hired you for is the safe, expected case, but register A2P 10DLC with your SMS provider before volume and honour STOP/HELP instantly. Don't repurpose the review thread into marketing blasts.
  • Canada — CASL: a service message to an existing customer about their own job is implied consent — identify yourself, honour opt-outs instantly.
  • Never gate (Google policy): asking only happy customers ("review gating") violates Google's terms. Ask everyone; the LLM net is your protection, not selective asking.
  • Never incentivize: no discounts for reviews — Google can wipe your whole profile.
  • The LLM drafts must never offer compensation in an apology — that's an owner decision, made by the owner.

Numbers to watch

Metric Healthy Where it comes from
Ask → review conversion 15–30% review_left / rows
Complaints caught privately every one is a win sentiment = unhappy
Time to owner callback on complaints < 2 hours alert timestamp → your call
Review count trajectory +5–15/month for a 10-job/week business Google Business Profile

A business doing 10 jobs a week goes from 12 reviews to 100+ in a year — usually enough to change map-pack position in a small market.

Week-one checklist

  • Close a test job — ask arrives 1–3 hours later, named, with the right job type
  • The link opens the Google review box directly (not a search page)
  • Reply "thanks, you guys were great" — auto thank-you, sentiment logged happy
  • Reply "actually the tap still drips" — your phone gets the alert, no thank-you sent
  • Reply STOP — suppressed, confirmed
  • Close a second job for the same customer — no second ask
  • Check Google for the first real review and mark review_left

Troubleshooting

  • Low conversion: your link probably opens a search page instead of the review modal. Use the official short link from Google Business Profile.
  • Crew forgets to close jobs: tie the trigger to money (invoice paid). Nobody forgets to invoice.
  • LLM marks polite complaints as happy: the prompt rule "ANY problem mentioned = unhappy" must be present. Test with "great service but the tap still drips."
  • Asks feel robotic: switch from the static template to the LLM-personalized variant — job notes in, one specific detail out.
  • Customer replies weeks later: keep the thread alive; triage runs on every inbound message, however late.

Don't want to download? Read it right here

Every file in the kit, on the page, in full. Expand one, hit copy, paste it wherever you work. Same content as the zip — the download just saves you the copying.

WORKFLOW.md · 13k chars
## TL;DR

> Every finished job triggers one short, personal text with your Google review link,
> sent when satisfaction peaks. An LLM reads whatever the customer texts back:
> happy replies get a thank-you, complaints get caught privately and escalated to
> you before they become 1-star reviews. The asking runs itself; you keep the judgment.

## The problem this solves

Reviews are the local-search ranking factor you control most directly, and the trust signal customers check first. But asking is awkward, easy to forget, and always loses to the next job. So the businesses with the best work often have the thinnest review profiles — and lose map-pack position to mediocre competitors who simply ask.

The fix: take asking off your plate entirely, and put an AI safety net under the replies.

## The full picture

```
                          THE END-TO-END FLOW
═══════════════════════════════════════════════════════════════════════

 ┌──────────────┐  marked   ┌──────────────────┐   wait    ┌─────────────────┐
 │   JOB DONE   │ ────────▶ │  TRIGGER FIRES   │ ────────▶ │  REVIEW ASK SMS │
 │ (invoice paid│           │ (webhook from    │  1–3 hrs  │ named, personal,│
 │  or calendar │           │  invoice/CRM/cal)│           │ one link        │
 │  event ends) │           └──────────────────┘           └────────┬────────┘
 └──────────────┘                    │                              │
                                     ▼                              ▼
                          ┌──────────────────┐            ┌──────────────────┐
                          │  SENT-LIST CHECK │            │     CUSTOMER     │
                          │ one ask per      │            │  taps link → ⭐⭐⭐⭐⭐ │
                          │ customer, EVER   │            │  or texts back   │
                          └──────────────────┘            └────────┬─────────┘
                                                                   │ reply
                                                                   ▼
                                                       ┌──────────────────────┐
                                                       │      LLM TRIAGE      │
                                                       │ reads the reply:     │
                                                       │  happy? → thank them │
                                                       │  problem? → ALERT    │
                                                       │  question? → forward │
                                                       └──────────┬───────────┘
                                                                  │
                                              ┌───────────────────┴──────────┐
                                              ▼                              ▼
                                    ┌──────────────────┐          ┌──────────────────┐
                                    │   YOUR PHONE     │          │   REVIEW LOG     │
                                    │ "Dana has a leak │          │ asks, replies,   │
                                    │  complaint —     │          │ reviews, saves   │
                                    │  call her first" │          │ (the ROI table)  │
                                    └──────────────────┘          └──────────────────┘
═══════════════════════════════════════════════════════════════════════
```

The quiet genius: the reply-to-fix line in your ask message is a satisfaction check and a review ask in one — and the LLM is the net that catches problems privately, before they go public.

## What you need

| Piece | What it does | Pick |
|---|---|---|
| Direct review link | Opens the Google review box in one tap | Google Business Profile → "Ask for reviews" → copy the `g.page` short link |
| Completion trigger | Tells the system a job finished | Invoice marked paid, calendar event ended, or CRM status change — whatever already exists |
| SMS sender | Delivers the ask | Twilio, or the texting in tools you already pay for |
| LLM | Triages replies, personalizes asks | Claude (Haiku tier — pennies per reply) |
| Review log | Tracks asks → reviews → saves | One table or sheet |

## Phase 1 — The automated ask

```
  JOB CLOSES ──▶ wait 1–3 hrs ──▶ checks ──▶ personalized SMS ──▶ log row
                                    │
                                    ├─ already asked this customer, ever? → skip
                                    ├─ job flagged "do not ask"?          → skip
                                    └─ customer opted out?                → skip
```

1. **Pick the trigger that already exists.** Don't add a crew step. Invoice-paid is best — nobody forgets to invoice.
2. **Wait 1–3 hours after completion.** Fresh enough to remember, settled enough to not feel robotic.
3. **Send one short, named, personal text** (template below).
4. **Suppress repeats forever.** One ask per customer, ever. The sent-list check runs before every send.
5. **Route replies into the LLM triage** (Phase 2).

**The ask (template):**

> Hi {{first_name}}, thanks for having {{business_name}} out for the {{job_type}} today.
> If you were happy with the work, a quick Google review helps us a ton: {{review_link}}
> — and if anything's not right, just reply here and we'll fix it.

**LLM-personalized variant:** feed the job notes to the LLM and let it name something real:

> You write SMS review requests for {{business_name}}. Using the job notes below, write
> ONE text under 300 characters that: greets {{first_name}} by name, references one
> specific detail of the work (from the notes), includes {{review_link}}, and ends with
> "if anything's not right, just reply here and we'll fix it." Warm, plain, no exclamation
> overload, no emojis.
>
> JOB NOTES: {{job_notes}}

"Thanks for letting us replace the hot-water tank in the Larch St basement" out-converts "thanks for your business" every single week.

## Phase 2 — The LLM reply net

Some customers reply to the text instead of (or before) leaving a review. Every one of those replies is gold — route them well.

```
  CUSTOMER REPLY arrives
        │
        ▼
  ┌──────────────────────────────────────────┐
  │ LLM TRIAGE — returns strict JSON:        │
  │ {                                        │
  │   "sentiment": "happy | unhappy |        │
  │                 question | unsubscribe", │
  │   "issue_summary": "tap still drips",    │
  │   "needs_human": true/false,             │
  │   "draft_reply": "…",                    │
  │   "confidence": 0.94                     │
  │ }                                        │
  └────────────────────┬─────────────────────┘
                       │
     ┌─────────────────┼──────────────────┬──────────────────┐
     ▼                 ▼                  ▼                  ▼
   happy            unhappy            question         unsubscribe
 ┌──────────┐   ┌───────────────┐   ┌─────────────┐   ┌─────────────┐
 │ auto     │   │ ALERT OWNER   │   │ forward to  │   │ add to      │
 │ thank-you│   │ NOW + draft   │   │ your inbox  │   │ suppression │
 │ reply    │   │ apology, hold │   │ with draft  │   │ list, reply │
 └──────────┘   │ all further   │   │ answer      │   │ confirm     │
                │ asks          │   └─────────────┘   └─────────────┘
                └───────────────┘
```

**The triage prompt:**

> You read SMS replies to a review request from {{business_name}}. Classify the reply below.
> Return ONLY JSON: sentiment ("happy", "unhappy", "question", "unsubscribe"),
> issue_summary (under 10 words, or null), needs_human (true if unhappy or any repair issue
> is mentioned), draft_reply (under 300 chars, warm, never defensive, never offers discounts,
> never mentions reviews if sentiment is unhappy), confidence (0–1).
> If the reply mentions ANY problem with the work, sentiment is "unhappy" even if polite.
>
> REPLY: {{reply_text}}

**The rule that makes this safe:** an "unhappy" classification stops everything. No thank-you, no further asks, owner alerted with the issue summary. A complaint you catch in a private text thread is a 1-star review that never happened.

## Data model

```
 review_asks
 ┌────────────────┬──────────────────────────────────────────┐
 │ id             │ uuid                                     │
 │ customer_phone │ +1403…  (unique — enforces one-ask-ever) │
 │ customer_name  │ Dana                                     │
 │ job_type       │ hot water tank replacement               │
 │ job_closed_at  │ timestamp                                │
 │ asked_at       │ timestamp                                │
 │ reply_text     │ text or null                             │
 │ sentiment      │ happy | unhappy | question | unsub | null│
 │ issue_summary  │ "tap still drips" or null                │
 │ review_left    │ boolean (manual weekly check)            │
 │ saved_job      │ boolean (complaint caught + fixed)       │
 └────────────────┴──────────────────────────────────────────┘
```

`review_left` and `saved_job` are your two ROI columns: reviews gained, disasters dodged.

## Folder structure

```
 review-engine/
 ├── CLAUDE.md            ← the AI-builder brief (ships in this kit)
 ├── api/
 │   ├── job-closed.ts    ← trigger webhook → schedule the ask
 │   └── inbound-sms.ts   ← replies → LLM triage → route
 ├── lib/
 │   ├── ask.ts           ← template/LLM personalization + suppression
 │   ├── triage.ts        ← the triage prompt + JSON validation
 │   └── log.ts           ← review_asks read/write
 └── prompts/
     ├── personalize.md
     └── triage.md
```

## Compliance notes (US + Canada + Google)

- **US — TCPA + 10DLC:** texting an existing customer about the job they just hired you for is the safe, expected case, but register A2P 10DLC with your SMS provider before volume and honour STOP/HELP instantly. Don't repurpose the review thread into marketing blasts.
- **Canada — CASL:** a service message to an existing customer about their own job is implied consent — identify yourself, honour opt-outs instantly.
- **Never gate (Google policy):** asking only happy customers ("review gating") violates Google's terms. Ask everyone; the LLM net is your protection, not selective asking.
- **Never incentivize:** no discounts for reviews — Google can wipe your whole profile.
- The LLM drafts must never offer compensation in an apology — that's an owner decision, made by the owner.

## Numbers to watch

| Metric | Healthy | Where it comes from |
|---|---|---|
| Ask → review conversion | 15–30% | `review_left` / rows |
| Complaints caught privately | every one is a win | `sentiment = unhappy` |
| Time to owner callback on complaints | < 2 hours | alert timestamp → your call |
| Review count trajectory | +5–15/month for a 10-job/week business | Google Business Profile |

A business doing 10 jobs a week goes from 12 reviews to 100+ in a year — usually enough to change map-pack position in a small market.

## Week-one checklist

- [ ] Close a test job — ask arrives 1–3 hours later, named, with the right job type
- [ ] The link opens the Google review box directly (not a search page)
- [ ] Reply "thanks, you guys were great" — auto thank-you, sentiment logged happy
- [ ] Reply "actually the tap still drips" — your phone gets the alert, no thank-you sent
- [ ] Reply STOP — suppressed, confirmed
- [ ] Close a second job for the same customer — no second ask
- [ ] Check Google for the first real review and mark `review_left`

## Troubleshooting

- **Low conversion:** your link probably opens a search page instead of the review modal. Use the official short link from Google Business Profile.
- **Crew forgets to close jobs:** tie the trigger to money (invoice paid). Nobody forgets to invoice.
- **LLM marks polite complaints as happy:** the prompt rule "ANY problem mentioned = unhappy" must be present. Test with "great service but the tap still drips."
- **Asks feel robotic:** switch from the static template to the LLM-personalized variant — job notes in, one specific detail out.
- **Customer replies weeks later:** keep the thread alive; triage runs on every inbound message, however late.
INSTALL.md · 3k chars
# INSTALL.md — One-Shot Build · Automated Review Requests (SBW-002)

You don't need to know how to code. You need three things:

1. **Claude Code** installed (`npm install -g @anthropic-ai/claude-code`)
2. Your **Google Business Profile** login + whatever tool marks your jobs done
   (invoice app, calendar, or CRM)
3. This kit, unzipped into an empty folder

## The one-shot

Open a terminal in the kit folder, run `claude`, and paste this entire block:

```
Read every file in this folder: CLAUDE.md, WORKFLOW.md, SKILLS.md, WALKTHROUGH.md.
They are the complete spec for an automated review-request system. CLAUDE.md is
your standing brief; its hard rules and definition-of-done are binding.

Then build it for me, one-shot, with these ground rules:

1. Ask me ONLY these setup questions, all at once, then stop asking:
   - Business name and what kind of work we do
   - Which tool tells us a job is finished (invoice app / calendar / CRM — and
     which one specifically)
   - My Google review short link (walk me through getting it if I don't have it)
   - My cell number (for complaint alerts)
   - SMS sender: Twilio (default) or my CRM's built-in texting

2. Scaffold the folder structure from CLAUDE.md exactly. Build the ask flow
   (Phase 1) completely before the LLM reply net (Phase 2), per WALKTHROUGH.md.

3. The one-ask-per-customer-ever rule lives in a database unique constraint —
   build it that way from the start, not as an app check.

4. When you need anything from a dashboard, give me a numbered click-path and
   wait. Never ask me to write code or config myself. Secrets in .env only.

5. Finish by walking me through the week-one checklist in WORKFLOW.md — including
   the "great work but the tap drips" complaint test — and don't call it done
   until CLAUDE.md's definition-of-done is fully checked.
```

## What you'll be doing while it builds

```
 YOU                                  CLAUDE CODE
 ───                                  ───────────
 answer 5 questions          ──▶      scaffolds + writes everything
 grab your g.page link       ──▶      wires the job-done trigger
 paste 2–3 keys into .env    ──▶      deploys
 close a fake job            ──▶      verifies the timed ask
 text back a fake complaint  ──▶      proves the safety net catches it
```

Total hands-on time: about 30 minutes of yours.

## If something goes sideways

Paste the error back into Claude Code and say "fix this, re-read CLAUDE.md
first." The kit files carry enough context that a fresh session can always
pick up where you left off.
CLAUDE.md · 4k chars
# CLAUDE.md — Automated Review Requests (SBW-002)

Drop this file in the root of your build folder. It is the standing brief for any AI assistant
(Claude Code or similar) working on this project. Read WORKFLOW.md for the full spec.

## Mission

Every completed job triggers one personalized SMS review ask, 1–3 hours after close.
An LLM triages every reply: happy → thank, unhappy → stop everything and alert the owner,
question → forward with a draft. One ask per customer, ever. Catch complaints before
they become public reviews.

## Architecture guts

```
 job-closed webhook ──▶ api/job-closed ──▶ suppression check ──▶ delay 1–3h ──▶ ask SMS
 inbound SMS        ──▶ api/inbound-sms ──▶ LLM triage ──▶ happy: auto-thank
                                                       ──▶ unhappy: HALT + owner alert
                                                       ──▶ question: forward + draft
                                                       ──▶ unsub: suppress forever
```

- The unique constraint on `customer_phone` in `review_asks` is what enforces
  one-ask-ever. The DB enforces it, not application memory.
- "Unhappy" is a circuit breaker: no thank-you, no future asks, owner alerted with
  `issue_summary`. This path must be impossible to skip.
- The personalization LLM call may fail; the static template is the fallback. An ask
  always goes out on time.

## The three questions

| Component | 1. Where does state live? | 2. Where does feedback live? | 3. What breaks if I delete it? |
|---|---|---|---|
| `review_asks` table | DB — unique phone = one-ask-ever | `review_left` + `saved_job` = ROI | dedupe gone → spam → CASL risk |
| `api/job-closed` | stateless; schedules + writes | trigger-to-ask latency | no asks ever fire |
| LLM triage (`lib/triage.ts`) | stateless; writes sentiment | weekly spot-check of replies vs labels | complaints reach Google instead of you |
| `prompts/*.md` | git — prompts are versioned code | conversion + misclassification rates | tone drifts, "ANY problem = unhappy" rule lost |
| suppression list | DB rows (unsub + do-not-ask) | complaint/opt-out rate | repeat asks, angry regulars |

## Hard rules

- One review ask per customer phone number, ever. Enforced by DB constraint.
- Sentiment "unhappy" halts all automation on that thread and alerts the owner. No
  automated message ever follows a complaint except the owner-approved one.
- Never gate (ask only happy customers) and never incentivize (discounts for reviews) —
  both violate Google policy and can nuke the profile.
- LLM apology drafts never offer compensation; that is an owner decision.
- STOP suppresses immediately and permanently.
- Secrets in env vars only. Customer data stays in our DB (PIPEDA).

## Folder structure

```
 review-engine/
 ├── CLAUDE.md            ← you are here
 ├── api/                 ← job-closed.ts, inbound-sms.ts — thin handlers
 ├── lib/                 ← ask.ts, triage.ts, log.ts — all logic lives here
 └── prompts/             ← personalize.md, triage.md — versioned like code
```

## Definition of done

- [ ] Test job close → ask arrives in the 1–3h window, correct name + job type
- [ ] Review link opens the Google review modal directly
- [ ] "great work but the tap drips" → classified unhappy, owner alerted, no thank-you
- [ ] "you guys were awesome" → auto thank-you, logged happy
- [ ] Second job, same customer → zero additional asks
- [ ] STOP → suppressed forever
- [ ] Owner can read review_asks and count reviews gained + complaints caught
SKILLS.md · 3k chars
# SKILLS.md — Automated Review Requests (SBW-002)

What you (or whoever builds this) need to know, and which skills to load into your AI
assistant before starting.

## Skill map

```
                       ┌──────────────────────┐
                       │  SBW-002 REVIEW ASKS │
                       └──────────┬───────────┘
          ┌──────────────┬───────┴───────┬────────────────┐
          ▼              ▼               ▼                ▼
   ┌─────────────┐ ┌────────────┐ ┌─────────────┐ ┌──────────────┐
   │ trigger     │ │ LLM triage │ │ small DB +  │ │ Google review│
   │ webhooks    │ │ (JSON out) │ │ suppression │ │ policy +CASL │
   │  REQUIRED   │ │  REQUIRED  │ │  REQUIRED   │ │   REQUIRED   │
   └─────────────┘ └────────────┘ └─────────────┘ └──────────────┘
```

## Required skills

| Skill | Why this build needs it | If you're using Claude Code |
|---|---|---|
| **Webhook triggers** | The whole system starts from "job closed" firing a webhook from your invoice/CRM/calendar tool. | Any webhook-handling skill; Make/n8n knowledge substitutes fine. |
| **LLM structured output** | Reply triage must return strict JSON with the "ANY problem = unhappy" rule baked in. | A `twilio-llm-intake` style skill — the SMS-to-structured-JSON pattern is identical. |
| **Database constraints** | One-ask-ever lives in a unique constraint, not in app code. | A `supabase-project-setup` style skill covers constraint patterns. |
| **Google review policy + CASL** | No gating, no incentives, instant opt-out — the rules that keep your profile alive. | Read the compliance section of WORKFLOW.md; no skill replaces it. |

## Suggested (not required)

| Skill | What it adds |
|---|---|
| **Prompt personalization** | Feeding job notes to the LLM so each ask names something real about the work. |
| **Transactional messaging patterns** | A `transactional-email-vendor-send` style skill if you also want an email fallback for customers without mobile numbers. |

## Workflows that pair with this one

```
  SBW-001 TEXT-BACK ──── rescued lead becomes a job ───▶ SBW-002 REVIEW ASKS
                                                              │
  SBW-003 VOICE AGENT ── booked call becomes a job ──────────┘
                          (same trigger, same log pattern)
```

- **SBW-001 Missed-Call Text-Back** — the lead you rescue today is the review you request
  next week. Same Twilio account, same logging discipline.
- **SBW-003 After-Hours AI Voice Receptionist** — jobs booked by the agent flow into the
  same job-closed trigger.

## Build order recommendation

1. Phase 1 with the static template — ship the same afternoon.
2. Watch one week of replies by hand; you'll see why the triage rules exist.
3. Add the LLM triage net, then the personalized asks.
WALKTHROUGH.md · 6k chars
# WALKTHROUGH.md — Automated Review Requests (SBW-002)

The build, step by step, in plain words. Each step tells you **what to do**,
**how long it takes**, and **how you know it worked**.

## Your board

```
 ┌─ TO DO ─────────┐  ┌─ BUILDING ──────┐  ┌─ TESTING ───────┐  ┌─ SHIPPED ───────┐
 │ 1 review link   │  │                 │  │                 │  │                 │
 │ 2 pick trigger  │  │  cards move     │  │  cards move     │  │  cards move     │
 │ 3 the ask text  │  │  this way ──▶   │  │  this way ──▶   │  │  this way ──▶   │
 │ 4 sent-list     │  │                 │  │                 │  │                 │
 │ 5 AI reply net  │  │                 │  │                 │  │                 │
 │ 6 owner alerts  │  │                 │  │                 │  │                 │
 └─────────────────┘  └─────────────────┘  └─────────────────┘  └─────────────────┘
```

## How the pieces hold hands

```
        ┌───────────┐
        │ job done  │
        └─────┬─────┘
              │ fires
              ▼
        ┌───────────┐  wait  ┌───────────┐
        │  trigger  │ ─────▶ │  the ask  │
        └───────────┘ 1–3h   └─────┬─────┘
                                   │ customer
                       ┌───────────┴───────────┐
                       ▼                       ▼
                 ┌───────────┐           ┌───────────┐
                 │ leaves a  │           │ texts     │
                 │ review ⭐  │           │ back      │
                 └─────┬─────┘           └─────┬─────┘
                       │                       ▼
                       │                 ┌───────────┐
                       │                 │ AI reads  │
                       │                 │ the reply │
                       │                 └─────┬─────┘
                       │                  ┌────┴─────┐
                       ▼                  ▼          ▼
                 ┌─────────────────────────┐   ┌──────────┐
                 │       review log        │   │ your     │
                 │  (asks, replies, wins)  │   │ phone    │
                 └─────────────────────────┘   └──────────┘
```

---

## Step 1 — Grab your review link (10 minutes)

**Do this:** Open Google Business Profile. Hit "Ask for reviews." Copy the short
link (it looks like `g.page/r/...`).

**You know it worked when:** opening the link on your phone shows the 5-star
review box directly — not a search page.

## Step 2 — Pick your trigger (30 minutes)

**Do this:** Choose the signal your business already makes when a job ends.
Best options, in order:

```
   1. invoice marked PAID   ← nobody forgets to invoice
   2. calendar event ends
   3. CRM status → "complete"
```

Connect it to a webhook (most invoice tools and CRMs have this built in, or
use Make/n8n to watch for it).

**You know it worked when:** closing a test job makes your function/scenario run.

## Step 3 — Send the ask (2 hours)

**Do this:** When the trigger fires, wait 1–3 hours, then send the message
template from WORKFLOW.md — customer name, job type, review link, and the
magic last line: *"if anything's not right, just reply here and we'll fix it."*

**You know it worked when:** your test phone gets a text that names the right
person and the right job.

## Step 4 — The sent-list (1 hour)

One rule: **one ask per customer, forever.**

**Do this:** Make a `review_asks` table (columns in WORKFLOW.md). Make the
phone number column unique. Check it before every send.

**You know it worked when:** closing two jobs for the same test customer sends
exactly one text.

## Step 5 — The AI reply net (half a day)

This is the part that saves your bacon.

**Do this:** Route inbound texts to the LLM with the triage prompt from
WORKFLOW.md. Act on the answer:

```
   "you guys were great"        → auto thank-you       😊
   "good but the tap drips"     → STOP + alert you     🚨
   "do you do furnaces too?"    → forward + draft      💬
   "stop texting me"            → suppress forever     ✋
```

**You know it worked when:** texting "great service but the tap still drips"
from your test phone makes your real phone buzz with the complaint — and no
cheerful thank-you goes out.

## Step 6 — Owner alerts (1 hour)

**Do this:** Every "unhappy" triage sends you: customer name, issue summary,
their number, and a suggested apology (no discounts — that's your call to make).

**You know it worked when:** the fake complaint reaches your pocket in under a
minute, ready to act on.

---

## After it ships — the weekly 10 minutes

```
   every Friday:
   ┌──────────────────────────────────────────────┐
   │ 1. check Google for new reviews → mark them  │
   │ 2. count: asks sent / reviews gained         │
   │ 3. count complaints caught privately         │
   │    (each one ≈ a 1-star that never happened) │
   │ 4. read 3 AI triage calls — still accurate?  │
   └──────────────────────────────────────────────┘
```

Reviews compound. Six months of this beats any ad budget a competitor can throw
at the map pack.