Skip to content

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
Subscription.nudge
You, to your agent

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

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.

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

StepVerbReturnsPriceApproval
1. Read usage and the sibling offersCollection.queryUsage against each limit, and the offers one step upFreeRuns on its own
2. Choose the offer that fitsCollection.queryThe sibling offer whose limit covers 6 months of their growth, with the reasonAbout 0.2¢Runs on its own
3. Write the note with their numbersContent.draftTwo sentences: their count against the limit, and the next plan’s price differenceAbout 0.4¢Runs on its own
4. Send itMessage.scheduleThe note, onceAbout 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
WhenThen
The subscription got a note in the last 30 daysIt is skipped and nothing is billed.
No sibling offer covers their usageIt holds the note and tells you: the gap is a pricing question, not a message.
The note fails the gradeIt 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

AskCompiles 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
TypeScript
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: 'Subscription.nudge' 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
FactValue
GuideSubscription.nudge
Guide approvalnone
GoalNudge 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.
Triggeron.Subscription.thresholdCrossed((sub, $) => sub.usage / sub.limit >= 0.8 && $.Subscription.nudge({ subscription: sub.$id }))
RetryThree times with backoff, then the next route; the same idempotency key never charges, sends or posts twice
Idempotency keyleaseline-subscription-nudge-1
LookupsA 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
StepFunctionDoerCheckerAssuranceInputs
1. Read usage and the sibling offerscodeCodeCode checks each result against its declared typemeasured$0.subscription
2. Choose the offer that fitsagenticThe CSM agentA second model grades the outputgraded$1
3. Write the note with their numbersagenticThe CSM agentAdversarial review by a second model: Pass, or held: every number from their usage, the price from the offergraded$1.usage, $2.offer
4. Send itcodeCodeCode reads the result back from where it landedmeasured$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
KindWhat it touches
SystemsMailboxes, sites and content
NounsCollection, Content, Action, Message
EventsCollection.queried, Content.drafted, Action.evaluated, Message.scheduled

Reference

Inputs, outputs, its Event and its errors
NameTypeDefaultMeaning
subscriptionSubscriptionRequiredThe subscription that crossed the threshold.
abovenumber0.8The share of a plan limit that triggers the note.
cooldownstring'30d'No second note to the same subscription inside this.
maxCostnumber0.05The 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).

Contents