Email marketing automation delivers the right message to the right person at the right time. Unlike one-time broadcasts, automated drip campaigns nurture leads, onboard users, and drive conversions based on user behavior and time. This guide shows you how to build sophisticated email automation systems.
Understanding Email Automation
Types of Automated Campaigns
Welcome Series: Introduce new subscribers to your brand Onboarding Sequences: Guide new users through product features Abandoned Cart: Recover lost sales Re-engagement: Win back inactive users Nurture Campaigns: Educate prospects before purchase Birthday/Anniversary: Personal touchpoints
The Automation Framework
// lib/email-automation.ts
interface Trigger {
type: 'event' | 'time' | 'behavior';
condition: (context: any) => boolean;
}
interface EmailAction {
templateId: string;
delay?: number; // milliseconds
data?: (context: any) => any;
}
interface Automation {
id: string;
name: string;
trigger: Trigger;
steps: EmailAction[];
}
export class AutomationEngine {
private automations: Map<string, Automation> = new Map();
register(automation: Automation) {
this.automations.set(automation.id, automation);
}
async evaluate(triggerType: string, context: any) {
for (const automation of this.automations.values()) {
if (automation.trigger.type === triggerType) {
if (automation.trigger.condition(context)) {
await this.execute(automation, context);
}
}
}
}
private async execute(automation: Automation, context: any) {
for (const step of automation.steps) {
const data = step.data ? step.data(context) : {};
if (step.delay) {
setTimeout(async () => {
await sendEmail({
to: context.email,
templateId: step.templateId,
dynamicTemplateData: data,
});
}, step.delay);
} else {
await sendEmail({
to: context.email,
templateId: step.templateId,
dynamicTemplateData: data,
});
}
}
}
}Building a Welcome Series
Welcome Campaign Structure
// automations/welcome-series.ts
export const welcomeSeries: Automation = {
id: 'welcome-series',
name: 'New Subscriber Welcome',
trigger: {
type: 'event',
condition: (ctx) => ctx.event === 'user.subscribed',
},
steps: [
{
templateId: 'welcome-1',
delay: 0,
data: (ctx) => ({
firstName: ctx.firstName,
}),
},
{
templateId: 'welcome-2',
delay: 24 * 60 * 60 * 1000, // 1 day
data: (ctx) => ({
firstName: ctx.firstName,
bestFeatureLink: '${process.env.APP_URL}/features/best',
}),
},
{
templateId: 'welcome-3',
delay: 3 * 24 * 60 * 60 * 1000, // 3 days
data: (ctx) => ({
firstName: ctx.firstName,
tutorialLink: '${process.env.APP_URL}/tutorial',
}),
},
{
templateId: 'welcome-4',
delay: 7 * 24 * 60 * 60 * 1000, // 7 days
data: (ctx) => ({
firstName: ctx.firstName,
communityLink: '${process.env.APP_URL}/community',
}),
},
],
};Email Templates
// templates/welcome-series.ts
export const welcomeTemplates = {
'welcome-1': {
subject: 'Welcome to {{firstName}} - Let's Get Started!',
html: `
<h1>Hi {{firstName}},</h1>
<p>Welcome to our platform! We're thrilled to have you on board.</p>
<p>Here's what you can do next:</p>
<ul>
<li>Complete your profile</li>
<li>Explore our features</li>
<li>Join our community</li>
</ul>
<a href="{{appUrl}}/dashboard">Get Started →</a>
`,
},
'welcome-2': {
subject: 'Our Favorite Feature (You're Going to Love This)',
html: `
<h1>Hi {{firstName}},</h1>
<p>Now that you've signed up, we wanted to show you our most popular feature.</p>
<p>{{featureDescription}}</p>
<a href="{{bestFeatureLink}}">Try It Now →</a>
`,
},
'welcome-3': {
subject: 'Quick Tutorial for {{firstName}}',
html: `
<h1>Get the Most Out of {{productName}}</h1>
<p>We've put together a quick tutorial to help you get started.</p>
<a href="{{tutorialLink}}">Watch Tutorial →</a>
`,
},
'welcome-4': {
subject: 'Join Our Community, {{firstName}}!',
html: `
<h1>You're Not Alone</h1>
<p>Join thousands of other users in our community.</p>
<a href="{{communityLink}}">Join Community →</a>
`,
},
};Abandoned Cart Recovery
Cart Abandonment Detection
// automations/abandoned-cart.ts
export const abandonedCart: Automation = {
id: 'abandoned-cart',
name: 'Cart Abandonment Recovery',
trigger: {
type: 'behavior',
condition: async (ctx) => {
// Check if cart has items
const cart = await db.cart.findUnique({
where: { userId: ctx.userId },
include: { items: true },
});
if (!cart || cart.items.length === 0) return false;
// Check if cart was abandoned for > 1 hour
const lastUpdated = new Date(cart.updatedAt);
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
return lastUpdated < oneHourAgo;
},
},
steps: [
{
templateId: 'cart-abandoned-1',
delay: 60 * 60 * 1000, // 1 hour after abandonment
data: async (ctx) => {
const cart = await db.cart.findUnique({
where: { userId: ctx.userId },
include: {
items: {
include: { product: true },
},
},
});
return {
firstName: ctx.firstName,
cartItems: cart.items,
total: cart.items.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
),
checkoutUrl: '${process.env.APP_URL}/cart',
};
},
},
{
templateId: 'cart-abandoned-2',
delay: 24 * 60 * 60 * 1000, // 1 day
data: async (ctx) => {
// Same as above, but emphasize urgency
const cart = await db.cart.findUnique({
where: { userId: ctx.userId },
include: { items: { include: { product: true } } },
});
// Add discount
const discount = 10; // 10% off
return {
firstName: ctx.firstName,
discount,
checkoutUrl: '${process.env.APP_URL}/cart?discount=${discount}',
};
},
},
],
};Behavioral Triggers
User Behavior Tracking
// lib/behavior-tracking.ts
export async function trackBehavior(userId: string, event: string, properties?: any) {
await db.userBehavior.create({
data: {
userId,
event,
properties: JSON.stringify(properties || {}),
timestamp: new Date(),
},
});
// Evaluate automations
await automationEngine.evaluate('behavior', { userId, event, properties });
}
// Usage in app
await trackBehavior(userId, 'product.viewed', { productId: '123' });
await trackBehavior(userId, 'page.visited', { page: '/pricing' });
await trackBehavior(userId, 'feature.used', { feature: 'export' });Feature Adoption Campaign
// automations/feature-adoption.ts
export const featureAdoption: Automation = {
id: 'feature-adoption',
name: 'Feature Adoption',
trigger: {
type: 'behavior',
condition: async (ctx) => {
// Check if user visited feature page but didn't use it
const visited = await db.userBehavior.count({
where: {
userId: ctx.userId,
event: 'page.visited',
properties: { path: { contains: '/features/advanced' } },
},
});
const used = await db.userBehavior.count({
where: {
userId: ctx.userId,
event: 'feature.used',
properties: { feature: 'advanced' },
},
});
return visited > 0 && used === 0;
},
},
steps: [
{
templateId: 'feature-adoption-tip',
delay: 2 * 60 * 60 * 1000, // 2 hours
data: (ctx) => ({
firstName: ctx.firstName,
featureName: 'Advanced Analytics',
benefit: 'Get insights into your data',
tutorialLink: '${process.env.APP_URL}/tutorials/advanced-analytics',
}),
},
],
};Dynamic Segmentation
Segment Users by Behavior
// lib/segmentation.ts
export async function segmentUsers() {
const users = await db.users.findMany();
for (const user of users) {
const segments = [];
// Power users: > 10 sessions per week
const sessionsLastWeek = await db.userBehavior.count({
where: {
userId: user.id,
event: 'session.start',
timestamp: {
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
});
if (sessionsLastWeek > 10) {
segments.push('power-users');
}
// At-risk: No sessions in 14 days
const lastSession = await db.userBehavior.findFirst({
where: {
userId: user.id,
event: 'session.start',
},
orderBy: { timestamp: 'desc' },
});
if (lastSession && new Date(lastSession.timestamp) < new Date(Date.now() - 14 * 24 * 60 * 60 * 1000)) {
segments.push('at-risk');
}
// Free trial: Account < 30 days old, not upgraded
const accountAge = Date.now() - new Date(user.createdAt).getTime();
if (accountAge < 30 * 24 * 60 * 60 * 1000 && !user.hasUpgraded) {
segments.push('free-trial');
}
// Update user segments
await db.users.update({
where: { id: user.id },
data: { segments },
});
}
}Re-engagement Campaigns
Win Back Inactive Users
// automations/re-engagement.ts
export const reEngagement: Automation = {
id: 're-engagement',
name: 'Re-engagement Campaign',
trigger: {
type: 'behavior',
condition: async (ctx) => {
const lastActivity = await db.userBehavior.findFirst({
where: { userId: ctx.userId },
orderBy: { timestamp: 'desc' },
});
if (!lastActivity) return false;
const daysSinceActivity = (Date.now() - new Date(lastActivity.timestamp).getTime()) / (24 * 60 * 60 * 1000);
return daysSinceActivity > 30;
},
},
steps: [
{
templateId: 're-engagement-1',
delay: 0,
data: (ctx) => ({
firstName: ctx.firstName,
newFeatures: getNewFeaturesSinceLastVisit(ctx.userId),
}),
},
{
templateId: 're-engagement-2',
delay: 3 * 24 * 60 * 60 * 1000,
data: (ctx) => ({
firstName: ctx.firstName,
specialOffer: '50% off upgrade',
}),
},
],
};A/B Testing
Test Email Variations
// lib/email-ab-test.ts
export async function createABTest(
name: string,
templateA: string,
templateB: string,
trafficSplit = 0.5
) {
const test = await db.aBTest.create({
data: {
name,
templateA,
templateB,
trafficSplit,
status: 'running',
createdAt: new Date(),
},
});
return test.id;
}
export async function sendABTestEmail(userId: string, testId: string) {
const test = await db.aBTest.findUnique({ where: { id: testId } });
// Determine which version to send
const random = Math.random();
const version = random < test.trafficSplit ? 'A' : 'B';
const templateId = version === 'A' ? test.templateA : test.templateB;
await sendEmail({
to: userId,
templateId,
metadata: {
abTestId: testId,
version,
},
});
// Track assignment
await db.aBTestAssignment.create({
data: {
testId,
userId,
version,
},
});
}
export async function trackABTestConversion(emailId: string) {
const email = await db.email.findUnique({
where: { id: emailId },
include: { metadata: true },
});
if (email.metadata?.abTestId) {
await db.aBTestConversion.create({
data: {
testId: email.metadata.abTestId,
version: email.metadata.version,
emailId,
},
});
}
}Analytics and Reporting
Campaign Performance
// lib/campaign-analytics.ts
export async function getCampaignStats(automationId: string) {
const emails = await db.email.findMany({
where: { automationId },
include: {
analytics: true,
},
});
const total = emails.length;
const sent = emails.filter(e => e.status === 'sent').length;
const delivered = emails.filter(e => e.status === 'delivered').length;
const opened = emails.filter(e => e.analytics.some(a => a.event === 'open')).length;
const clicked = emails.filter(e => e.analytics.some(a => a.event === 'click')).length;
return {
total,
sent,
delivered,
deliveryRate: (delivered / sent) * 100,
opened,
openRate: (opened / delivered) * 100,
clicked,
clickRate: (clicked / delivered) * 100,
};
}Best Practices
1. Always Provide Value
Every email should offer something valuable: information, entertainment, or offers.
2. Respect Frequency
Don't overwhelm users. Space out emails appropriately.
3. Personalize Beyond Names
Use behavior, preferences, and purchase history to personalize content.
4. Mobile-Optimize
50%+ of emails are opened on mobile devices.
5. Test Continuously
A/B test subject lines, content, and send times.
6. Monitor Metrics
Track open rates, click rates, and conversions.
7. Clean Lists
Remove inactive subscribers to maintain deliverability.
Conclusion
Email marketing automation creates personalized experiences at scale. By implementing:
- Welcome series to engage new subscribers
- Behavioral triggers based on user actions
- Dynamic segmentation for targeted campaigns
- A/B testing to optimize performance
You'll see higher engagement, better conversion rates, and stronger customer relationships.
Remember: Automation doesn't mean set and forget. Continuously monitor performance, test new approaches, and refine your campaigns based on data.
The key is to deliver the right message at the right time. With these patterns, you're well-equipped to build sophisticated email automation that drives results.