URL: /capabilities/keep-and-grow/saved-renewals · Id: `page:/capabilities/keep-and-grow/saved-renewals`

# Saved renewals

> At-risk accounts kept through their renewal, billed only for each one still paying 30 days past its renewal date.

At-risk accounts kept through their renewal, billed only for each one still paying 30 days past its renewal date.

```text title="Run it · Ask"
Save this month’s at-risk renewals, $120 for each one still paying 30 days later, and show me the plan first.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Renewal.save({ goal: 'Save this month’s at-risk renewals, $120 for each one still paying 30 days later, and show me the plan first.', budget: { amount: 1080, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About 7¢ per 10 flagged accounts",
  "bills": "$120 a saved renewal",
  "sample": {
    "account": "Willow Bend Rentals",
    "flagged": "usage down 46% over 4 weeks",
    "offer": "60-day pause",
    "renewed": "February 14",
    "state": "paying at day 30, accepted"
  }
}
```

- **Kind:** Service
- **Price:** $120 per saved renewal
- **Your time:** 25 minutes
- **Approval:** Runs on its own
- **Deepens:** [`process:retention`](https://startupbuilder.dev/workflows/processes/retention)
- **Default limits:** [What waits for you](/capabilities/agents#what-waits)

## What you get
- **10 flagged accounts**, as [`noun:subscription`](https://startupbuilder.dev/business/nouns/subscription) records
- `saved[]` · Each account still paying 30 days past its renewal: the clause that flagged it, the offer it took, and the payment that cleared.
- `lost[]` · Each flagged account that cancelled, with its reason; none is billed.
- `bill` · Accepted saves times the price.

**Connect once:** [Stripe](/code/integrations/stripe), [Gmail](/code/integrations/gmail). **How long:** 30 days before each renewal to 30 days after it.

## Trigger
It runs when you call it, or on any Trigger you set.

```ts
await $.Workflow.create({ verb: 'Renewal.save', trigger: { schedule: 'weekly' } })
```

## 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. List the renewals due in 30 days | `Collection.query` | Every subscription renewing inside the horizon, from Stripe | Free | Runs on its own |
| 2. Score each one by the risk rule, daily | [`Customer.score`](/capabilities/keep-and-grow/silence-watch) | Each account at risk or not, with the clauses that fired, before the save note goes | Free | Runs on its own |
| 3. Send each flagged account one save note | [`Customer.save`](/capabilities/keep-and-grow/save-and-win-back) | One note with their own numbers and the offer your Policy allows, within 24 hours of the flag | About 7¢ | Runs on its own |
| 4. Open the renewal on the saved terms | `Renewal.open` | The renewal set on the offer the account took: a pause, a downgrade or a credit | Free | Runs on its own |
| 5. Read the payment 30 days past the renewal | `Collection.query` | Each saved account `paying` when its renewal payment cleared and no refund followed in 30 days | Free | Runs on its own |

Calls: `Collection.query`, then `Customer.score`, then `Customer.save`, then `Renewal.open`, then `Collection.query`.

About 7¢ per 10 flagged accounts, each time it runs, capped at 50¢. Its own steps cost nothing; the compositions it orders add 7¢.

| How it runs | What it says |
| --- | --- |
| **Count only accounts the rule flagged first** | A save bills only when your declared risk rule flagged the account before the note went. An account that was renewing anyway is never a save. |
| **Price it against replacing the customer** | Replacing a lost $149-a-month customer costs up to the $795 that plan can afford to acquire one. $120 is 15% of that. |
| **Read paying at day 30, not a promise** | A reply that says “we will stay” bills nothing. The renewal payment clearing, with no refund in 30 days, does. |

## Example: give it a goal
> Save this month’s at-risk renewals, $120 for each one still paying 30 days later, and show me the plan first.

The plan it shows runs 5 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| The best save is a discount past your Policy | It is a `price` action and waits for you by the seat rule on `price`; the 24 hours pause while it waits. |
| A flagged account has an open support conversation | It is handed to a person with the context, never offered a deal over an open issue. |
| A saved account disputes the renewal charge | The save is credited and the dispute is handed to Billing. |

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-renewal-save-1-<step>-<item>`.

## What it promises
| Term | This Service |
| --- | --- |
| Done | An account your risk rule flagged before its renewal date is still paying 30 days after that date. |
| Counted by | `Renewal.saved` |
| Billed | $120 a saved renewal, on acceptance |
| Target | Every flagged account renewing in the month. |
| Accepted when | The renewal payment clearing with no refund in 30 days accepts it. |
| Credited | An account that refunds or disputes the renewal payment inside 30 days. |
| Remedy | A flagged account that cancels is never billed, and its reason is written to the customer for the next rule. |
| Evaluator | Code reads the flag time from Silence watch and the renewal invoice and any refund from Stripe. |
| Window | 30 days before each renewal date to 30 days after it. |
| Assurance | `measured` |
| Oversight | `none`: agents and code do every step |

You pay $120 for each saved renewal you accept. Behind it, the agents spend about 7¢ in calls for 10 flagged accounts, never more than 50¢, shown beside your bill. That spend is inside the $120: you are billed for accepted units only, never for the calls.

**February: 31 Leaseline renewals due, 9 flagged at risk.**

| Count | What happened |
| --- | --- |
| 9 | Ordered |
| 9 | Flagged by the risk rule and sent a save note: 4 on usage, 3 on logins, 2 on a failed payment. |
| 6 | Still paying 30 days past renewal: 3 on a pause, 2 on a downgrade, 1 after the fix. |
| 3 | Cancelled anyway: never billed, each reason written to the customer. |
| 1 | Disputed the renewal charge inside 30 days: credited. |
| **5** | **Accepted and billed: 5 × $120 = $600** |

## Change it
| Ask | Compiles to |
| --- | --- |
| Flag anyone with no login in 10 days instead of 14. | [`Customer.score`](/capabilities/keep-and-grow/silence-watch) |
| Offer a 90-day pause instead of 60. | [`Customer.save`](/capabilities/keep-and-grow/save-and-win-back) |
| Send me accounts paying over $500 a month instead of saving them. | [`Renewal.save`](/capabilities/keep-and-grow/saved-renewals) |

```ts
import { $, Service } from 'startup-builder'

export default Service({
  name: 'renewal-save',
  verb: 'Renewal.save',
  input: { rule: 'string', horizon: 'string', policy: 'string' },
  maxCost: 0.5,
  orders: ['Customer.score', 'Customer.save'],                               // each one's cost rolls up into this call
  oversight: { posture: 'none' },
  contract: {
    done: 'Renewal.saved', accept: { after: 'renewal', window: '30d', refund: 'none' }, assurance: 'measured',
    bills: { usd: 120, per: 'saved renewal' }, credits: ['refund-or-dispute-within-30d'], remedy: 'never-bill-a-loss',
  },
  execute: async (ctx, { rule, horizon = '30d', policy }) => {
    const due = await ctx.step('due', () => $.Collection.query({ from: 'subscription', where: `renewsAt <= now + ${horizon}` }))
    const scored = await ctx.step('score', () => $.Customer.score({ customers: due.map((s) => s.customer), rule }))
    for (const c of scored.atRisk) await ctx.step(`save:${c.customer}`, () => $.Customer.save({ customer: c.customer, policy }))
  },
})
```

Extend it with `extends: 'Renewal.save'` 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).

| Orders | Kind | Runs, each at its own price | Rolls up |
| --- | --- | --- | --- |
| [Silence watch](/capabilities/keep-and-grow/silence-watch) | Workflow | 30 × Free | Free |
| [Save and win back](/capabilities/keep-and-grow/save-and-win-back) | Agent task | 10 × About 0.7¢ a customer | About 7¢ |

It serves [Keep and grow customers](/capabilities/keep-and-grow). It deepens [`process:retention`](https://startupbuilder.dev/workflows/processes/retention).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Renewal.save` |
| Guide approval | `none` |
| Goal | Save this month’s at-risk renewals, $120 for each one still paying 30 days later, 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. |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-renewal-save-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

Nothing here waits for you: every step reads, or writes only your own records, up to the 50¢ cap.

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. List the renewals due in 30 days | `code` | Code | Code reads each renewal date from the subscription in Stripe | `measured` | `$0.horizon` |
| 2. Score each one by the risk rule, daily, by [Silence watch](/capabilities/keep-and-grow/silence-watch) | `code` | Code | Code scores usage by the declared rule and reads each flag back | `measured` | `$1.subscriptions, $0.rule` |
| 3. Send each flagged account one save note, by [Save and win back](/capabilities/keep-and-grow/save-and-win-back) | `agentic` | The [CSM](/capabilities/agents/csm) agent | A second model checks each note against the account’s record and your Policy | `graded` | `$2.atRisk, $0.policy` |
| 4. Open the renewal on the saved terms | `code` | Code | Code reads the subscription back from Stripe with the new terms | `measured` | `$3.accepted` |
| 5. Read the payment 30 days past the renewal | `code` | Code | Code reads the renewal invoice and any refund from Stripe at day 30 | `measured` | `$4.renewals` |

Code does steps 1, 2, 4 and 5; and the [CSM](/capabilities/agents/csm) agent does step 3. A second model checks the step it grades, never the one that wrote it, so the whole call is `graded`.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | None. |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:customer`](https://startupbuilder.dev/business/nouns/customer), [`noun:renewal`](https://startupbuilder.dev/business/nouns/renewal) |
| Events | `Collection.queried`, `Renewal.opened` |

## 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. |
| `horizon` | `string` | `'30d'` | How far ahead it reads renewal dates. |
| `policy` | `Policy` | `your retention Policy` | What a save may offer without asking: a pause up to 60 days, a downgrade, a fix, or a credit up to the Policy’s amount. |

Emits one Event named for its Verb. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).