How a shipping update gets sent
Every 30 minutes during the day, an EventBridge Scheduler rule fires the notifier Lambda. The Lambda reads the order list, looks at one row at a time, compares the order’s current status to the last status it told the customer about, and decides whether to do nothing or to send an update — and if so, which one. The whole decision is plain Python. No model. No guessing. Every setting lives in the rules doc, where the owner can edit it without a deploy.
Key takeaways
- The notifier runs on a schedule via EventBridge Scheduler, every 30 minutes during the day.
- It compares each order’s current status to the last status it actually told the customer about.
- Five moves per order, every check: nothing, shipped, out for delivery, delivered, delayed.
- DynamoDB tracks the last status sent per order so the same update never goes out twice.
- The notifier itself never calls a model. The decision is entirely deterministic.
The decision flow, per order
Status stages: placed, shipped, out for delivery, delivered, delayed
Every order moves through a small, fixed set of stages. Placed is the start — the customer has ordered but nothing has shipped, so there’s nothing new to say. Shipped means the parcel is with the carrier and on its way. Out for delivery means it’s on a vehicle and arriving today. Delivered means it’s at the door. Delayed is the off-path stage: the order passed its expected delivery date without arriving. Each of these (except placed) has its own friendly update in the voice doc.
The stages exist for a reason. A “shipped” note sets the customer’s expectation and gives them a tracking link. An “out for delivery” note tells them to keep an eye out today, which cuts the “is it coming?” question. A “delivered” note closes the loop and is a nice moment to say thanks. Different stages do different jobs; the updates reflect that.
Per-order overrides exist too. The order list has an optional column called mute_until. Put a date there and the notifier holds all updates for that one order until the date passes — useful for a pre-order that shipped early, or an order you’re handling by hand.
Five moves, always
Every order, every check, lands in exactly one of five moves. The names are simple on purpose.
- Nothing. The status hasn’t changed since the last update, or the customer has unsubscribed, or the order is muted. Do nothing. Most orders, most checks, are nothing.
- Shipped. The status just became shipped and no “on its way” email has gone out. Send the shipped update with the tracking link. Write a row to the
sn-sendsDynamoDB table marking that the shipped update fired. - Out for delivery. The status became out for delivery and no “arriving today” email has gone out. Send that update. Write the send to
sn-sends. - Delivered. The status became delivered and no “it arrived” email has gone out. Send the delivered update with a friendly thank-you. Write the send to
sn-sends. - Delayed. The order passed its expected delivery date without a delivered status. Warn the customer and alert the owner. Mark the order as delayed in DynamoDB. Part 5 covers this move in full.
State that makes the decision deterministic
The notifier reads two DynamoDB tables every check. sn-sends records every update that’s gone out: (order_id, status, sent_date, sent_via). sn-prefs records each customer’s preference: (order_id, unsubscribed, mute_until). With those two tables, the move-decision logic is a few dozen lines of Python and zero guesswork. A given order with a given status and a given send history always produces the same move. Re-running the check produces no extra emails (because the state in DynamoDB shows what already fired).
This is why the same order never gets two “shipped” emails. Carriers sometimes report the same status twice, or the webhook fires a retry. The notifier doesn’t care: it checks sn-sends first, sees the shipped update already went out, and lands at nothing.
Why the scheduled check uses no model
The notifier could call a model on the check to write a smarter update, or to decide whether to send at all. It doesn’t. Two reasons. First, the check should be the one part of the system that is utterly predictable — if the status moved to shipped and no shipped email went out, the email fires. A model in that loop introduces variance the team can’t reason about. Second, model calls cost money, and most orders on most checks have nothing new, so the call would be wasted.
Bedrock fires elsewhere — on the inbox forwarding lane in Part 2, and on the monthly summary mentioned in Part 6. Not on the scheduled check. The notifier itself is plain Python that reads a list and writes events.
Next post: how a message reaches the customer, how quiet hours and unsubscribes are honored, and what context every update carries.
All posts