How a follow-up gets timed
Once a day, at 9am local time, an EventBridge Scheduler rule fires the timer Lambda. The Lambda reads the quote list, looks at one row at a time, computes how many days since you sent it and how many days until it expires, and decides whether to do nothing or to draft a nudge — and if so, which kind. The whole decision is plain Python. No model. No vector retrieval. Every threshold lives in the rules doc, where a rep can edit it without a deploy.
Key takeaways
- The timer runs once a day via EventBridge Scheduler at 9am local time.
- The cadence lives in the rules doc — for example a first nudge at 3 days, a second at 7, and a last call a few days before expiry.
- Four moves per quote, every check: resting, first nudge, follow-up nudge, last call.
- DynamoDB tracks last-nudge and reply state per quote so the same step never fires twice.
- The timer itself never calls a model. The decision is entirely deterministic.
The decision flow, per quote
The cadence: day 3, day 7, last call isn’t magic, it’s in the doc
The rules doc has one short section that names the cadence in plain prose: “Standard quotes: nudge at 3 days and 7 days after sending, then a last call 3 days before the quote expires. High-value quotes over $20,000: a gentler 5/12 cadence so it never feels like pressure. Small quotes under $500: a single nudge at 4 days, then stop.” The numbers are days since you sent the quote. The first number is the first nudge. The last-call step is keyed to the expiry date instead — it fires a few days before the quote lapses, whatever the sent date was.
The cadence exists for a reason. A nudge at day 3 catches the quote that simply got buried in an inbox. A nudge at day 7 catches the customer who meant to reply and forgot. A last call before expiry is the honest “this price is about to change” reminder that often turns a maybe into a yes. Different deal sizes deserve different tempos; the cadence reflects that.
Per-quote overrides exist too. The quote sheet has an optional column called cadence_override. Type a comma-separated list of days there and the timer uses your numbers instead of the default for that one row. This is the right escape hatch for the customer who told you on the call “give me two weeks.”
Four moves, always
Every quote, every check, lands in exactly one of four buckets. The names are simple on purpose.
- Resting. The quote isn’t due for a nudge yet, or the customer has already replied. Do nothing. Most quotes, most days, are resting.
- First nudge. The first cadence step just came due and there’s no reply yet. Draft a friendly check-in. Write a row to the
qf-nudgesDynamoDB table marking that the first step has fired. - Follow-up nudge. A later cadence step came due without a reply. Draft a short, warm follow-up that gently references the earlier message so the customer doesn’t feel cold-emailed twice. Write the new nudge to
qf-nudges. - Last call. The quote is inside the last-call window before its expiry, still with no reply. Draft a gentle “this quote is set to expire on the 30th — happy to extend it if you need more time” note. Mark it in DynamoDB. This is the final message in the chain; after it, the quote rests until it expires and gets closed out.
State that makes the decision deterministic
The timer reads two DynamoDB tables every check. qf-nudges records every nudge that’s gone out: (quote_id, step_index, nudge_date, sent_via). qf-reply records every customer reply: (quote_id, reply_date, outcome). With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given quote with a given sent date, a given cadence, and a given reply/nudge history always produces the same move. Re-running the check produces no extra nudges (because the state in DDB shows what already fired).
When a customer replies, that’s an explicit stop: a row in qf-reply moves the quote to resting forever. Part 5 covers exactly what each kind of reply does.
Why the daily check uses no model
The timer could call a model on the check to decide whether to nudge at all, or to judge the tone of the moment. It doesn’t. Two reasons. First, the daily check should be the one part of the system that is utterly predictable — if the rules doc says nudge at day 7 and there’s no reply, the nudge is queued. A model in that loop introduces variance the team can’t reason about. Second, model calls cost money, and most days most quotes are resting, so the call would be wasted nine days out of ten.
Bedrock fires elsewhere — on the inbox lane in Part 2, on drafting the nudge wording in Part 4, and on reading the reply in Part 5. Not on the daily check. The timer itself is plain Python that reads a doc and writes events.
Next post: how a quote nudge reaches the buyer, how a draft gets written in your voice and approved, and how quiet hours and weekends are honored.
All posts