Upgrade note at the limit
A customer at 80% of a plan limit in, one note out with their numbers and the next plan’s price, at most once a month.
- Kind
- Agent task
- Price
- 0.65¢ per note
- Your time
- 7 minutes
- Approval
- Runs on its own
- Deepens
- Retention
Nudge customers near their unit limit toward the next plan, spend up to 50¢, and show me the plan first.
curl -X POST "https: //api.sb/ leaseline.app/ subscriptions/ nudge? mode= try" \
-H "Authorization: Bearer $SB_TOKEN" \
-H "Content-Type: application/ json" \
-d '{"goal": "Nudge customers near their unit limit toward the next plan, spend up to 50¢, and show me the plan first.", "budget": {"amount": 0.5, "currency": "USD"}}'
npx startup-builder do Subscription.nudge --try --data '{"goal": "Nudge customers near their unit limit toward the next plan, spend up to 50¢, and show me the plan first.", "budget": {"amount": 0.5, "currency": "USD"}}'
{
"name": "do",
"arguments": {
"mode": "try",
"code": "return await $.Subscription.nudge({ goal: 'Nudge customers near their unit limit toward the next plan, spend up to 50¢, and show me the plan first.', budget: { amount: 0.5, currency: 'USD' } })"
}
}
import { $ } from 'startup-builder'
const result = await $.try.Subscription.nudge({ goal: 'Nudge customers near their unit limit toward the next plan, spend up to 50¢, and show me the plan first.', budget: { amount: 0.5, currency: 'USD' } })
{
"mode": "try",
"stored": false,
"estimate": "About 0.65¢ a note",
"sample": {
"subscription": "Cedar Hollow Rentals",
"usage": "168 of 200 units",
"to": "the 400-unit plan",
"note": "You are tracking 168 of your 200 units. The 400-unit plan is $30 more a month.",
"sent": "yes"
}
}
What you get
- 1 note · A customer at 80% of a plan limit in, one note out with their numbers and the next plan’s price, at most once a month. · Subscription
to· The sibling offer the note proposes, and why it fits their usage.note· The note as sent: their count, the limit, the next plan and its price difference.sent· Whether it went out, or why it was held, such as a nudge 12 days ago.
Connect once: Stripe. How long: Seconds.
Trigger
It runs when a subscription’s usage crosses 80% of a plan limit.
on.Subscription.thresholdCrossed((sub, $) => sub.usage / sub.limit >= 0.8 && $.Subscription.nudge({ subscription: sub.$id }))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 usage and the sibling offers | Collection.query | Usage against each limit, and the offers one step up | Free | Runs on its own |
| 2. Choose the offer that fits | Collection.query | The sibling offer whose limit covers 6 months of their growth, with the reason | About 0.2¢ | Runs on its own |
| 3. Write the note with their numbers | Content.draft | Two sentences: their count against the limit, and the next plan’s price difference | About 0.4¢ | Runs on its own |
| 4. Send it | Message.schedule | The note, once | About 0.05¢ | Runs on its own |
Calls: Collection.query, then Content.draft, then Message.schedule.
About 0.65¢ a note, each time it runs, capped at 5¢.
Example: give it a goal
Nudge customers near their unit limit toward the next plan, spend up to 50¢, 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
| When | Then |
|---|---|
| The subscription got a note in the last 30 days | It is skipped and nothing is billed. |
| No sibling offer covers their usage | It holds the note and tells you: the gap is a pricing question, not a message. |
| The note fails the grade | It is rewritten once; the redo is free. |
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-subscription-nudge-1-<step>-<item>.
How it is graded
A second model checks the note against the customer’s usage and the offer’s price. It passes when every number matches their usage, the price matches the offer, and the note makes one proposal in two sentences. A miss is redone at no charge.
Change it
| Ask | Compiles to |
|---|---|
| Offer annual billing at the same time. | Subscription.nudge |
| Nudge at 90% instead of 80%. | Subscription.nudge |
| Send me the accounts over $500 a month instead. | Subscription.nudge |
import { $, workflow } from 'startup-builder'
export default workflow({
name: 'subscription-nudge',
verb: 'Subscription.nudge',
input: { subscription: 'string', above: 'number', cooldown: 'string' },
maxCost: 0.05,
grade: { evaluator: 'usage and the Offer’s price', passWhen: 'numbers match; one proposal; two sentences' , remedy: 'redo' },
execute: async (ctx, { subscription, above = 0.8, cooldown = '30d' }) => {
const s = await ctx.step('read', () => $.Collection.query({ from: 'subscription', id: subscription, with: ['usage', 'offer.siblings', 'lastNudge'] }))
if (s.lastNudge && within(s.lastNudge, cooldown)) return { sent: false, why: 'cooldown' }
const note = await ctx.step('write', () => $.Content.draft({ kind: 'upgrade-note', subscription: s, maxCost: 0.005 }))
await ctx.step('send', () => $.Message.schedule({ to: s.customer, body: note.body }))
return { to: note.offer, note: note.body, sent: true }
},
})Extend it with extends: and base(), or compose your own from these steps.
Works with
Plays that order it: Annual prepay, Portfolio cross-sell, Next order due, Product-led growth, Signal expansion, Single sign-on upgrade, Usage upsell.
It serves Keep and grow customers, and runs in Keep and grow. It deepens Retention.
For agents
For agents: the guide, its approvals, retries and lookups
| Fact | Value |
|---|---|
| Guide | Subscription.nudge |
| Guide approval | none |
| Goal | Nudge customers near their unit limit toward the next plan, spend up to 50¢, 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. |
| Trigger | on.Subscription.thresholdCrossed((sub, |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | leaseline-subscription-nudge-1 |
| Lookups | A find: input asks unless exactly one record fits |
| Step |
|---|
| 1. Read usage and the sibling offers |
| 2. Choose the offer that fits |
| 3. Write the note with their numbers |
| 4. Send it |
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 usage and the sibling offers | code | Code | Code checks each result against its declared type | measured | $0.subscription |
| 2. Choose the offer that fits | agentic | The CSM agent | A second model grades the output | graded | $1 |
| 3. Write the note with their numbers | agentic | The CSM agent | Adversarial review by a second model: Pass, or held: every number from their usage, the price from the offer | graded | $1.usage, |
| 4. Send it | code | Code | Code reads the result back from where it landed | measured | $3.passed |
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 | Mailboxes, sites and content |
| Nouns | Collection, Content, Action, Message |
| Events | Collection.queried, Content.drafted, Action.evaluated, Message.scheduled |
Reference
Inputs, outputs, its Event and its errors
| Name | Type | Default | Meaning |
|---|---|---|---|
subscription | Subscription | Required | The subscription that crossed the threshold. |
above | number | 0.8 | The share of a plan limit that triggers the note. |
cooldown | string | '30d' | No second note to the same subscription inside this. |
maxCost | number | 0.05 | 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 Subscription.nudged. Refuses with max-cost before it spends, and with the Data calls and Accounts groups (refusals).