Skip to content
All essays
WebJanuary 15, 202614 min

Email Marketing Automation: Building Drip Campaigns

Learn how to build sophisticated email marketing automation with drip campaigns, behavioral triggers, and personalized content

Ü
Ümit Uz
Mobile & Full Stack Developer

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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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.

Enjoyed this essay? Share it.

Related essays

Next essay
Remix Loaders: Server-Side Data Loading Made Simple