Wraps Logo
Developer Experience10 min readWraps Team

Email Templates as React,Workflows as TypeScript

Write email templates as React components and automation workflows as TypeScript. Version-controlled, type-safe, and code-reviewable—deployed to your AWS account.

React Email templates
TypeScript workflows
Git-native version control

The Problem

Most email platforms force you into a GUI editor for templates and a drag-and-drop builder for workflows. This means no version control, no type safety, no code review, and no way to test changes before they go live.

  • Template changes can't be reviewed in a PR
  • No way to roll back a bad deploy besides manual restore
  • Workflow logic lives in a UI you can't grep or test
  • Variables are strings with no type checking
  • Collaboration means "don't edit while I'm editing"

Wraps takes a different approach: templates are React components, workflows are TypeScript files. Your email infrastructure lives in your repo, reviewed in PRs, deployed with your code.

Templates as React Components

Email templates are React components using React Email. You get JSX, props with TypeScript types, shared brand constants, and component composition. The CLI compiles them to HTML and uploads to SES.

import { Html, Head, Body, Container, Text, Button, Hr } from '@react-email/components';
import brand from '../brand';

export const subject = 'Welcome to {{companyName}}!';
export const emailType = 'transactional' as const;
export const testData = { name: 'Jane', activationUrl: 'https://yourapp.com/activate' };

type WelcomeProps = {
  name: string;
  activationUrl: string;
};

export default function Welcome({ name, activationUrl }: WelcomeProps) {
  return (
    <Html>
      <Head />
      <Body style={{ backgroundColor: brand.backgroundColor }}>
        <Container style={{ maxWidth: 600, margin: '0 auto' }}>
          <Text style={{ fontSize: 24, color: brand.textColor }}>
            Welcome, {name}!
          </Text>
          <Text style={{ color: brand.textColor }}>
            Thanks for signing up. Click below to activate your account.
          </Text>
          <Hr />
          <Button
            href={activationUrl}
            style={{
              backgroundColor: brand.primaryColor,
              color: '#fff',
              padding: '12px 24px',
              borderRadius: 6,
            }}
          >
            Activate Account
          </Button>
        </Container>
      </Body>
    </Html>
  );
}
import { defineBrand } from '@wraps.dev/client';

export default defineBrand({
  primaryColor: '#7C3AED',
  secondaryColor: '#6366f1',
  backgroundColor: '#FAFAFA',
  textColor: '#4B5563',
  fontFamily: 'system-ui, -apple-system, sans-serif',
  buttonStyle: 'rounded',
  buttonRadius: '6px',
  companyName: 'YourApp',
  companyAddress: '123 Main St, City, ST 12345',
  // logoUrl: 'https://yourapp.com/logo.png',
});

How Compilation Works

When you run wraps email templates push, the CLI compiles your React components through a pipeline that produces SES-compatible HTML, syncs to the dashboard, and writes a lockfile for conflict detection.

Template Compilation Pipeline

esbuild
Proxy
React Email
SES
Dashboard
Lockfile

TypeScript compiled to JavaScript with esbuild

Fast Compilation

esbuild compiles TypeScript in milliseconds. No webpack, no Babel.

Prop Extraction

Template variables are extracted from props for dashboard editing.

React Email Rendering

Components rendered to cross-client HTML with inline styles.

Lockfile Tracking

A local lockfile prevents accidental overwrites between code and dashboard edits.

Workflows as TypeScript

Automation workflows are TypeScript files that define triggers, steps, delays, and conditions. They live in your repo alongside templates and deploy with wraps email workflows push.

import {
  defineWorkflow, sendEmail, delay,
} from '@wraps.dev/client';

export default defineWorkflow({
  name: 'Welcome Sequence',
  trigger: { type: 'event', eventName: 'contact.subscribed' },

  steps: [
    sendEmail('send-welcome', {
      template: 'welcome',
    }),

    delay('wait-3d', { days: 3 }),

    sendEmail('send-tips', {
      template: 'getting-started-tips',
    }),

    delay('wait-7d', { days: 7 }),

    sendEmail('send-case-study', {
      template: 'case-study',
    }),
  ],
});
import {
  defineWorkflow, cascade,
} from '@wraps.dev/client';

export default defineWorkflow({
  name: 'Re-engagement',
  trigger: { type: 'event', eventName: 'contact.inactive' },

  steps: [
    // Cascade: try each channel, stop on engagement
    ...cascade('re-engage', {
      channels: [
        { type: 'email', template: 'we-miss-you', wait: { days: 3 } },
        { type: 'email', template: 'special-offer', wait: { days: 5 } },
        { type: 'email', template: 'last-chance' },
      ],
    }),
  ],
});
import {
  defineWorkflow, sendEmail, condition,
} from '@wraps.dev/client';

export default defineWorkflow({
  name: 'Order Confirmation',
  trigger: { type: 'event', eventName: 'order.completed' },

  steps: [
    sendEmail('send-receipt', {
      template: 'order-receipt',
    }),

    condition('check-vip', {
      field: 'event.total',
      operator: 'greater_than',
      value: 100,
      branches: {
        yes: [
          sendEmail('send-vip-thanks', {
            template: 'vip-thank-you',
          }),
        ],
        no: [],
      },
    }),
  ],
});

Workflow Primitives

Workflows are built from composable primitives. Here are the most common ones — click a row to see an example.

PrimitiveDescription
sendEmailSend an email using a named template with variable substitution
delayWait a duration before continuing (minutes, hours, days, weeks)
conditionBranch on a field comparison with yes/no paths
cascadeTry channels sequentially, stop when the contact engages

The Cascade Primitive

Cascade is designed for sequences where you want to stop as soon as the contact engages. Each step waits for a specified duration. If the contact opens or clicks during that window, the cascade stops. Otherwise, it moves to the next step.

import { defineWorkflow, cascade } from '@wraps.dev/client';

export default defineWorkflow({
  name: 'Win-Back Campaign',
  trigger: { type: 'event', eventName: 'contact.churned' },

  steps: [
    // Spread cascade — it expands to send + wait + condition nodes
    ...cascade('win-back', {
      channels: [
        {
          type: 'email',
          template: 'we-miss-you',
          wait: { days: 3 },
          // If they open/click within 3 days, stop here
        },
        {
          type: 'email',
          template: 'heres-whats-new',
          wait: { days: 5 },
          // If they engage with this one, stop
        },
        {
          type: 'email',
          template: 'final-offer',
          // Last channel — no wait, cascade ends
        },
      ],
    }),
  ],
});

This replaces complex branching logic in traditional workflow builders. One primitive handles the entire re-engagement pattern, and the logic is readable in a code review.

Code and Dashboard Stay in Sync

Templates pushed from code appear in the dashboard for visual editing. Edits made in the dashboard are tracked separately. The CLI detects when both sides have changed and warns you before overwriting.

How Sync Works

  • A local lockfile tracks the SHA256 hash of each pushed template
  • Dashboard edits are tracked separately with a lastEditedFrom field
  • templates push compares hashes — warns on conflict, skips unchanged
  • Use --force to overwrite dashboard edits when pushing from code

Send with the SDK

Install the SDK to send emails using your templates:

npm install @wraps.dev/email

Send using a named SES template with data substitution, or render React components to HTML and send directly.

import { WrapsEmail } from '@wraps.dev/email';

const wraps = new WrapsEmail();

await wraps.sendTemplate({
  from: 'hello@yourapp.com',
  to: 'user@example.com',
  template: 'welcome',
  templateData: {
    name: 'Jane',
    activationUrl: 'https://yourapp.com/activate?token=abc',
  },
});
import { WrapsEmail } from '@wraps.dev/email';
import { WelcomeEmail } from './templates/welcome';
import { render } from '@react-email/render';

const wraps = new WrapsEmail();

const html = await render(
  WelcomeEmail({ name: 'Jane' })
);

await wraps.send({
  from: 'hello@yourapp.com',
  to: 'user@example.com',
  subject: 'Welcome to YourApp!',
  html,
});

Why This Matters

Moving email templates and workflows into code gives you the same guarantees you expect from application development: version control, type safety, testing, and rollbacks.

TraditionalWraps
TemplatesGUI editor onlyReact components + GUI
Version controlManual exportsGit-native
Type safetyNoneFull TypeScript
Code reviewScreenshot diffsPR diffs
WorkflowsDrag-and-dropTypeScript + visual builder
TestingManual sendsUnit testable
RollbackManual restoregit revert
CollaborationOne editor at a timeBranches + PRs

Getting Started

Initialize a templates directory, write your first template, and push it to SES in under a minute.

# Initialize templates directory
npx @wraps.dev/cli email templates init

# Edit wraps/templates/welcome.tsx

# Push to SES + dashboard
npx @wraps.dev/cli email templates push

# Push workflows
npx @wraps.dev/cli email workflows push

See It In Action

◐ Scaffolding templates directory...
✓ Created wraps/templates/
✓ welcome.tsx
✓ brand.ts
✓ wraps.config.ts
✓ Templates initialized!
◐ Compiling templates...
✓ welcome.tsx → welcome (esbuild 12ms)
◐ Uploading to SES...
✓ 1 template pushed to SES
✓ Dashboard synced
✓ Lockfile updated

Continue Learning

Ready to code your email?

Write templates as React, workflows as TypeScript. Deploy to your AWS account with one command.

npx @wraps.dev/cli email templates init
Get Started