URL: /capabilities/keep-and-grow/silence-watch · Id: `page:/capabilities/keep-and-grow/silence-watch`

# Silence watch

> Every morning, each customer scored on your declared risk rule, and anyone who went quiet flagged with why, before they cancel.

Every morning, each [customer](https://startupbuilder.dev/business/nouns/customer) scored on your declared risk rule, and anyone who went quiet flagged with why, before they cancel.

```text title="Run it · Ask"
Tell me which customers went quiet this month, and spend nothing.
```

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

```json title="Run it · 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"
  }
}
```

- **Kind:** Workflow
- **Price:** Free
- **Your time:** 7 minutes
- **Approval:** Runs on its own
- **Deepens:** [`workflow:silence-watch`](https://startupbuilder.dev/workflows/processes/retention/silence-watch)

## 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. · [`noun:customer`](https://startupbuilder.dev/business/nouns/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](/code/integrations/stripe). **How long:** Seconds.

## Trigger
It runs every morning at 06:00.

```ts
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`](/code/apps/analytics-and-experiments/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

| 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`](/capabilities/keep-and-grow/silence-watch) |
| Try to save each one it flags. | [`Customer.save`](/capabilities/keep-and-grow/save-and-win-back) |
| Send me the list once a week instead of every day. | [`Customer.score`](/capabilities/keep-and-grow/silence-watch) |

```ts
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](/code/compose) from these steps.

## Works with
**Plays that order it:** [Annual prepay](/plays/annual-prepay), [Next order due](/plays/next-outcome), [Run it beside the old tool](/plays/run-beside).

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

## For agents

| 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: 'all' }))` |
| 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 | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Read each customer’s usage | None | None. |
| 2. Score by the declared rule | None | None. |
| 3. Flag each one at risk | None | None. |

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

| 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, $0.rule` |
| 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

| Kind | What it touches |
| --- | --- |
| Systems | [Analytics and experiments](/code/apps/analytics-and-experiments) |
| Nouns | [`noun:metric`](https://startupbuilder.dev/business/nouns/metric), [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:customer`](https://startupbuilder.dev/business/nouns/customer) |
| Events | `Metric.queried`, `Collection.queried`, `Customer.updated` |

## Reference

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