How a price move gets noticed
On each page’s schedule, an EventBridge Scheduler run fires the checker Lambda. The Lambda fetches the page politely, reads the price with that page’s saved rule, and writes the reading to history. Then the watcher looks at the new reading next to the last one, computes the change, and decides whether to stay quiet or to send an alert — and if so, which kind. The whole decision is plain Python. No model. No guessing. Every threshold lives in the rules doc, where a rep can edit it without a deploy.
Key takeaways
- The checks run on a staggered schedule via EventBridge Scheduler — gentle, never a burst.
- Each page has a move threshold in the rules doc — default 5%, with a bigger “big swing” band at 2× that.
- Four moves per page, every check: steady, first alert, repeat move, big swing.
- DynamoDB holds the last reading and last-alert state so the same move isn’t sent twice.
- The watcher itself never calls a model. The decision is entirely deterministic.
The decision flow, per page
Thresholds: 5% isn’t magic, it’s in the doc
The rules doc has one short section per product line. Each section names the threshold in plain prose: “Kettles: alert on any move of 5% or more; treat 10% or a stock change as a big swing. Premium blenders: alert on 3%; they barely move. Clearance lines: alert on 8%; they bounce around.” The first number is the move threshold — the smallest change worth an alert. The bigger number is the big-swing band — a jump large enough to flag as urgent the moment it happens.
The thresholds exist for a reason. A 5% move on a high-margin product is worth a glance. A 1% move is noise — rounding, a coupon, a regional tweak — and alerting on it just trains the owner to ignore the system. A big swing (a price halving, or a product going out of stock) usually means a real event — a clearance, a supply problem, a deliberate undercut — and deserves to jump the queue.
Per-page overrides exist too. The watch list sheet has an optional column called threshold_override. Type a percentage there and the watcher uses your number instead of the product-line default for that one row. This is the right escape hatch for the one product where even a 2% move matters because the margins are razor-thin.
Four moves, always
Every page, every check, lands in exactly one of four buckets. The names are simple on purpose.
- Steady. The price didn’t move past the threshold, or the page is muted. Do nothing. Most pages, most checks, are steady.
- First alert. The price crossed the move threshold and no alert has gone out for this move yet. Send a fresh alert with full context. Write a row to the
pm-alertsDynamoDB table marking that this move has fired. - Repeat move. The price moved again in the same direction since the last alert. Send a follow-up that names the previous reading so the owner sees the trend, not a fresh surprise. Write the new alert to
pm-alerts. - Big swing. A large jump — past the big-swing band — or a stock-state flip. Send it flagged as urgent, marked so dispatch can let it past the daily cap. A big swing usually means a real pricing event, so it’s one of the few cases worth interrupting an owner for.
State that makes the decision deterministic
The watcher reads two DynamoDB tables every check. pm-readings holds the price history: (page_id, ts) with the price, currency, and stock flag. pm-alerts records every alert that’s gone out: (page_id, move, alert_date, dispatched_via). With those two tables, the move-decision logic is a few dozen lines of Python and zero magic. A given page with a given new reading, a given threshold, and a given history always produces the same move. Re-running a check produces no extra alerts (because the state in DDB shows what already fired).
A muted page is an explicit row in a third table, pm-mute, with a mute_until timestamp. The watcher reads it in the “page muted?” check and treats the page as steady until the mute ends. Part 5 covers muting in detail.
Why the check uses no model
The watcher could call a model on each check to decide whether a move is “interesting”. It doesn’t. Two reasons. First, the check should be the one part of the system that is utterly predictable — if the rules doc says alert at 5% and the price moved 7%, the alert fires. A model in that loop introduces variance the team can’t reason about. Second, model calls cost money, and most pages most checks are steady, so the call would be wasted nine times out of ten.
Bedrock fires elsewhere — when a page layout changes and the saved rule stops finding the price (the re-read lane from Part 2), and on the monthly summary mentioned in Part 6. Not on the routine check. The watcher itself is plain Python that reads a number and compares it to history.
Next post: how an alert finds the right person, how quiet hours and a daily cap are honored, and what context rides along with every alert.
All posts