> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ensemble.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# email Operation

> Send transactional and marketing emails via Cloudflare Email, Resend, or SMTP with template rendering, batch processing, and attachment support. From simple messages to complex batch campaigns with personalized templates and file attachments.

## Basic Usage

```yaml theme={null}
operations:
  - name: send-welcome
    operation: email
    config:
      provider: cloudflare
      from: welcome@example.com
      to: ${input.email}
      subject: Welcome to Our Platform!
      html: |
        <h1>Welcome {{input.name}}!</h1>
        <p>We're excited to have you on board.</p>
```

## Configuration

```yaml theme={null}
config:
  provider: string           # cloudflare, resend, smtp
  from: string              # Sender email address
  to: string | string[]     # Recipient(s)
  subject: string           # Email subject
  body: string              # Plain text body
  html: string              # HTML body
  cc: string | string[]     # CC recipients (optional)
  bcc: string | string[]    # BCC recipients (optional)
  replyTo: string           # Reply-to address (optional)
  attachments: array        # File attachments (optional)
  headers: object           # Custom headers (optional)
  tags: string[]            # Email tags (optional)
  metadata: object          # Custom metadata (optional)
```

## Email Providers

### Cloudflare Email (Recommended)

Zero-configuration email sending via Cloudflare Email Routing:

```yaml theme={null}
operations:
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: noreply@example.com
      fromName: Your App
      to: ${input.email}
      subject: Your Receipt
      html: <h1>Thank you for your purchase!</h1>
```

**wrangler.toml** configuration:

```toml theme={null}
[[send_email]]
name = "EMAIL"
destination_address_list = "allowed-recipients"  # Optional allowlist

[env.production]
[[env.production.send_email]]
name = "EMAIL"
destination_address_list = "production-recipients"
```

**Features**:

* No API key required
* Automatic DKIM signing
* 100,000 emails/day free tier
* Edge-native performance
* Instant delivery

### Resend

Developer-friendly email API with great deliverability:

```yaml theme={null}
operations:
  - name: send
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}
      from: team@example.com
      to: ${input.email}
      subject: Welcome!
      html: <p>Welcome to our platform</p>
```

**Environment Variables**:

```bash theme={null}
RESEND_API_KEY=re_123456789
```

**Features**:

* Simple REST API
* Email analytics dashboard
* 100 emails/day free tier
* Webhook support
* React Email integration

### SMTP

Generic SMTP for any mail server:

```yaml theme={null}
operations:
  - name: send
    operation: email
    config:
      provider: smtp
      host: smtp.gmail.com
      port: 587
      secure: true
      auth:
        user: ${env.SMTP_USER}
        pass: ${env.SMTP_PASS}
      from: alerts@example.com
      to: ${input.email}
      subject: Alert Notification
      html: <p>Alert: {{input.message}}</p>
```

**Supported Services**:

* Gmail
* SendGrid
* Mailgun
* Amazon SES
* Custom SMTP servers

## Email Templates

### Inline HTML

Simple emails with inline HTML:

```yaml theme={null}
operations:
  - name: send-confirmation
    operation: email
    config:
      provider: cloudflare
      from: orders@example.com
      to: ${input.customer_email}
      subject: Order #${input.order_id} Confirmed
      html: |
        <!DOCTYPE html>
        <html>
        <head>
          <style>
            body { font-family: Arial, sans-serif; line-height: 1.6; }
            .header { background: #4CAF50; color: white; padding: 20px; }
            .content { padding: 20px; }
            .button { background: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; }
          </style>
        </head>
        <body>
          <div class="header">
            <h1>Order Confirmed!</h1>
          </div>
          <div class="content">
            <p>Hi {{input.customer_name}},</p>
            <p>Your order #{{input.order_id}} has been confirmed.</p>
            <p>Total: ${{input.total}}</p>
            <a href="{{input.order_url}}" class="button">View Order</a>
          </div>
        </body>
        </html>
```

### Template Variables

Use template expressions for dynamic content:

```yaml theme={null}
operations:
  - name: send-notification
    operation: email
    config:
      provider: cloudflare
      from: notifications@example.com
      to: ${input.email}
      subject: ${input.notification_type} - Action Required
      html: |
        <h1>Hello {{input.name}}!</h1>
        <p>{{input.message}}</p>
        <p>Generated at {{new Date().toLocaleString()}}</p>
```

### Liquid Templates (Advanced)

For complex templates, store them in KV with Liquid syntax:

```yaml theme={null}
operations:
  # Step 1: Fetch template from KV
  - name: get-template
    operation: storage
    config:
      type: kv
      action: get
      key: email-templates/welcome

  # Step 2: Render template with Liquid
  - name: render
    operation: code
    config:
      script: scripts/render-liquid-template
    input:
      template: ${get-template.output.value}
      name: ${input.name}
      activationUrl: ${input.activation_url}
      appName: 'Your App'

  # Step 3: Send rendered email
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: welcome@example.com
      to: ${input.email}
      subject: Welcome to Your App!
      html: ${render.output.html}
```

```typescript theme={null}
// scripts/render-liquid-template.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default async function renderLiquidTemplate(context: AgentExecutionContext) {
  const { template, name, activationUrl, appName } = context.input

  const Liquid = require('liquidjs')
  const engine = new Liquid()

  const rendered = await engine.parseAndRender(template, {
    name,
    activationUrl,
    appName
  })

  return { html: rendered }
}
```

**Template in KV**:

```liquid theme={null}
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: Arial, sans-serif; }
    .button { background: #0066cc; color: white; padding: 12px 24px; }
  </style>
</head>
<body>
  <h1>Welcome {{ name | capitalize }}!</h1>
  <p>Click below to activate your account:</p>
  <a href="{{ activationUrl }}" class="button">Activate Account</a>
  <p>Best regards,<br>The {{ appName }} Team</p>
</body>
</html>
```

## Email Attachments

### Single Attachment

```yaml theme={null}
operations:
  - name: send-invoice
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}
      from: billing@example.com
      to: ${input.customer_email}
      subject: Your Invoice
      html: <p>Please find your invoice attached.</p>
      attachments:
        - filename: invoice.pdf
          content: ${generate-pdf.output.content}
          contentType: application/pdf
          encoding: base64
```

### Multiple Attachments

```yaml theme={null}
operations:
  - name: send-report
    operation: email
    config:
      provider: cloudflare
      from: reports@example.com
      to: ${input.email}
      subject: Monthly Report
      html: <p>Your monthly report is attached.</p>
      attachments:
        - filename: report.pdf
          content: ${generate-pdf.output}
          contentType: application/pdf
          encoding: base64
        - filename: data.csv
          content: ${export-csv.output}
          contentType: text/csv
          encoding: utf8
        - filename: summary.txt
          content: "Total Sales: $${input.total_sales}"
          contentType: text/plain
```

### Attachment from Storage

```yaml theme={null}
operations:
  # Step 1: Fetch file from R2
  - name: get-file
    operation: storage
    config:
      type: r2
      action: get
      key: invoices/${input.invoice_id}.pdf

  # Step 2: Email with attachment
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: billing@example.com
      to: ${input.email}
      subject: Invoice #${input.invoice_id}
      html: <p>Invoice attached</p>
      attachments:
        - filename: invoice-${input.invoice_id}.pdf
          content: ${get-file.output.content}
          contentType: application/pdf
```

## Batch Sending

Send personalized emails to multiple recipients:

```yaml theme={null}
operations:
  - name: send-newsletter
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}
      from: newsletter@example.com
      batch:
        recipients:
          - email: alice@example.com
            name: Alice
            lastPurchase: Laptop
          - email: bob@example.com
            name: Bob
            lastPurchase: Phone
          - email: charlie@example.com
            name: Charlie
            lastPurchase: Tablet
      subject: Your Weekly Newsletter
      html: |
        <h1>Hi {{ name }}!</h1>
        <p>Based on your recent purchase of {{ lastPurchase }}, we have recommendations for you.</p>
      rateLimit: 10                    # Emails per second
```

**Output**:

```yaml theme={null}
{
  sent: 3,
  failed: 0,
  messageIds: ["msg-1", "msg-2", "msg-3"],
  errors: []
}
```

## CC and BCC

```yaml theme={null}
operations:
  - name: send-update
    operation: email
    config:
      provider: cloudflare
      from: updates@example.com
      to: customer@example.com
      cc:
        - manager@example.com
        - team@example.com
      bcc: archive@example.com
      subject: Project Update
      html: <p>Project status: In Progress</p>
```

## Reply-To

Set a different reply address:

```yaml theme={null}
operations:
  - name: send-support
    operation: email
    config:
      provider: cloudflare
      from: noreply@example.com
      replyTo: support@example.com
      to: ${input.email}
      subject: Support Ticket #${input.ticket_id}
      html: <p>Your support ticket has been created. Reply to this email for updates.</p>
```

## Custom Headers

Add tracking and custom headers:

```yaml theme={null}
operations:
  - name: send-campaign
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}
      from: marketing@example.com
      to: ${input.email}
      subject: Special Offer!
      html: <p>Limited time offer!</p>
      headers:
        X-Campaign-ID: campaign-2024-q1
        X-Customer-Segment: premium
        List-Unsubscribe: <https://example.com/unsubscribe?id=${input.user_id}>
        X-Priority: 1
```

## Tags and Metadata

Organize and track emails:

```yaml theme={null}
operations:
  - name: send-order-confirmation
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}
      from: orders@example.com
      to: ${input.email}
      subject: Order Confirmation
      html: <p>Your order has been confirmed!</p>
      tags:
        - transactional
        - order-confirmation
        - ${input.customer_segment}
      metadata:
        orderId: ${input.order_id}
        customerId: ${input.customer_id}
        orderValue: ${input.total}
```

## Common Patterns

### Transactional Email

```yaml theme={null}
ensemble: send-order-confirmation

inputs:
  order_id: string
  customer_email: string
  customer_name: string
  items: array
  total: number

operations:
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: orders@shop.com
      to: ${input.customer_email}
      subject: Order #${input.order_id} Confirmed
      html: |
        <!DOCTYPE html>
        <html>
        <body>
          <h1>Thank you, {{input.customer_name}}!</h1>
          <p>Your order #{{input.order_id}} has been confirmed.</p>
          <h2>Order Details:</h2>
          <ul>
            {{input.items.map(item => `<li>${item.name} x ${item.quantity} - $${item.price}</li>`).join('')}}
          </ul>
          <p><strong>Total: ${{input.total}}</strong></p>
        </body>
        </html>

outputs:
  messageId: ${send.output.messageId}
  success: ${send.output.success}
```

### Welcome Email Workflow

```yaml theme={null}
ensemble: welcome-new-user

inputs:
  email: string
  name: string
  userId: string

operations:
  # Generate activation token
  - name: generate-token
    operation: code
    config:
      script: scripts/generate-activation-token

  # Store token in KV
  - name: store-token
    operation: storage
    config:
      type: kv
      action: put
      key: activation:${input.userId}
      value: ${generate-token.output.token}
      ttl: 86400                       # 24 hours
```

```typescript theme={null}
// scripts/generate-activation-token.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function generateActivationToken(context: AgentExecutionContext) {
  const crypto = require('crypto')
  const token = crypto.randomBytes(32).toString('hex')
  return {
    token,
    expiresAt: Date.now() + 86400000  // 24 hours
  }
}
```

```yaml theme={null}
  # Send welcome email
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: welcome@example.com
      to: ${input.email}
      subject: Welcome to Our Platform!
      html: |
        <h1>Welcome {{input.name}}!</h1>
        <p>Click below to activate your account:</p>
        <a href="https://example.com/activate?token={{generate-token.output.token}}">
          Activate Account
        </a>
        <p>This link expires in 24 hours.</p>

outputs:
  sent: true
  activationToken: ${generate-token.output.token}
```

### Password Reset Email

```yaml theme={null}
ensemble: password-reset

inputs:
  email: string
  userId: string

operations:
  # Generate reset token
  - name: generate-token
    operation: code
    config:
      script: scripts/generate-reset-token

  # Store token
  - name: store-token
    operation: storage
    config:
      type: kv
      action: put
      key: reset:${input.userId}
      value: ${generate-token.output.token}
      ttl: 3600
```

```typescript theme={null}
// scripts/generate-reset-token.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function generateResetToken(context: AgentExecutionContext) {
  const crypto = require('crypto')
  return {
    token: crypto.randomBytes(32).toString('hex'),
    expiresAt: Date.now() + 3600000  // 1 hour
  }
}
```

```yaml theme={null}
  # Send reset email
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: security@example.com
      to: ${input.email}
      subject: Password Reset Request
      html: |
        <h1>Reset Your Password</h1>
        <p>Click below to reset your password:</p>
        <a href="https://example.com/reset?token={{generate-token.output.token}}">
          Reset Password
        </a>
        <p>This link expires in 1 hour.</p>
        <p>If you didn't request this, ignore this email.</p>

outputs:
  sent: true
  resetToken: ${generate-token.output.token}
```

### Email with PDF Invoice

```yaml theme={null}
ensemble: send-invoice

inputs:
  customer_email: string
  customer_name: string
  invoice_number: string
  items: array
  total: number

operations:
  # Generate PDF
  - name: generate-pdf
    operation: pdf
    config:
      html: |
        <!DOCTYPE html>
        <html>
        <head>
          <style>
            body { font-family: Arial; padding: 40px; }
            table { width: 100%; border-collapse: collapse; }
            th, td { border: 1px solid #ddd; padding: 12px; }
            .total { font-size: 1.2em; font-weight: bold; }
          </style>
        </head>
        <body>
          <h1>Invoice #{{input.invoice_number}}</h1>
          <p>Bill To: {{input.customer_name}}</p>
          <table>
            <thead>
              <tr><th>Item</th><th>Quantity</th><th>Price</th><th>Total</th></tr>
            </thead>
            <tbody>
              {{input.items.map(item => `
                <tr>
                  <td>${item.name}</td>
                  <td>${item.quantity}</td>
                  <td>$${item.price}</td>
                  <td>$${item.quantity * item.price}</td>
                </tr>
              `).join('')}}
            </tbody>
          </table>
          <p class="total">Total: ${{input.total}}</p>
        </body>
        </html>
      format: A4

  # Email PDF
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: billing@example.com
      to: ${input.customer_email}
      subject: Invoice #${input.invoice_number}
      html: |
        <h2>Hi {{input.customer_name}},</h2>
        <p>Thank you for your business! Your invoice is attached.</p>
        <p><strong>Invoice #:</strong> {{input.invoice_number}}</p>
        <p><strong>Amount:</strong> ${{input.total}}</p>
      attachments:
        - filename: invoice-${input.invoice_number}.pdf
          content: ${generate-pdf.output}
          contentType: application/pdf
          encoding: base64

outputs:
  sent: true
  messageId: ${send.output.messageId}
```

### Data Export Email

```yaml theme={null}
ensemble: export-and-email

inputs:
  admin_email: string
  export_type: string

operations:
  # Export data from KV
  - name: export
    operation: storage
    config:
      type: kv
      action: export
      prefix: users:
      format: csv
      exportOptions:
        headers: true
        fields: [id, email, name, created_at]

  # Convert to base64
  - name: encode
    operation: code
    config:
      script: scripts/encode-export-data
    input:
      data: ${export.output.data}
```

```typescript theme={null}
// scripts/encode-export-data.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function encodeExportData(context: AgentExecutionContext) {
  const { data } = context.input

  return {
    content: btoa(data),
    filename: `export-${Date.now()}.csv`
  }
}
```

```yaml theme={null}
  # Email export
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: reports@example.com
      to: ${input.admin_email}
      subject: Data Export - ${input.export_type}
      html: |
        <h2>Data Export Report</h2>
        <p>Your requested {{input.export_type}} export is attached.</p>
        <ul>
          <li><strong>Format:</strong> CSV</li>
          <li><strong>Records:</strong> {{export.output.count}}</li>
          <li><strong>Generated:</strong> {{new Date().toISOString()}}</li>
        </ul>
      attachments:
        - filename: ${encode.output.filename}
          content: ${encode.output.content}
          contentType: text/csv
          encoding: base64

outputs:
  sent: true
  recordCount: ${export.output.count}
```

### Newsletter Campaign

```yaml theme={null}
ensemble: send-newsletter

inputs:
  subject: string
  content: string

operations:
  # Fetch subscriber list
  - name: get-subscribers
    operation: storage
    config:
      type: d1
      query: |
        SELECT email, name, preferences
        FROM subscribers
        WHERE active = true AND newsletter_opt_in = true

  # Send batch emails
  - name: send-campaign
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}
      from: newsletter@example.com
      batch:
        recipients: ${get-subscribers.output.results}
      subject: ${input.subject}
      html: ${input.content}
      rateLimit: 5                     # 5 emails/second
      headers:
        List-Unsubscribe: <https://example.com/unsubscribe>

outputs:
  sent: ${send-campaign.output.sent}
  failed: ${send-campaign.output.failed}
  totalSubscribers: ${get-subscribers.output.results.length}
```

## Error Handling

### Retry on Failure

```yaml theme={null}
operations:
  - name: send
    operation: email
    config:
      provider: cloudflare
      from: alerts@example.com
      to: ${input.email}
      subject: Important Alert
      html: <p>{{input.message}}</p>
    retry:
      maxAttempts: 3
      backoff: exponential           # 1s, 2s, 4s delays
```

### Fallback Provider

```yaml theme={null}
operations:
  # Try primary provider
  - name: send-primary
    operation: email
    config:
      provider: cloudflare
      from: alerts@example.com
      to: ${input.email}
      subject: Alert
      html: <p>{{input.message}}</p>

  # Fallback to SMTP if primary fails
  - name: send-fallback
    condition: ${send-primary.failed}
    operation: email
    config:
      provider: smtp
      host: smtp.gmail.com
      port: 587
      secure: true
      auth:
        user: ${env.SMTP_USER}
        pass: ${env.SMTP_PASS}
      from: backup@example.com
      to: ${input.email}
      subject: Alert
      html: <p>{{input.message}}</p>

outputs:
  sent: ${send-primary.success || send-fallback.success}
  provider: ${send-primary.success ? 'cloudflare' : 'smtp'}
```

### Validate Email Address

```yaml theme={null}
operations:
  # Validate email
  - name: validate
    operation: code
    config:
      script: scripts/validate-email
    input:
      email: ${input.email}

  # Send if valid
  - name: send
    condition: ${validate.output.valid}
    operation: email
    config:
      provider: cloudflare
      from: info@example.com
      to: ${input.email}
      subject: Welcome
      html: <p>Welcome!</p>
```

```typescript theme={null}
// scripts/validate-email.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function validateEmail(context: AgentExecutionContext) {
  const { email } = context.input
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

  if (!emailRegex.test(email)) {
    throw new Error('Invalid email address')
  }

  return { valid: true }
}
```

```yaml theme={null}
### Handle Batch Errors
```

```yaml theme={null}
operations:
  - name: send-batch
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}
      from: newsletter@example.com
      batch:
        recipients: ${input.recipients}
      subject: Newsletter
      html: ${input.content}

  # Log errors
  - name: log-errors
    condition: ${send-batch.output.failed > 0}
    operation: storage
    config:
      type: kv
      action: put
      key: email-errors:${Date.now()}
      value: ${send-batch.output.errors}

outputs:
  sent: ${send-batch.output.sent}
  failed: ${send-batch.output.failed}
  errors: ${send-batch.output.errors}
```

## Testing

### Test with TestConductor

```typescript theme={null}
import { describe, it, expect } from 'vitest';
import { TestConductor } from '@ensemble/conductor/testing';

describe('send-welcome-email', () => {
  it('should send welcome email', async () => {
    const conductor = await TestConductor.create({
      projectPath: './conductor',
      mocks: {
        email: {
          'send-welcome': {
            success: true,
            messageId: 'msg-123',
            provider: 'cloudflare'
          }
        }
      }
    });

    const result = await conductor.executeAgent('send-welcome-email', {
      email: 'test@example.com',
      name: 'Test User'
    });

    expect(result).toBeSuccessful();
    expect(result.output.messageId).toBe('msg-123');
  });

  it('should handle invalid email', async () => {
    const conductor = await TestConductor.create({
      projectPath: './conductor'
    });

    const result = await conductor.executeAgent('send-welcome-email', {
      email: 'invalid-email',
      name: 'Test User'
    });

    expect(result).toBeFailed();
    expect(result.error).toContain('Invalid email');
  });
});
```

### Mock Email Provider

```typescript theme={null}
const conductor = await TestConductor.create({
  mocks: {
    email: {
      '*': {                           // Mock all email operations
        success: true,
        messageId: 'mock-msg-id',
        timestamp: new Date().toISOString()
      }
    }
  }
});
```

## Best Practices

**1. Use Plain Text Fallback**

```yaml theme={null}
# Good: Include plain text version
operations:
  - name: send
    operation: email
    config:
      provider: cloudflare
      html: <h1>Welcome!</h1><p>Thanks for signing up.</p>
      body: |
        Welcome!

        Thanks for signing up.

# Bad: HTML only
operations:
  - name: send
    operation: email
    config:
      provider: cloudflare
      html: <h1>Welcome!</h1><p>Thanks for signing up.</p>
```

**2. Validate Recipients**

```yaml theme={null}
# Good: Validate before sending
operations:
  - name: validate
    operation: code
    config:
      script: scripts/validate-email-simple
    input:
      email: ${input.email}

  - name: send
    operation: email

# Bad: No validation
operations:
  - name: send
    operation: email
    config:
      to: ${input.email}  # Could be invalid
```

```typescript theme={null}
// scripts/validate-email-simple.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function validateEmailSimple(context: AgentExecutionContext) {
  const { email } = context.input

  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    throw new Error('Invalid email')
  }

  return { valid: true }
}
```

```yaml theme={null}
**3. Set Reply-To for No-Reply Emails**
```

```yaml theme={null}
# Good: Set reply-to
operations:
  - name: send
    operation: email
    config:
      from: noreply@example.com
      replyTo: support@example.com
      subject: Notification

# Bad: No reply-to with noreply
operations:
  - name: send
    operation: email
    config:
      from: noreply@example.com
      subject: Notification
```

**4. Rate Limit Batch Sending**

```yaml theme={null}
# Good: Set rate limit
operations:
  - name: send-batch
    operation: email
    config:
      batch:
        recipients: ${input.recipients}
      rateLimit: 10  # 10 emails/second

# Bad: No rate limiting
operations:
  - name: send-batch
    operation: email
    config:
      batch:
        recipients: ${input.recipients}
```

**5. Include Unsubscribe Link**

```yaml theme={null}
# Good: Include unsubscribe
operations:
  - name: send
    operation: email
    config:
      html: |
        <p>Newsletter content...</p>
        <p><a href="https://example.com/unsubscribe?id=${input.user_id}">Unsubscribe</a></p>
      headers:
        List-Unsubscribe: <https://example.com/unsubscribe?id=${input.user_id}>

# Bad: No unsubscribe link
operations:
  - name: send
    operation: email
    config:
      html: <p>Newsletter content...</p>
```

**6. Use Tags for Organization**

```yaml theme={null}
# Good: Tag emails
operations:
  - name: send
    operation: email
    config:
      tags:
        - transactional
        - order-confirmation
      metadata:
        orderId: ${input.order_id}

# Bad: No tags or metadata
operations:
  - name: send
    operation: email
```

**7. Store API Keys in Environment**

```yaml theme={null}
# Good: Use environment variables
operations:
  - name: send
    operation: email
    config:
      provider: resend
      apiKey: ${env.RESEND_API_KEY}

# Bad: Hardcoded API key
operations:
  - name: send
    operation: email
    config:
      provider: resend
      apiKey: re_123456789  # Never do this!
```

**8. Handle Attachments Safely**

```yaml theme={null}
# Good: Validate attachment size
operations:
  - name: validate-size
    operation: code
    config:
      script: scripts/validate-attachment-size
    input:
      attachmentLength: ${input.attachment.length}

  - name: send
    operation: email
    config:
      attachments:
        - filename: ${input.filename}
          content: ${input.attachment}

# Bad: No size validation
operations:
  - name: send
    operation: email
    config:
      attachments:
        - content: ${input.attachment}
```

```typescript theme={null}
// scripts/validate-attachment-size.ts
import type { AgentExecutionContext } from '@ensemble-edge/conductor'

export default function validateAttachmentSize(context: AgentExecutionContext) {
  const { attachmentLength } = context.input
  const maxSize = 10 * 1024 * 1024  // 10MB

  if (attachmentLength > maxSize) {
    throw new Error('Attachment too large')
  }

  return { valid: true }
}
```

```yaml theme={null}
## Common Pitfalls

### Pitfall: Missing From Address
```

```yaml theme={null}
# Bad: No from address
- name: send
  operation: email
  config:
    to: user@example.com
    subject: Hello

# Good: Include from
- name: send
  operation: email
  config:
    from: info@example.com
    to: user@example.com
    subject: Hello
```

### Pitfall: HTML Without Plain Text

```yaml theme={null}
# Bad: HTML only
- name: send
  operation: email
  config:
    html: <p>Content</p>

# Good: Include both
- name: send
  operation: email
  config:
    html: <p>Content</p>
    body: Content
```

### Pitfall: No Error Handling

```yaml theme={null}
# Bad: No retry or fallback
- name: send
  operation: email
  config:
    provider: cloudflare

# Good: Retry and fallback
- name: send-primary
  operation: email
  config:
    provider: cloudflare
  retry:
    maxAttempts: 3

- name: send-fallback
  condition: ${send-primary.failed}
  operation: email
  config:
    provider: smtp
```

### Pitfall: Batch Without Rate Limiting

```yaml theme={null}
# Bad: No rate limit
- name: send-batch
  operation: email
  config:
    batch:
      recipients: ${input.recipients}  # Could hit rate limits

# Good: Set rate limit
- name: send-batch
  operation: email
  config:
    batch:
      recipients: ${input.recipients}
    rateLimit: 10
```

## Next Steps

<CardGroup cols={2}>
  <Card title="sms Operation" icon="message" href="/conductor/operations/sms">
    Send SMS messages
  </Card>

  <Card title="pdf Operation" icon="file-pdf" href="/conductor/operations/pdf">
    Generate PDF documents
  </Card>

  <Card title="storage Operation" icon="database" href="/conductor/operations/storage">
    Store email templates
  </Card>

  <Card title="Testing" icon="vial" href="/conductor/building/testing-observability">
    Test email workflows
  </Card>
</CardGroup>
