Part 3 of 7 · Quote follow-up series ~5 min read

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

Decision flow per quote on every daily check A vertical decision flow diagram. At the top, an input box "Quote from the list" with the row's customer, amount, sent date, expiry, owner, and DynamoDB state. Below that, a step "Compute days_since_sent and days_to_expiry" — today's date in the configured timezone compared against the sent date and the expiry date. Below that, a check "Already replied?" — if yes, route to "Resting" (do nothing this check). If no, continue. The next step "Look up cadence from the rules doc" — pulls the cadence; for example a standard quote returns nudge at days 3, 7, and a last call 3 days before expiry. The next step "Find the cadence step now due" — iterates the cadence and picks the first step where days_since_sent reached the threshold or the quote is within the last-call window. If none, route to "Resting." If one, look at the DynamoDB state to see whether a nudge has already gone out for this step. If no nudge yet for this step, route to "First nudge" (or "Follow-up nudge" if an earlier step has already fired). If the due step is the last-call window before expiry, route to "Last call" — draft a gentle this-is-set-to-expire note. Each terminal box — Resting, First nudge, Follow-up nudge, Last call — emits an event to EventBridge with the move and the quote context. A note at the bottom: the rules doc holds every threshold; the timer's code only enforces them — change the cadence in the doc and tomorrow's check uses the new value. Quote from the list customer · amount · sent · expiry Step 1 Compute the day counts since_sent & to_expiry Step 2 Already replied? read DDB qf-reply table Step 3 Look up the cadence e.g. day 3, day 7, last call Step 4 Which step is due? none → resting near expiry → last call Step 5 Already nudged this step? read DDB qf-nudges table Resting do nothing First nudge first step due Follow-up nudge a later step Last call near expiry, no reply if yes none expiry first later step The rules doc holds every threshold — change the cadence and tomorrow’s check uses the new value.
Fig 3. The timer’s decision tree, per quote, per daily check. Five steps decide which of four moves applies. The rules doc holds every threshold; the timer only enforces them.

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-nudges DynamoDB 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