91 lines
2.2 KiB
Markdown
91 lines
2.2 KiB
Markdown
# Scheduling & Timing
|
||
|
||
## Campaign duration
|
||
|
||
| Setting | Value |
|
||
|---------|-------|
|
||
| Recommended | 30–45 days |
|
||
| Minimum | 14 days (not recommended for new domains) |
|
||
| Maximum | 45 days (MVP cap) |
|
||
|
||
`endsAt = startedAt + durationDays`
|
||
|
||
## Daily send distribution
|
||
|
||
### Send windows
|
||
|
||
Avoid off-hours sends that signal automation.
|
||
|
||
| Parameter | Default |
|
||
|-----------|---------|
|
||
| Timezone | Campaign or mailbox TZ |
|
||
| Window start | 08:00 |
|
||
| Window end | 18:00 |
|
||
| Weekends | Optional: reduce volume 50% or pause |
|
||
|
||
### Jitter between sends
|
||
|
||
| Parameter | Default | Rationale |
|
||
|-----------|---------|-----------|
|
||
| Min gap | 45 minutes | Avoid burst patterns |
|
||
| Max gap | 180 minutes | Stay within daily quota |
|
||
| Distribution | Uniform random | Human-like variance |
|
||
|
||
```
|
||
nextSendAt = lastSendAt + random(45min, 180min)
|
||
if nextSendAt outside window: roll to next window start
|
||
```
|
||
|
||
## 48-hour volume cap
|
||
|
||
Industry guidance: never increase average daily volume more than **20% in 48 hours**.
|
||
|
||
```typescript
|
||
function cap48hIncrease(history: number[], proposed: number): number {
|
||
const avg48h = average(last48h(history));
|
||
return Math.min(proposed, Math.ceil(avg48h * 1.20));
|
||
}
|
||
```
|
||
|
||
Applies to all recipes except explicit expert override in Custom (with warning).
|
||
|
||
## Cron vs queue architecture
|
||
|
||
### Current (MVP)
|
||
|
||
- `node-cron` fires dispatcher every 15 min
|
||
- All eligible sends in one batch
|
||
|
||
### Target
|
||
|
||
| Component | Role |
|
||
|-----------|------|
|
||
| Scheduler cron | Every 5 min: enqueue due sends |
|
||
| Send queue | BullMQ / pg-boss / SQLite queue |
|
||
| Worker | One send per job with jitter |
|
||
| Rescuer cron | Every 10 min: IMAP scan per mailbox |
|
||
|
||
Benefits: precise timing, backoff, idempotency keys, pause per campaign.
|
||
|
||
## Campaign day rollover
|
||
|
||
At midnight (campaign TZ):
|
||
|
||
1. Increment `currentDay`
|
||
2. Load `DailySchedule` for new day
|
||
3. Reset `actualSends`, `actualReplies` counters
|
||
4. If `currentDay > durationDays` → complete or maintenance mode
|
||
|
||
## Maintenance mode (post-campaign)
|
||
|
||
After primary campaign completes:
|
||
|
||
- Continue Flat recipe at **20% of maxEmailsPerDay**
|
||
- Keeps engagement signals during live cold outreach
|
||
- Operator can disable
|
||
|
||
## Timezone handling
|
||
|
||
- Store all timestamps UTC in DB
|
||
- Display in campaign timezone in UI
|
||
- Send windows evaluated in campaign timezone
|