Guide

Automate Email Draft Creation with Review Workflows

Direct sends can't be undone. One poorly personalized email to a VP burns a relationship. Drafts give you a review gate: generate 50 personalized messages from templates with conditional logic, route them through a review workflow, then send only the approved ones. Works with Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP.

By Prem Keshari

Why drafts are the safety net for automation

According to HubSpot’s 2024 Sales Engagement Report, 23% of automated outbound emails contain personalization errors (wrong name, broken merge fields, incorrect company). Those errors cost deals. Drafts insert a human review step between generation and delivery.

The workflow: template + data = drafts. Review drafts. Send the approved ones. Delete the rest. Same efficiency as direct sends, but with a safety net that catches the 23%.

Create a single draft

# Create a draft (appears in your Drafts folder)
nylas email drafts create \
  --to "sarah@acme.com" \
  --subject "Following up on our conversation" \
  --body "Hi Sarah — wanted to circle back on the API integration timeline."

# List, inspect, then send
nylas email drafts list
nylas email drafts show <draft-id>
nylas email drafts send <draft-id>

Build templates with conditional content blocks

Static templates feel generic. Conditional blocks change the message based on the contact’s attributes, making each draft feel hand-written.

#!/usr/bin/env python3
"""Generate drafts with conditional content blocks."""

import csv
import subprocess
from dataclasses import dataclass

@dataclass
class Contact:
    email: str
    name: str
    company: str
    title: str
    last_subject: str
    days: int
    deal_stage: str  # "discovery", "proposal", "negotiation"

def build_body(c: Contact) -> str:
    """Build email body with conditional sections."""
    greeting = f"Hi {c.name},"

    # Conditional opening based on days since last contact
    if c.days <= 7:
        opening = f"Great catching up last week about {c.last_subject}."
    elif c.days <= 30:
        opening = (f"It's been {c.days} days since we discussed "
                   f"{c.last_subject}. Wanted to keep things moving.")
    else:
        opening = (f"It's been a while since our conversation about "
                   f"{c.last_subject}. Hope things are going well at {c.company}.")

    # Conditional CTA based on deal stage
    if c.deal_stage == "discovery":
        cta = "Would you have 15 minutes this week to explore this further?"
    elif c.deal_stage == "proposal":
        cta = ("I've put together a proposal based on our discussion. "
               "Can I walk you through it on a quick call?")
    elif c.deal_stage == "negotiation":
        cta = ("I'd love to finalize the details. "
               "When works for a 30-minute call with your team?")
    else:
        cta = "Would you have time for a quick call this week?"

    return f"{greeting}\n\n{opening}\n\n{cta}\n\nBest,\nYour Name"

def create_draft(c: Contact) -> str:
    """Create a draft via Nylas CLI and return the draft ID."""
    body = build_body(c)
    result = subprocess.run(
        ["nylas", "email", "drafts", "create",
         "--to", c.email,
         "--subject", f"Following up on {c.last_subject}",
         "--body", body, "--json"],
        capture_output=True, text=True, check=True,
    )
    return result.stdout.strip()

# Generate drafts from CSV
with open("outbound_list.csv") as f:
    reader = csv.DictReader(f)
    manifest = []
    for row in reader:
        contact = Contact(
            email=row["email"], name=row["name"],
            company=row["company"], title=row["title"],
            last_subject=row["last_subject"],
            days=int(row["days"]), deal_stage=row["deal_stage"]
        )
        draft_output = create_draft(contact)
        manifest.append({"email": contact.email, "draft": draft_output})
        print(f"Draft created for {contact.name} ({contact.email})")

# Save manifest for review workflow
import json
with open("draft_manifest.json", "w") as f:
    json.dump(manifest, f, indent=2)
print(f"\nCreated {len(manifest)} drafts. Review manifest: draft_manifest.json")

Build a review workflow

The review workflow has three stages: generate, review, approve/reject. Here’s how to structure it:

#!/bin/bash
# review-drafts.sh — interactive review of generated drafts

echo "=== Draft Review Workflow ==="
echo ""

# List all drafts with recipients
nylas email drafts list --json | jq -r '
  .[] | "\(.id)\t\(.to[0].email)\t\(.subject)"
' | while IFS=$'\t' read -r id email subject; do
  echo "---"
  echo "To:      $email"
  echo "Subject: $subject"
  echo "Draft:   $id"
  echo ""

  # Show preview of the body
  nylas email drafts show "$id" --json | jq -r '.body' | head -5
  echo "..."
  echo ""

  read -p "Action [s]end / [d]elete / [v]iew full / [n]ext: " action
  case "$action" in
    s) nylas email drafts send "$id"
       echo "SENT to $email" ;;
    d) nylas email drafts delete "$id"
       echo "DELETED draft for $email" ;;
    v) nylas email drafts show "$id" --json | jq -r '.body'
       read -p "Send this? [y/n]: " confirm
       [[ "$confirm" == "y" ]] && nylas email drafts send "$id" ;;
    *) echo "Skipped" ;;
  esac
done

Schedule approved drafts for optimal delivery

According to Mailchimp’s Send Time Optimization data (2024), emails sent between 9-11 AM in the recipient’s local timezone get 14% higher open rates. After reviewing and approving drafts, schedule them for business hours:

# Send approved drafts on a schedule (stagger across 2 hours)
APPROVED_IDS=("draft_001" "draft_002" "draft_003")

for i in "${!APPROVED_IDS[@]}"; do
  id="${APPROVED_IDS[$i]}"
  offset_minutes=$(( i * 5 ))

  nylas email drafts send "$id" \
    --schedule "tomorrow 9:$(printf '%02d' $offset_minutes)am"
  echo "Scheduled: $id (9:$(printf '%02d' $offset_minutes) AM)"
done

Pull conversation context into templates

The best follow-ups reference your last conversation. Pull context from email history before generating the draft:

# Find the last email thread with a contact
LAST=$(nylas email list --json --limit 50 | jq -r '
  [.[] | select(.from[0].email == "sarah@acme.com"
    or (.to[]?.email == "sarah@acme.com"))]
  | sort_by(.date) | last')

SUBJECT=$(echo "$LAST" | jq -r '.subject')
DATE=$(echo "$LAST" | jq -r '.date')
SNIPPET=$(echo "$LAST" | jq -r '.snippet // ""' | head -c 200)

echo "Last conversation: $SUBJECT"
echo "Preview: $SNIPPET"

Batch draft creation (Bash)

#!/bin/bash
# batch-drafts.sh — create personalized drafts from CSV

CREATED=0
while IFS=, read -r email name company title last_subject days deal_stage; do
  [[ "$email" == "email" ]] && continue  # skip header

  # Build conditional body
  if [ "$days" -le 7 ]; then
    opening="Great catching up last week about $last_subject."
  elif [ "$days" -le 30 ]; then
    opening="It's been $days days since we discussed $last_subject."
  else
    opening="It's been a while since our $last_subject conversation."
  fi

  body="Hi $name,

$opening

Would you have time this week for a quick call?

Best,
Your Name"

  nylas email drafts create \
    --to "$email" \
    --subject "Following up on $last_subject" \
    --body "$body"

  CREATED=$((CREATED + 1))
  echo "[$CREATED] Draft for $name ($email)"
done < outbound_list.csv

echo "Done. Created $CREATED drafts."
echo "Review: nylas email drafts list"

Next steps