# Business hours sending option

- **Pattern:** `ab-000607` (`campaign-orchestration-sequencing.cadence-spacing.business-hours`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000607
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000607)

## Why it matters

B2B sequences that fire at 3am local time or Saturday afternoon land in the recipient's inbox alongside spam and weekend cleanup, tanking open rates and conversion. Without a business-hours gate in the scheduler, computed send timestamps push cold outreach outside the 9-5 Monday-Friday window when decision-makers actually read email, degrading user experience for both senders configuring campaigns and recipients receiving off-hours pings that signal low-effort automation.

## Severity rationale

Low because the impact is reduced engagement and sender reputation drift, not data loss or outages.

## Remediation

Add a `send_in_business_hours` flag to the sequence schema and gate the step scheduler on it. Before calling `scheduleStep(sendAt)`, route the timestamp through an `advanceToBusinessHours(dt, timezone)` helper that checks day-of-week (skip 0=Sunday, 6=Saturday) and hour (clamp to 9-17), advancing to the next 9am weekday when outside the window. See `src/lib/scheduler/business-hours.ts`:

```ts
if (day === 6) local.setDate(local.getDate() + 2)
if (hour >= 17) { local.setDate(local.getDate() + 1); local.setHours(9, 0, 0, 0) }
```

## Detection

- **ID:** `business-hours`
- **Severity:** `low`
- **What to look for:** Check whether sequences can be configured to send only during business hours (e.g., 9am-5pm Monday-Friday). This is distinct from timezone support — it means the system can delay a send that falls on a weekend or outside working hours until the next business-hours window. Look for: a `send_in_business_hours` flag on sequences or steps, logic that checks if a computed send time falls within business hours before scheduling, and logic that advances the timestamp to the next business-hours window if it falls outside.
- **Pass criteria:** Sequences have a business-hours option. When enabled, sends scheduled outside 9am-5pm Mon-Fri are deferred to the start of the next business day. Count the enforcement points and enumerate all day/hour checks in the scheduler — at least 2 boundary conditions must be checked (start hour and end hour).
- **Fail criteria:** No business-hours setting. All sends are scheduled for the computed time regardless of day or hour. A configuration flag with no enforcement logic does not count as pass.
- **Skip (N/A) when:** The product sends to consumers (B2C) rather than businesses, where business-hours scheduling is not applicable.
- **Detail on fail:** `"No business-hours option — sequences always send at the computed time regardless of day of week or hour"` or `"business_hours flag exists on sequence but no enforcement logic found in step scheduler"`
- **Remediation:** Add a business-hours advancement function:

  ```ts
  function advanceToBusinessHours(dt: Date, timezone: string): Date {
    const { zonedTimeToUtc, utcToZonedTime } = require('date-fns-tz')
    let local = utcToZonedTime(dt, timezone)
    const day = local.getDay() // 0=Sun, 6=Sat
    const hour = local.getHours()

    if (day === 0) local.setDate(local.getDate() + 1) // Sunday → Monday
    if (day === 6) local.setDate(local.getDate() + 2) // Saturday → Monday
    if (hour < 9) local.setHours(9, 0, 0, 0)
    if (hour >= 17) { local.setDate(local.getDate() + 1); local.setHours(9, 0, 0, 0) }
    // Re-check after advancement
    const newDay = local.getDay()
    if (newDay === 0) local.setDate(local.getDate() + 1)
    if (newDay === 6) local.setDate(local.getDate() + 2)

    return zonedTimeToUtc(local, timezone)
  }
  ```

Taxons: user-experience

HTML version: https://auditbuffet.com/patterns/ab-000607
