Part 4 of 7 · Weekly report builder series ~5 min read

How the report reaches the owner

The report is written and the numbers are checked. Now the send step has to put it in the right inbox, at the right local time, only when the week’s data is actually complete, and in a shape that reads well on a phone over coffee. Get any of those wrong and the report is worse than no report: a 2am email, a half-week of numbers because a source hadn’t synced yet, a wall of figures nobody scrolls past. Four small checks sit between the written report and the actual send.

Key takeaways

  • Recipient resolution: the configured owner, plus any extra readers listed in the config doc.
  • The Monday run fires in the owner’s timezone, so 7am means 7am where they are.
  • A completeness check holds the send if a source hasn’t synced for the full week yet.
  • The email leads with the summary, then the numbers table, then anything flagged.
  • Every send is logged so the run is auditable and next week’s comparison has a clean record.

Four checks on every send

Four checks between the written report and the inbox A horizontal flow diagram. On the far left, a "Report ready" box: the writer produced a checked report with the summary, the numbers table, and any flags. Four check gates sit in a row to the right, each drawn as a vertical bar. Gate 1: Resolve recipients — reads the configured owner email first, then any extra readers listed in the config doc (a co-owner, a bookkeeper); the owner is always included. Gate 2: Timezone — reads the owner's timezone from Parameter Store and confirms the run is firing at the configured local time (default 7am Monday); the EventBridge schedule itself runs in that timezone, so this gate is mostly a guard against a misconfigured run. Gate 3: Completeness — checks each source's last sync time against the week it should cover; if a source hasn't synced for the full week, the send is held and a one-off retry is scheduled for later that morning rather than mailing a half-week of numbers. Gate 4: Compose — formats the email: the plain-English summary at the top, the numbers table with this week, last week, and four-week-average columns, the Needs a look section if anything was flagged, and a footer link to the full source table via a Function URL. After all four gates pass, the report ships via SES outbound to the resolved recipients. A note at the bottom: every send is logged to DynamoDB so next week's comparison has a clean record of what was reported. Report ready summary, table, and any flags Gate 1 Resolve recipients owner email, always included extra readers? co-owner, bookkeeper Gate 2 Owner timezone read TZ from Parameter Store 7am Monday means 7am where they are Gate 3 Complete? all synced each source synced full week? if not, hold and retry later that AM Gate 4 Compose the email summary on top, then the table + flags, + link to full source via Function URL Send — SES outbound to the owner and any extra readers SES SendRawEmail · HTML report with the table inline every send logged to DDB wr-runs — next week’s comparison has a record Every check is a deterministic gate — no model calls, no surprise behavior on a quiet Monday.
Fig 4. Four checks between the written report and the inbox. Resolve the recipients. Honor the owner’s timezone. Hold if the week’s data isn’t complete. Compose with summary, table, and flags. Then send via SES and log the run so next week has a clean record.

Gate 1: resolve the recipients

The report is for the owner, but it’s often read by more than one person. The config doc lists the owner’s email and any extra readers — a co-owner who wants the same Monday view, a bookkeeper who likes to see the cash figures, a manager who runs a location. Gate 1 reads that list. The owner is always on it; the extras are opt-in. If the list is somehow empty (a config mistake), the report goes to the admin fallback rather than to nobody, and the run is logged with a note so the config can be fixed.

There’s no per-reader customization here on purpose — everyone gets the same report. The point of the system is one shared, trusted weekly view, not five slightly different ones that drift apart.

Gate 2: the owner’s timezone

“Every Monday morning” only means something if it’s the owner’s Monday morning. The EventBridge schedule that fires the run is set in the owner’s timezone, read from Parameter Store, so the default 7am send lands at 7am wherever the business actually is — not 7am UTC, which could be the middle of the night. Gate 2 is mostly a guard: it confirms the run really is firing at the configured local time and the timezone setting looks sane before anything goes out. The failure it prevents is the embarrassing one — a CI rotation quietly resetting the schedule to UTC and the owner getting their report at 2am.

Gate 3: is the week actually complete?

A report built from half the week’s data is worse than no report, because it looks authoritative while being wrong. Gate 3 checks each source’s last sync time against the week the report is supposed to cover. If every source has synced through Sunday, the send proceeds. If one hasn’t — the bank export didn’t land, the point-of-sale was offline Saturday night — the send is held, and a one-off EventBridge Scheduler rule retries it a couple of hours later that same morning. If a source is still missing by the final retry, the report goes out anyway, but that source is flagged in the Needs a look section as incomplete, so the owner knows a number is partial rather than assuming it’s the real total.

The trade-off favors a slightly late but complete report over a punctual but partial one. A report that’s an hour late is fine; a report that quietly understates sales because Saturday hadn’t synced is not.

Gate 4: compose, then send

The voice doc sets the shape of the email: the plain-English summary right at the top so the owner gets the gist in the preview pane, then the numbers table with this-week / last-week / four-week-average columns, then the Needs a look section if anything was flagged. The table is inline HTML so it reads on a phone without downloading anything. A footer link points to a Function URL where the owner can pull the full underlying rows for any figure — useful when a number surprises them and they want to see exactly which orders made it up.

Once composed, the report ships via SES outbound to the resolved recipients. The sender identity is a verified address on the business’s domain so the email lands in the inbox, not the spam folder. Every send — recipients, the figures reported, and the run timestamp — writes a row to wr-runs in DynamoDB. Next Monday’s run reads that row to build its “versus last week” comparison from exactly what was reported, not a recomputed guess.

Why the checks exist

None of these gates are clever. They’re the small care a thoughtful person would take if they were sending the report by hand — make sure it goes to the right people, don’t send it at 2am, don’t send it until the week’s actually finished, and lay it out so it’s readable in ten seconds. Putting them in code as four sequential checks makes them part of the system, not something you’re trusting yourself to remember at 7am every Monday for the next three years.

Next post: how a number that looks off gets flagged — the checks that run before the report is even written, and how a flagged figure is shown without ever being reported as fact.

All posts