Part 4 of 7 · Newsletter composer series ~5 min read

How a newsletter draft reaches the owner

The composer marked an issue ready. Now the sender Lambda has to figure out who reviews it, on what channel, at what time of day, and with what attached. Get any of those wrong and the review is worse than useless: a draft sent to someone who left, a 2am ping, a draft that hides the one line nobody can back up. Four small guardrails sit between the finished draft and the review message landing — and the most important one makes sure the owner sees exactly what the model couldn’t source.

Key takeaways

  • Reviewer resolution: per-issue override beats the per-list default beats fallback to the configured admin.
  • Slack is the default review channel; email is the fallback if no Slack ID is configured.
  • Quiet hours defer the review message to the next available business hour.
  • The grounding check attaches the source item behind every paragraph and flags anything unsourced.
  • Every review message ships with the full draft, the sources, and the Approve, Edit, and Skip buttons.

Four guardrails on every review message

Four guardrails between the finished draft and the review message A horizontal flow diagram. On the far left, a "Draft ready" box: the composer produced a finished issue marked ready, with each paragraph tagged to the source item it came from. Four guardrail gates sit in a row to the right, each drawn as a vertical bar. Gate 1: Resolve reviewer — looks up the per-issue reviewer override first; if blank, falls back to the per-list default reviewer from the rules doc; if still blank, falls back to the configured admin. Returns a Slack member ID if one is set, otherwise an email address. Gate 2: Quiet hours — checks the local time against the rules-doc quiet-hours window; if outside business hours, defers the review message to the next available business minute via an EventBridge Scheduler one-off rule. Gate 3: Grounding check — walks each paragraph, attaches the source item behind it, and flags any sentence that the draft couldn't tie to an item, so the owner sees the gap in red before approving. Gate 4: Final compose — formats the review message with the full draft, the subject line, the per-paragraph sources, the list it would send to, and the count, then adds Approve, Edit, and Skip buttons. After all four gates pass, the review message ships via Slack for the resolved Slack ID, or via SES outbound for the resolved email. A note at the bottom: every review message is logged to DynamoDB so the next step knows the draft is awaiting sign-off. Draft ready full issue, each line tagged to a source Gate 1 Resolve reviewer issue override? list default? admin fallback? prefer Slack ID, fall back to email Gate 2 Quiet hours local time in business window? if not, defer via Scheduler Gate 3 Grounding check attach source behind each line flag any line with no item, show in red Gate 4 Compose + context full draft + subject line + sources, list, count, then three buttons Review message — Slack DM (preferred) or SES outbound email (fallback) chat.postMessage for Slack · SES SendRawEmail for inbox every review message logged to DDB nc-issues — awaiting sign-off Every gate is a deterministic check — and nothing is sent to the list until the owner approves.
Fig 4. Four guardrails between the finished draft and the review message. Resolve the reviewer. Honor quiet hours. Run the grounding check. Compose with the full draft and its sources. Then ship via Slack or email and log it so the system knows the draft is awaiting sign-off.

Gate 1: resolve the reviewer

Three places the sender Lambda looks for the reviewer of an issue, in order. First, a per-issue override in the rules doc — if this week’s issue is assigned to a specific person (say the founder is out and a colleague is covering), that person reviews it. Second, the per-list default in the rules doc (“the customer newsletter is reviewed by the marketing lead”). Third, the configured admin fallback — the person who set the composer up and gets every unrouted draft. The fallback should never fire in steady state; if it does, the monthly summary names it so the rules doc can be fixed.

Once the sender knows which person to ask, it looks up their delivery preference. The rules doc maps each reviewer to a Slack member ID if one is set, otherwise to an email address. Slack is preferred because a review with action buttons is faster than an email round-trip, and the draft sits right there in the thread. Email is the fallback so a draft never gets stuck waiting on a channel nobody checks.

Gate 2: quiet hours

The composer runs in the morning before the send day, so the first review message usually lands in business hours. But a redraft, a deferred run, or a manually triggered re-compose can finish later. Gate 2 reads the rules doc’s quiet-hours setting (default 6pm to 8am, configurable per business). If the current local time is in the quiet window, the sender creates a one-off EventBridge Scheduler rule that fires at the next business-hour minute and exits without sending. The Scheduler re-invokes the same sender Lambda with the same payload at the deferred time, where Gate 2 lets it through.

A review at 11pm is a review that gets read at 8am anyway — but now with a notification badge that’s been nagging all night. Deferring is kinder and costs nothing but a few minutes’ wait.

Gate 3: the grounding check (the one that matters most)

This is the guardrail that makes the whole system trustworthy. Because every paragraph in the draft was tagged with the id of the item it came from (Part 3), the sender can walk the draft line by line and pull up the exact item behind each one. The review message shows them side by side: the paragraph, and a small “from: [item title] [link]” under it. The owner can see at a glance that the line about the new feature came from the forwarded note, and the line about the client win came from the row someone typed in the sheet.

If any sentence has no matching source — which should be rare after the redraft step in Part 3, but never assume — it’s flagged in red with a plain warning: “This line isn’t backed by any item. Edit or remove it before sending.” The owner can’t miss it. A newsletter that quietly states something that didn’t happen is the one failure mode worth real care here, and this gate makes it visible instead of buried.

Gate 4: compose with full context, then ship

Gate 4 assembles the review message: the proposed subject line, the full draft as it would appear in the email, the per-paragraph sources from Gate 3, the name of the list it would go to, and the subscriber count. Below all of that sit three buttons — Approve, Edit, Skip. The whole point is that the owner can make a confident decision without leaving the message: read the issue, see where every claim came from, and act.

For Slack, the message is posted via chat.postMessage with Block Kit blocks so the buttons work. For email fallback, the same content is wrapped in a small HTML email, and the buttons become links that hit a Function URL to record the action. Every review message — Slack or email — writes a row to nc-issues in DynamoDB marking the issue as awaiting sign-off, with a pointer to the draft in S3. Part 5 picks up what happens when the owner taps one of those buttons.

Why the guardrails exist

None of these gates are exotic. They’re the care a thoughtful editor would take before handing a draft to the boss — check who’s actually reviewing, don’t interrupt them at 11pm, show your sources, and lay it all out so the decision is easy. Putting them in code as four small sequential gates makes them part of the design, not something you’re trusting any one weekly run to remember. And none of them send anything to the list — that only happens after the owner approves, which is the whole subject of the next post.

Next post: how an issue gets approved and sent — the three actions on the review message, the cooling-off window, and how the send, the items, and the audit trail all stay in sync.

All posts