Emails in a campaign
.email is a step. Your handler writes the subject and the body, as there's no default copy, and we send each lead one Email per step per run. Everything in Emails holds here: whether you may email someone, and what you say, is yours to decide. Read lead.consent and the clock in your handler, and return c.skip() when you shouldn't send.
export const renewals = defineCampaign('renewals')
.email(
(lead, c) =>
c.send({
subject: 'Your plan renews Friday',
text: `Hi ${lead.name}, your plan renews Friday. Acme, 1 Main St, Springfield.`,
}),
{ name: 'heads-up', from: 'sales@acme.com' },
)
.wait('2d')
.call(sales)
.onEmailReply((email, lead, c) => {
if (/unsubscribe|remove me/i.test(email.text ?? '')) return c.unsubscribe();
return c.assign(lead.fields.ownerRep);
});It goes from c.send's from, else the step's from, else your oldest Email address whose provider says it can send. Name one with its address, such as sales@acme.com. A lead with no email skips the step with no_email, and your handler never hears of it.
How an Email step ends
The lead moves on once your provider has taken it, not on delivery. lastOutcome says how: email_sent, email_failed, email_unknown, skipped or no_email. A bounce later doesn't move anyone, though it suppresses the address for the next .email step.
A send that may have reached your provider is never sent again, including after a crash, as each has a key per lead, step and run. A lead that started over gets a new Email.
| Refused because | The lead |
|---|---|
suppressed | Leaves as suppressed |
invalid_recipient | Leaves as removed |
too_large, too_many_recipients | Goes to the next step |
billing_*, usage_limit_reached, plan_has_no_seats, test_cap_reached | Waits, and the campaign pauses |
address_not_found, address_not_ready, provider_unavailable | Parks as step_failed, with the reason as lastOutcome |
rate_limited | Comes back in 5 minutes |
address_not_found is also what parks a lead when there's no address to send from at all. Fix the address, then retry the parked leads.
What an Email reply does
An Email from a lead reaches every campaign it's in, and goals and exits hear it as lead.email_received, with event.email. Only an Email that answers one this campaign sent, by its In-Reply-To or References headers and never by its subject, goes further.
| What came in | What happens |
|---|---|
| A one-click unsubscribe | It isn't an Email. The address is suppressed, and the next .email step takes the lead out as suppressed. |
| An out-of-office or other automatic answer | Goals and exits hear it, with event.email.autoReply. Nothing else does, and it ends nothing. |
| An answer to this campaign's Email | Goals and exits first. If neither ends it, .onEmailReply decides. With no .onEmailReply, the lead leaves as exit if the campaign emails. |
| Any other Email from the lead | Goals and exits only. |
.onEmailReply returns one of c.continue(), c.exit(), c.assign(repId) or c.unsubscribe(). With assign, the Rep owns the Thread and the lead leaves as exit. unsubscribe suppresses the sender's address, as manual, and the lead leaves as suppressed: a reply that says "please stop" is yours to read, and CAN-SPAM expects you to honor it.
Texts have .onReply, and Emails .onEmailReply, so each hook gets its own channel's message and actions. A campaign that does both can have both.
If .onEmailReply keeps failing, the default stands. While a campaign is paused, none of your code runs, and a person's answer still ends the sequence.