Silence watch
Every morning, each customer scored on your declared risk rule, and anyone who went quiet flagged with why, before they cancel.
- Kind
- Workflow
- Price
- Free
- Your time
- 7 minutes
- Approval
- Runs on its own
- Deepens
- Silence watch
Tell me which customers went quiet this month, and spend nothing.
curl -X POST "https: //api.sb/ leaseline.app/ customers/ score? mode= try" \
-H "Authorization: Bearer $SB_TOKEN" \
-H "Content-Type: application/ json" \
-d '{"goal": "Tell me which customers went quiet this month, and spend nothing.", "budget": {"amount": 0, "currency": "USD"}}'
npx startup-builder do Customer.score --try --data '{"goal": "Tell me which customers went quiet this month, and spend nothing.", "budget": {"amount": 0, "currency": "USD"}}'
{
"name": "do",
"arguments": {
"mode": "try",
"code": "return await $.Customer.score({ goal: 'Tell me which customers went quiet this month, and spend nothing.', budget: { amount: 0, currency: 'USD' } })"
}
}
import { $ } from 'startup-builder'
const result = await $.try.Customer.score({ goal: 'Tell me which customers went quiet this month, and spend nothing.', budget: { amount: 0, currency: 'USD' } })
{
"mode": "try",
"stored": false,
"estimate": "Free",
"sample": {
"customer": "Cedar Hollow Rentals",
"last login": "16 days ago",
"usage": "units tracked down 45% in 4 weeks",
"flagged": "at risk",
"next": "Save and win back"
}
}
What you get
- 1 day · Every morning, each customer scored on your declared risk rule, and anyone who went quiet flagged with why, before they cancel. · Customer
scored· customers scored this morning.atRisk[]· Each customer who crossed the rule, with every reason, such as “no login in 16 days” and “units tracked down 45%”.events· An at-risk Event per flag, which starts Save and win back.
Connect once: Stripe. How long: Seconds.
Trigger
It runs every morning at 06:00.
every.day(($) => $.Customer.score({ customers: 'all' }))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 each customer’s usage | Metric.query | Logins, the product’s core count and payment status over 28 days, per customer | Free | Runs on its own |
| 2. Score by the declared rule | Collection.query | Each customer at risk or not, with the clauses that fired | Free | Runs on its own |
| 3. Flag each one at risk | Customer.update | The customer marked at risk, with a task for its owner and an at-risk Event | Free | Runs on its own |
Calls: Metric.query, then Collection.query, then Customer.update.
Free, each time it runs, capped at 0¢.
Example: give it a goal
Tell me which customers went quiet this month, and spend nothing.
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 usage feed is missing | It scores no one and opens a task to fix the feed, rather than flagging everyone. |
| A customer is already flagged | The flag is updated with today’s reasons, never doubled. |
| You call it twice in one day | The morning’s scores return: it is idempotent per day. |
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-score-1-<step>-<item>.
Change it
| Ask | Compiles to |
|---|---|
| Flag anyone who has not logged in for 10 days instead of 14. | Customer.score |
| Try to save each one it flags. | Customer.save |
| Send me the list once a week instead of every day. | Customer.score |
import { $, workflow } from 'startup-builder'
export default workflow({
name: 'silence-watch',
verb: 'Customer.score',
input: { rule: 'string', customers: "'all' | string[]" },
trigger: { schedule: 'daily at 06:00' },
maxCost: 0,
execute: async (ctx, { rule = 'retention.default', customers = 'all' }) => {
const usage = await ctx.step('usage', () => $.Metric.query({ metrics: ['logins', 'core-count', 'payment-failed'], by: 'customer', period: '28d', customers }))
const atRisk = usage.filter((u) => crosses(u, rule)) // the declared rule
for (const c of atRisk) await ctx.step(`flag: ${c.customer}`, () => $.Customer.update({ id: c.customer, risk: { at: true, why: c.reasons } }))
return { scored: usage.length, atRisk }
},
})Extend it with extends: and base(), or compose your own from these steps.
Works with
Plays that order it: Annual prepay, Next order due, Run it beside the old tool.
It serves Keep and grow customers, and runs in Keep and grow. It deepens Silence watch.
For agents
For agents: the guide, its approvals, retries and lookups
| Fact | Value |
|---|---|
| Guide | Customer.score |
| Guide approval | none |
| Goal | Tell me which customers went quiet this month, and spend nothing. 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 | every.day(($) => $.Customer.score({ customers: |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | leaseline-customer-score-1 |
| Lookups | A find: input asks unless exactly one record fits |
| Step |
|---|
| 1. Read each customer’s usage |
| 2. Score by the declared rule |
| 3. Flag each one at risk |
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 each customer’s usage | code | Code | Code checks each result against its declared type | measured | $0.customers |
| 2. Score by the declared rule | code | Code | Code checks each result against its declared type | measured | $1, |
| 3. Flag each one at risk | code | Code | Code reads the result back from where it landed | measured | $2.atRisk |
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 | Analytics and experiments |
| Nouns | Metric, Collection, Customer |
| Events | Metric.queried, Collection.queried, Customer.updated |
Reference
Inputs, outputs, its Event and its errors
| Name | Type | Default | Meaning |
|---|---|---|---|
rule | Policy | your retention Policy | The declared risk rule. The default: no login in 14 days, usage down 40% over 4 weeks, or a failed payment. |
customers | 'all' | Customer[] | 'all' | Who is scored. |
maxCost | number | 0.00 | 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.scored. Refuses with max-cost before it spends, and with the Data calls and Accounts groups (refusals).