Review requests
A customer who scored you 9 or 10 in, one short ask out with a link to the review site that needs reviews most.
- Kind
- Workflow
- Price
- 0.05¢ per ask
- Your time
- 6 minutes
- Approval
- Runs on its own
- Deepens
- Retention
Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first.
curl -X POST "https: //api.sb/ leaseline.app/ reviews/ request? mode= try" \
-H "Authorization: Bearer $SB_TOKEN" \
-H "Content-Type: application/ json" \
-d '{"goal": "Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first.", "budget": {"amount": 0.1, "currency": "USD"}}'
npx startup-builder do Review.request --try --data '{"goal": "Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first.", "budget": {"amount": 0.1, "currency": "USD"}}'
{
"name": "do",
"arguments": {
"mode": "try",
"code": "return await $.Review.request({ goal: 'Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first.', budget: { amount: 0.1, currency: 'USD' } })"
}
}
import { $ } from 'startup-builder'
const result = await $.try.Review.request({ goal: 'Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first.', budget: { amount: 0.1, currency: 'USD' } })
{
"mode": "try",
"stored": false,
"estimate": "About 0.05¢ an ask",
"sample": {
"customer": "Cedar Hollow Rentals",
"score": "10",
"venue": "Capterra",
"sent": "yes",
"published": "day 4"
}
}
What you get
- 1 ask · A customer who scored you 9 or 10 in, one short ask out with a link to the review site that needs reviews most. · Review
venue· The review site asked for, and why: the fewest reviews of yours.sent· Whether the ask went out, or why not.published· The review’s URL once the site publishes it, mirrored back asReview.published.
Connect once: Google Business Profile. How long: Seconds.
Trigger
It runs when a customer activates with a score of 9 or more.
on.Customer.activated((customer, $) => customer.score >= 9 && $.Review.request({ customer: customer.$id }))Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.
| Step | Verb | Returns | Price | Approval |
|---|---|---|---|---|
| 1. Check the rule | Collection.query | Activated, scored at or above the line, and not asked in 180 days | Free | Runs on its own |
| 2. Pick the review site | Collection.query | The site with the fewest of your reviews, from the declared table | Free | Runs on its own |
| 3. Send the ask | Message.schedule | The fixed template with the site’s review link | About 0.05¢ | Runs on its own |
Calls: Collection.query, then Message.schedule.
About 0.05¢ an ask, each time it runs, capped at 1¢.
Example: give it a goal
Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first.
The plan it shows runs 3 steps, priced before it spends anything; Run it above shows the result it returns.
When something fails
Each failure, and what happens next
| When | Then |
|---|---|
| The customer was asked in the last 180 days | It is skipped and nothing is billed. |
| A site’s review link changes | The ask goes to the next site in the table and the link is flagged for you. |
| You call it twice for one customer | One ask per customer and site: it is idempotent. |
Each step retries three times, then falls through to the next route; one item's failure lands in skipped with its reason, never failing the call. Before a step would cross maxCost it stops and returns what it has, with a resume link. Each step's idempotency key is the call's key plus the step and the item: leaseline-review-request-1-<step>-<item>.
Change it
| Ask | Compiles to |
|---|---|
| Ask after their first renewal instead. | Review.request |
| Answer every review that lands, in my voice. | Review.answer |
| Ask the happiest ones for an introduction too. | Referral.ask |
import { $, workflow } from 'startup-builder'
export default workflow({
name: 'review-request',
verb: 'Review.request',
input: { customer: 'string', minScore: 'integer', venues: 'string[]' },
trigger: { event: 'Customer.activated' },
maxCost: 0.01,
execute: async (ctx, { customer, minScore = 9, venues = ['g2', 'capterra', 'google-business-profile'] }) => {
const c = await ctx.step('rule', () => $.Collection.query({ from: 'customer', id: customer, with: ['score', 'lastReviewAsk'] }))
if (c.score < minScore || within(c.lastReviewAsk, '180d')) return { sent: false }
const venue = fewestReviews(venues) // the declared table
return ctx.step('send', () => $.Message.schedule({ to: customer, template: 'review-ask', vars: { venue } }))
},
})Extend it with extends: and base(), or compose your own from these steps.
Works with
Plays that order it: customer referrals, Marketplace listings, Review requests, Written references.
It serves Keep and grow customers, and runs in Borrowed, Keep and grow. It deepens Retention.
For agents
For agents: the guide, its approvals, retries and lookups
| Fact | Value |
|---|---|
| Guide | Review.request |
| Guide approval | none |
| Goal | Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | on.Customer.activated((customer, |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | leaseline-review-request-1 |
| Lookups | A find: input asks unless exactly one record fits |
| Step |
|---|
| 1. Check the rule |
| 2. Pick the review site |
| 3. Send the ask |
The amounts live once, on the hub (approvals).
Who does each step, and how it is checked
| Step | Function | Doer | Checker | Assurance | Inputs |
|---|---|---|---|---|---|
| 1. Check the rule | code | Code | Code checks each result against its declared type | measured | $0.customer, |
| 2. Pick the review site | code | Code | Code checks each result against its declared type | measured | $0.venues |
| 3. Send the ask | code | Code | Code reads the result back from where it landed | measured | $1, |
The doer is never the checker, and the weakest assurance on the path is the whole call’s.
Under the hood
Under the hood: the Systems, Nouns and Events it touches
| Kind | What it touches |
|---|---|
| Systems | Mailboxes |
| Nouns | Collection, Message |
| Events | Collection.queried, Message.scheduled |
Reference
Inputs, outputs, its Event and its errors
| Name | Type | Default | Meaning |
|---|---|---|---|
customer | Customer | Required | The customer to ask. |
minScore | integer | 9 | The score they gave, 0 to 10, at or above which they are asked. |
venues | VenueId[] | ['g2', | Where your buyers read reviews; the one with the fewest of yours goes first. |
cooldown | string | '180d' | No second ask inside this. |
maxCost | number | 0.01 | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a resume link. |
Emits Review.requested. Refuses with max-cost before it spends, and with the Data calls and Accounts groups (refusals).