URL: /capabilities/keep-and-grow/review-requests · Id: `page:/capabilities/keep-and-grow/review-requests`

# 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.

A [customer](https://startupbuilder.dev/business/nouns/customer) who scored you 9 or 10 in, one short ask out with a link to the review site that needs reviews most.

```text title="Run it · Ask"
Ask every customer who rated us 9 or 10 for a review, spend up to 10¢, and show me the plan first.
```

```ts title="Run it · SDK"
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' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 0.05¢ an ask",
  "sample": {
    "customer": "Cedar Hollow Rentals",
    "score": "10",
    "venue": "Capterra",
    "sent": "yes",
    "published": "day 4"
  }
}
```

- **Kind:** Workflow
- **Price:** 0.05¢ per ask
- **Your time:** 6 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:retention`](https://startupbuilder.dev/workflows/processes/retention)

## 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. · [`noun:review`](https://startupbuilder.dev/business/nouns/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 as `Review.published`.

**Connect once:** [Google Business Profile](/code/integrations/google-business-profile). **How long:** Seconds.

## Trigger
It runs when a customer activates with a score of 9 or more.

```ts
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`](/code/apps/mailboxes/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

| 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`](/capabilities/keep-and-grow/review-requests) |
| Answer every review that lands, in my voice. | `Review.answer` |
| Ask the happiest ones for an introduction too. | [`Referral.ask`](/capabilities/keep-and-grow/referral-ask) |

```ts
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: 'Review.request'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [customer referrals](/plays/customer-referrals), [Marketplace listings](/plays/marketplace-listings), [Review requests](/plays/review-requests), [Written references](/plays/written-references).

It serves [Keep and grow customers](/capabilities/keep-and-grow), and runs in [Borrowed](/strategy/borrowed), [Keep and grow](/strategy/keep-and-grow). It deepens [`process:retention`](https://startupbuilder.dev/workflows/processes/retention).

## For agents

| 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, $) => customer.score >= 9 && $.Review.request({ customer: customer.$id }))` |
| 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 | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Check the rule | None | None. |
| 2. Pick the review site | None | None. |
| 3. Send the ask | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Check the rule | `code` | Code | Code checks each result against its declared type | `measured` | `$0.customer, $0.minScore` |
| 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, $2.venue` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | [Mailboxes](/code/apps/mailboxes) |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:message`](https://startupbuilder.dev/business/nouns/message) |
| Events | `Collection.queried`, `Message.scheduled` |

## Reference

| 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', 'capterra', 'google-business-profile']` | 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`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).