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
doneSchedule 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)"
donePull 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
- Personalize outbound email — when you’re ready to skip the review step and send directly with scheduling and throttling
- Build email autocomplete — speed up manual composition with fuzzy contact search from your shell
- Parse signatures for enrichment — extract job titles and company info to improve conditional template targeting
- Command reference — every flag, subcommand, and example