Skip to content

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
Customer.save
You, to your agent

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' } })
Response
{
  "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"
  }
}
Try mode reads, stores nothing and spends nothingAPI referenceIts Nouns

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, credit or none, and the usage that chose it.
  • note · The note as sent, with the numbers from their own usage.
  • outcome · kept, paused or lost, 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.

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

StepVerbReturnsPriceApproval
1. Read why they are leavingCollection.queryThe cancel reason, 90 days of usage and their recent conversationsFreeRuns on its own
2. Choose the offer under the PolicyCollection.queryOne offer from the Policy that fits the usage, recorded as a Decision with its reasonAbout 0.2¢Runs on its own
3. Write the noteContent.draftA note with their own numbers and one offer, in two short paragraphsAbout 0.4¢Runs on its own
4. Send it, and one follow-up to silenceMessage.scheduleThe note sent within 24 hours, one follow-up after 5 days of silenceAbout 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
WhenThen
The customer has an open support conversationThey are handed to a person with the context, never offered a deal over an open issue.
The best fit is a discount past the PolicyThe 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 twiceOne 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

AskCompiles 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
TypeScript
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: 'Customer.save' 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
FactValue
GuideCustomer.save
Guide approvalnone
GoalTry 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.
Triggeron.Customer.atRisk((customer, $) => $.Customer.save({ customer: customer.$id }))
RetryThree times with backoff, then the next route; the same idempotency key never charges, sends or posts twice
Idempotency keyleaseline-customer-save-1
LookupsA 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
StepFunctionDoerCheckerAssuranceInputs
1. Read why they are leavingcodeCodeCode checks each result against its declared typemeasured$0.customer
2. Choose the offer under the PolicyagenticThe CSM agentA second model grades the outputgraded$1, $0.policy
3. Write the noteagenticThe CSM agentAdversarial review by a second model: Pass, or held: every number from their usage, the offer inside the Policygraded$1.usage, $2.offer
4. Send it, and one follow-up to silencecodeCodeCode reads the result back from where it landedmeasured$3.passed, $0.followUp

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
KindWhat it touches
SystemsMailboxes, sites and content
NounsCollection, Content, Action, Message
EventsCollection.queried, Content.drafted, Action.evaluated, Message.scheduled

Reference

Inputs, outputs, its Event and its errors
NameTypeDefaultMeaning
customerCustomerRequiredThe customer at risk, or the one who cancelled.
policyPolicyyour retention PolicyWhat may be offered without asking: a pause up to 60 days, a downgrade, a fix, or a credit up to the Policy’s amount.
followUpstring'5d'When a silent customer gets one follow-up.
maxCostnumber0.10The 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).

Contents