Part 3 of 7 · Renewal negotiator series ~5 min read

How a renewal offer gets drafted

Once a day, an EventBridge Scheduler rule fires the drafter Lambda. The Lambda reads the registry, looks at one account at a time, computes how many days until the renewal, and decides whether to do nothing or to prepare an offer — and if so, which kind. The choice of plan and discount is plain Python reading your rules. Only after the numbers are fixed does the model get involved, and only to write the words. Every threshold and cap lives in the rules doc, where you can edit it without a deploy.

Key takeaways

  • The drafter runs once a day via EventBridge Scheduler and only acts on renewals inside the prepare-ahead window.
  • The plan and discount band come from your rules doc — tiers, caps, and floor prices live there, not in code.
  • Four moves per account: no offer, upgrade offer, loyalty offer, or right-size offer.
  • The model writes the email in your voice from the chosen plan and discount — it never sets a number.
  • Every drafted discount is checked against the cap before the draft is queued.

The decision flow, per account

Decision flow per account on every daily check A vertical decision flow diagram. At the top, an input box "Account from registry" with the account's name, plan, price, renewal date, usage, and history. Below that, a step "Compute days_to_renewal" — today's date in the configured timezone subtracted from the renewal date. Below that, a check "Already offered or skipped?" — if yes, route to "No offer" (do nothing this cycle). If no, continue. The next step "Look up plan menu and caps" — pulls the plan tiers, discount caps, and floor prices from the rules doc. The next step "Pick the move by usage and history" — plain Python reads the usage signal and history and chooses one of three offer shapes, or no offer if the renewal is still outside the prepare-ahead window. If usage is near the plan ceiling, route to "Upgrade offer." If usage is steady and the account is loyal, route to "Loyalty offer" within the cap. If usage is shrinking, route to "Right-size offer." After the move and discount are fixed, a final step "Draft in your voice + cap check" calls Bedrock to write the email and re-checks the discount against the cap before queueing. Each terminal box — No offer, Upgrade offer, Loyalty offer, Right-size offer — writes a draft (or a skip) to the approval queue with the account context. A note at the bottom: the rules doc holds every plan, cap, and floor; the model only writes the words around numbers the rules already chose. Account from registry plan · price · renewal · usage Step 1 Compute days_to_renewal renewal_date − today (TZ) Step 2 Already offered or skipped? read DDB rn-offers table Step 3 Look up plan menu + caps e.g. mid tier → cap 15% Step 4 Pick move by usage? outside window → no offer shrinking → right-size Step 5 Draft in voice + cap check Bedrock writes, cap re-checked No offer do nothing Upgrade offer near plan ceiling Loyalty offer steady, within cap Right-size offer shrinking usage if yes none shrink ceiling steady The rules doc holds every plan, cap, and floor — the model only writes the words around them.
Fig 3. The drafter’s decision tree, per account, per daily check. Five steps decide which of four moves applies. The rules doc holds every plan, cap, and floor; the drafter only enforces them and the model only writes the words.

Plans and caps live in the doc, not the code

The rules doc has one short section per tier. Each names the plans and the limits in plain prose: “Small accounts (under $100/month): loyalty discount up to 10%, never below the floor of $69. Mid accounts ($100–$400): up to 15%. Enterprise: route to a human, no auto-draft.” It also names the upgrade path for each plan (“Pro upgrades to Business at $240”) and the prepare-ahead window (“start drafting 30 days before the renewal date”). The numbers are yours; the drafter reads them and never exceeds them.

The caps exist for a reason. A loyalty discount is a tool, not a giveaway — a 10% cap on a small account protects your margin while still feeling generous. A floor price stops a stack of discounts from ever taking a plan below what it costs you to serve. And routing enterprise to a human keeps the highest-stakes negotiations where a person belongs. Per-account overrides exist too: the registry has an optional cap_override column for the one customer you’ve agreed special terms with.

Four moves, always

Every account inside the prepare-ahead window lands in exactly one of four buckets. The names are simple on purpose.

  • No offer. The renewal is still outside the window, or the account has already been offered or skipped this cycle, or the tier is one you route to a human. Do nothing. Most accounts, most days, are no-offer.
  • Upgrade offer. The usage signal is near the plan ceiling — seats nearly full, hours nearly used up. The rules pick the next plan and a discount inside the cap. The pitch frames the upgrade as the better-value option, not a price hike.
  • Loyalty offer. Steady usage, a loyal account, no upgrade needed. The rules pick a modest renewal discount inside the cap to thank them and keep them. The most common shape for healthy small accounts.
  • Right-size offer. The usage signal is shrinking — the customer is using less than they pay for and is a churn risk. The rules pick a smaller plan that fits their real usage, keeping the relationship instead of losing it to a cancel button. Counterintuitive, but a smaller paying customer beats a churned one.

Where the model comes in (and where it doesn’t)

By the time Step 5 runs, the plan and the discount are fixed numbers from the rules. The drafter passes them to Bedrock with the account context and the matching voice template, and asks for one thing: an email in your voice that presents this exact offer. The prompt is explicit — “Use only the plan and discount given. Do not invent prices, percentages, or terms. Write warmly, in the supplied voice, two short paragraphs.” Most accounts use Claude Haiku 4.5; a flagged-tricky account (a long history of back-and-forth, a sensitive note in the row) uses Claude Sonnet 4.6 for the heavier reasoning. The model returns the draft text only.

After the draft comes back, the drafter does one last deterministic thing: it re-reads the discount it asked for and confirms it’s still inside the cap and above the floor. If a number somehow drifted, the draft is rejected and the account is flagged for a human rather than queued. The check is cheap and it’s the backstop that makes “the model never sets the price” true in practice, not just in intent.

Why split the numbers from the words

The drafter could ask the model to pick the discount too. It doesn’t, for the same reason a good sales manager sets discount authority in advance: pricing is a commercial decision with margin consequences, and you want it to be the same every time for the same situation. A model deciding the discount introduces variance you can’t reason about and a customer could later dispute. Letting plain Python own the numbers and the model own the prose gives you both — consistent pricing and a warm, tailored email — with a clean line between the two.

Next post: how the finished draft reaches the owner — owner resolution, the discount-cap gate, quiet hours, and how the draft lands in the approval queue without ever touching the customer.

All posts