email.no_reply event and add a
no_reply_expired entry to the conversation timeline.
You opt in by passing noReplyEventAfter on the send:
Why we built it
Every cold-outbound system on Earth has reinvented this clock. You send an email, you write down(leadId, sendId, fireAt) somewhere, you run
a worker that scans for due rows every minute, you cancel the row when a
reply lands, you wonder why the worker drifted by an hour, you discover
your scheduler doesn’t survive a deploy.
We already own the conversation. We already see the inbound. The natural
home for the timer is next to the conversation, not in your DB. So we
moved it. You stop running a scheduler.
The window
noReplyEventAfter accepts duration strings (LLM- and human-friendly)
or a raw number (interpreted as milliseconds).
| Input | Means |
|---|---|
"4h" | 4 hours |
"1d" | 1 day |
"3 days" | 3 days |
"90 minutes" | 90 minutes |
"1.5h" | 90 minutes |
14400000 | 4 hours (ms) |
1d if you omit the field. Floor is 1 minute — anything
shorter rounds up. Below 60 seconds the alarm churn isn’t useful (no
human replies to email in 30 seconds anyway).
Cancel and reset
The timer’s lifecycle is a small state machine and the rules are worth knowing:- A new outbound resets the timer. If you reply to your own thread before the recipient does (a “bumping” follow-up), the clock starts over from that send’s window.
- An inbound reply cancels the timer. No
email.no_replyevent fires; the conversation is now in a “they replied” state. - Each outbound-without-reply fires the event once. The event isn’t a recurring schedule. If you want repeated nudges, send another message — the next timer arms automatically.
noReplyAt is the future timestamp the alarm will fire at, or null
if no timer is currently armed. lastNoReplyAt is when the most
recent no_reply_expired event happened (or null if it never has).
When the event fires
The event payload is the standard shape with adata.waitedMs field
that tells you the actual elapsed time (which can be slightly longer
than the requested window because alarms aren’t preempted to the
millisecond):
Picking windows
A few rules of thumb that hold up across most outbound playbooks:- First follow-up: 2–4 days. Short enough that the original send is still in their head, long enough that they don’t feel rushed.
- Subsequent follow-ups: longer. Each step in a sequence should wait longer than the last. 3 → 7 → 14 days is a reasonable curve.
- Don’t go below an hour for outbound to humans. It looks desperate. The 1-minute floor exists for testing and for transactional use cases (like an OTP that didn’t land).
- Match the timezone roughly. A
1dtimer crosses one day boundary. If you sent at 11pm Friday recipient-local, a 1d timer fires Saturday night and the follow-up lands when nobody’s reading. Pick2dor3dfor evening sends if you don’t want the next message to land on the weekend.