Skip to content

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

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

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.

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

StepVerbReturnsPriceApproval
1. Read each customer’s usageMetric.queryLogins, the product’s core count and payment status over 28 days, per customerFreeRuns on its own
2. Score by the declared ruleCollection.queryEach customer at risk or not, with the clauses that firedFreeRuns on its own
3. Flag each one at riskCustomer.updateThe customer marked at risk, with a task for its owner and an at-risk EventFreeRuns 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
WhenThen
The usage feed is missingIt scores no one and opens a task to fix the feed, rather than flagging everyone.
A customer is already flaggedThe flag is updated with today’s reasons, never doubled.
You call it twice in one dayThe 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

AskCompiles 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
TypeScript
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: 'Customer.score' 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
FactValue
GuideCustomer.score
Guide approvalnone
GoalTell 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.
Triggerevery.day(($) => $.Customer.score({ customers: 'all' }))
RetryThree times with backoff, then the next route; the same idempotency key never charges, sends or posts twice
Idempotency keyleaseline-customer-score-1
LookupsA 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
StepFunctionDoerCheckerAssuranceInputs
1. Read each customer’s usagecodeCodeCode checks each result against its declared typemeasured$0.customers
2. Score by the declared rulecodeCodeCode checks each result against its declared typemeasured$1, $0.rule
3. Flag each one at riskcodeCodeCode reads the result back from where it landedmeasured$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
KindWhat it touches
SystemsAnalytics and experiments
NounsMetric, Collection, Customer
EventsMetric.queried, Collection.queried, Customer.updated

Reference

Inputs, outputs, its Event and its errors
NameTypeDefaultMeaning
rulePolicyyour retention PolicyThe 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.
maxCostnumber0.00The 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).

Contents