Delivery Rules — Email + SMS Timing

For agents: These are the timing rules that govern when automated email and SMS sends actually fire. They apply to the initial audit delivery email, every email + SMS in the post-audit drip sequence (follow-up-sequence), and the makeover-delivery sequence (delivery-sequence). Internal-facing channels (Slack notifications, HelpScout drafts) are NOT gated by these rules.

Initial audit delivery

  • Minimum 4-hour delay between when analysis completes (Gate 2 passes) and when the audit ships.
  • If the 4-hour mark falls outside the time-of-day window below, delivery is pushed forward to the next valid window slot.
  • Admin override: “Deliver Now” bypasses the delay AND the window. Use sparingly.

Time-of-day window

  • Sends only between 8:04 AM Eastern and 6:00 PM Eastern.
  • Window opens at 8:04 AM (not 8:00) so sends don’t land on round 5-minute marks.
  • Within the day, sends fire at random seconds inside each tick to look less mechanical.

Days the system never sends

  • Sunday
  • New Year’s Day (January 1)
  • Independence Day (July 4)
  • Thanksgiving (4th Thursday of November)
  • Christmas Eve (December 24)
  • Christmas Day (December 25)

Minimum 4-hour gap between same-medium sends

Shipped 2026-05-04. Prevents email/SMS bunching when blocked-day deferrals collide with natural-day rows.

  • Rule: at least 4 hours must pass between any two same-medium sends to the same church (email-to-email, or SMS-to-SMS).
  • Applies to all sequence types: main, comms_dept, confirmation, makeover_delivery.
  • Mechanism: gate runs in processRow (functions/src/sequences/processQueue.js) between the v2-eligibility check and the delivery-window check. Reads most-recent same-medium send from the church doc’s existing emailsSent[] / smsSent[] arrays — no extra Firestore reads. When the gap is too small, scheduledFor is pushed forward to (lastSent + 4h) and lastDeferredReason: 'min_gap_per_medium' + lastDeferredAt are stamped.
  • Idempotent: before the new scheduledFor matures, the cron simply doesn’t pick the row up. After it matures, the gap check passes and normal flow resumes.
  • Tunable via constant MIN_GAP_PER_MEDIUM_MS at the top of processQueue.js.
  • Test mode bypasses the gate — 14-min compressed validation runs need all messages to fire end-to-end.
  • Behavioral implication for follow-up-sequence: the “7-day” label is illustrative — Sundays/holidays + the 4h gap can stretch elapsed time slightly past 7 calendar days. bonusExpiresAt = deliveredAt + 7d does NOT stretch; bonus-deadline copy remains accurate to the original 7-day commitment. In rare worst-case scenarios (multiple consecutive blocked days), Email 6/7 could fire after bonus expiry; copy in those steps shouldn’t reference the deadline as “tomorrow” in absolute terms.

Test mode

  • Submitting an audit with ?test=1 bypasses the 4-hour delay AND the time-of-day window — the audit ships immediately so you can verify the full flow without waiting.
  • The post-audit follow-up sequence timeline also compresses 720× in test mode (a 7-day sequence runs in ~14 minutes).
  • The makeover-delivery sequence has its own testMode: true flag that compresses its 7-day timeline 720× — see delivery-sequence § Test Mode.
  • Caveat: while convenient for end-to-end testing, test mode means you can’t verify the timing rules themselves on a test submission. To verify timing, submit a real audit and watch the schedule.

Scope — what’s gated, what isn’t

Channel / surfaceGated by these rules?
Initial audit delivery emailYes
Post-audit drip emails (1–7 of follow-up-sequence)Yes
Post-audit drip SMS (1–3 of follow-up-sequence)Yes
The Comms Dept branch emails / SMS (Part Three of follow-up-sequence)Yes
Makeover-delivery emails (1–6 of delivery-sequence)Yes
Makeover-delivery SMS (1–3 of delivery-sequence)Yes
Slack notifications (admin-facing)No — fire immediately
HelpScout drafts (admin-facing)No — fire immediately

Source of truth

  • Time-of-day window + 4-hour delivery delay + blackout days: functions/src/audit/deliveryWindow.js
  • 4-hour minimum gap between same-medium sends: functions/src/sequences/processQueue.js (constant MIN_GAP_PER_MEDIUM_MS, helper lastSendMsForMedium, gate in processRow)

When the runtime rules change, update this doc to match.