Save and win back
A customer at risk in, and within 24 hours one note out with the offer your Policy allows that fits their usage.
- Kind
- Agent task
- Price
- 0.7¢ per customer
- Your time
- 13 minutes
- Approval
- Runs on its own
- Deepens
- Save and win back
Try to save the five customers who went quiet, spend up to $1, and show me the plan first.
curl -X POST "https: //api.sb/ leaseline.app/ customers/ save? mode= try" \
-H "Authorization: Bearer $SB_TOKEN" \
-H "Content-Type: application/ json" \
-d '{"goal": "Try to save the five customers who went quiet, spend up to $1, and show me the plan first.", "budget": {"amount": 1, "currency": "USD"}}'
npx startup-builder do Customer.save --try --data '{"goal": "Try to save the five customers who went quiet, spend up to $1, and show me the plan first.", "budget": {"amount": 1, "currency": "USD"}}'
{
"name": "do",
"arguments": {
"mode": "try",
"code": "return await $.Customer.save({ goal: 'Try to save the five customers who went quiet, spend up to $1, and show me the plan first.', budget: { amount: 1, currency: 'USD' } })"
}
}
import { $ } from 'startup-builder'
const result = await $.try.Customer.save({ goal: 'Try to save the five customers who went quiet, spend up to $1, and show me the plan first.', budget: { amount: 1, currency: 'USD' } })
{
"mode": "try",
"stored": false,
"estimate": "About 0.7¢ a Customer",
"sample": {
"customer": "Cedar Hollow Rentals",
"offer": "pause",
"note": "You tracked 4 renewals in September and none since. Pause for 60 days instead of cancelling? ",
"outcome": "paused"
}
}
What you get
- 1 customer · A customer at risk in, and within 24 hours one note out with the offer your Policy allows that fits their usage. · Customer
offer·pause,downgrade,fix,creditornone, and the usage that chose it.note· The note as sent, with the numbers from their own usage.outcome·kept,pausedorlost, when they answer or the follow-up window ends.
Connect once: Stripe, Gmail. How long: Minutes to send; waits 5 days for an answer.
Trigger
It runs on every at-risk flag from Silence watch, and every cancellation.
on.Customer.atRisk((customer, $) => $.Customer.save({ 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. Read why they are leaving | Collection.query | The cancel reason, 90 days of usage and their recent conversations | Free | Runs on its own |
| 2. Choose the offer under the Policy | Collection.query | One offer from the Policy that fits the usage, recorded as a Decision with its reason | About 0.2¢ | Runs on its own |
| 3. Write the note | Content.draft | A note with their own numbers and one offer, in two short paragraphs | About 0.4¢ | Runs on its own |
| 4. Send it, and one follow-up to silence | Message.schedule | The note sent within 24 hours, one follow-up after 5 days of silence | About 0.1¢ | Runs on its own |
Calls: Collection.query, then Content.draft, then Message.schedule.
About 0.7¢ a customer, each time it runs, capped at 10¢.
Example: give it a goal
Try to save the five customers who went quiet, spend up to $1, and show me the plan first.
The plan it shows runs 4 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 has an open support conversation | They are handed to a person with the context, never offered a deal over an open issue. |
| The best fit is a discount past the Policy | The offer is a price action: it waits for you, by the seat rule on price, and the 24 hours pause while it waits. |
| One customer is flagged twice | One offer per flag: it is idempotent per customer and flag Event. |
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-customer-save-1-<step>-<item>.
How it is graded
A second model checks each note against the customer’s record and your retention Policy; code times the offer from the flag. It passes when each at-risk or cancelling customer gets a note with an offer inside the Policy within 24 hours, and every fact in it matches their record. A miss is free: you pay nothing for it.
Change it
| Ask | Compiles to |
|---|---|
| Offer a 90-day pause instead of 60. | Customer.save |
| Send me anyone paying over $200 a month instead. | Customer.save |
| Tell me each week what reasons people give. | Metric.query |
import { $, workflow } from 'startup-builder'
export default workflow({
name: 'customer-save',
verb: 'Customer.save',
input: { customer: 'string', policy: 'string', followUp: 'string' },
trigger: { event: 'Customer.atRisk' },
maxCost: 0.1,
review: { pattern: 'adversarial', rubric: 'every number from their usage; the offer inside the Policy' },
grade: { evaluator: 'the note against the record and the Policy; time to offer' , passWhen: 'an offer in the Policy within 24 hours', remedy: 'free' },
execute: async (ctx, { customer, policy = 'retention.default', followUp = '5d' }) => {
const c = await ctx.step('read', () => $.Collection.query({ from: 'customer', id: customer, with: ['usage:90d' , 'conversations', 'cancelReason'] }))
if (c.conversations.some((x) => x.open)) return ctx.step('handoff', () => $.Conversation.answer({ conversation: c.conversations[0].$id, to: 'owner' }))
const offer = pickWithinPolicy(c, policy) // a choice inside declared bounds
const note = await ctx.step('write', () => $.Content.draft({ kind: 'save-note', customer: c, offer, maxCost: 0.005 }))
await ctx.step('send', () => $.Message.schedule({ to: customer, body: note.body, followUp: { after: followUp, ifNoReply: true } }))
return { offer, note: note.body }
},
})Extend it with extends: and base(), or compose your own from these steps.
Works with
Plays that order it: Run it beside the old tool.
It serves Keep and grow customers, and runs in Keep and grow. It deepens Save and win back.
For agents
For agents: the guide, its approvals, retries and lookups
| Fact | Value |
|---|---|
| Guide | Customer.save |
| Guide approval | none |
| Goal | Try to save the five customers who went quiet, spend up to $1, 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.atRisk((customer, |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | leaseline-customer-save-1 |
| Lookups | A find: input asks unless exactly one record fits |
| Step |
|---|
| 1. Read why they are leaving |
| 2. Choose the offer under the Policy |
| 3. Write the note |
| 4. Send it, and one follow-up to silence |
The amounts live once, on the hub (approvals).
Who does each step, and how it is checked
| Step | Function | Doer | Checker | Assurance | Inputs |
|---|---|---|---|---|---|
| 1. Read why they are leaving | code | Code | Code checks each result against its declared type | measured | $0.customer |
| 2. Choose the offer under the Policy | agentic | The CSM agent | A second model grades the output | graded | $1, |
| 3. Write the note | agentic | The CSM agent | Adversarial review by a second model: Pass, or held: every number from their usage, the offer inside the Policy | graded | $1.usage, |
| 4. Send it, and one follow-up to silence | code | Code | Code reads the result back from where it landed | measured | $3.passed, |
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, sites and content |
| Nouns | Collection, Content, Action, Message |
| Events | Collection.queried, Content.drafted, Action.evaluated, Message.scheduled |
Reference
Inputs, outputs, its Event and its errors
| Name | Type | Default | Meaning |
|---|---|---|---|
customer | Customer | Required | The customer at risk, or the one who cancelled. |
policy | Policy | your retention Policy | What may be offered without asking: a pause up to 60 days, a downgrade, a fix, or a credit up to the Policy’s amount. |
followUp | string | '5d' | When a silent customer gets one follow-up. |
maxCost | number | 0.10 | 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 Customer.saved. Refuses with max-cost before it spends, and with the Data calls and Accounts groups (refusals).