Guide
Auto-Create Email Drafts from the CLI
Drafts are the safety net between automation and your inbox. Instead of sending personalized emails directly, generate pre-filled drafts that pull in contact context and conversation history. Review each one, make adjustments, then send — batch efficiency with human quality control.
Why drafts, not direct sends
Direct sends cannot be undone. One poorly personalized email to a VP burns a relationship. A template that renders Hi ${NAME} instead of a real name is embarrassing at best, deal-killing at worst.
Drafts give you a review step: generate 50 personalized messages, scan each one in your inbox, edit where needed, send the good ones. Same efficiency as automated outbound, but with human quality control before anything leaves your account.
The workflow is simple: template + data = drafts. Review drafts. Send the approved ones. Delete the rest.
Create a draft from the CLI
The nylas email drafts subcommand manages your draft lifecycle — create, list, view, send, and delete.
# Create a simple draft
nylas email drafts create \
--to "sarah@acme.com" \
--subject "Following up on our conversation" \
--body "Hi Sarah — wanted to circle back on what we discussed last week."
# List your drafts
nylas email drafts list
# View a specific draft
nylas email drafts show <draft-id>
# Send a draft when ready
nylas email drafts send <draft-id>Drafts appear in your email client just like ones you composed manually. You can edit them in Gmail, Outlook, or any other client before sending — the CLI just creates them faster.
Template-driven drafts
Real outbound campaigns need templates with merge fields. Start with a plain-text template file, then render it with contact data before creating each draft.
Bash template
# templates/follow-up.txt
Hi ${NAME},
I wanted to follow up on our conversation about ${LAST_SUBJECT}.
It has been ${DAYS} days and I wanted to make sure nothing fell through the cracks.
As ${TITLE} at ${COMPANY}, you mentioned ${KEY_POINT}. I have some ideas that might help.
Would you have 15 minutes this week to discuss?
Best,
Your NamePython template
from string import Template
FOLLOW_UP = Template("""Hi $name,
I wanted to follow up on our conversation about $last_subject.
It has been $days days and I wanted to make sure nothing fell through the cracks.
As $title at $company, you mentioned some challenges I think we can help with.
Would you have 15 minutes this week to discuss?
Best,
Your Name""")
body = FOLLOW_UP.safe_substitute(
name="Sarah",
last_subject="API integration timeline",
days="14",
title="VP of Engineering",
company="Acme Corp"
)Node / TypeScript template
function renderTemplate(template: string, data: Record<string, string>): string {
return template.replace(/\$\{(\w+)\}/g, (_, key) => data[key] ?? "[" + key + "]");
}
// Usage
const template = "Hi ${NAME},\n\nFollowing up on ${LAST_SUBJECT}. Would love to reconnect this week.\n\nBest,\nYour Name";
const body = renderTemplate(template, {
NAME: "Sarah",
LAST_SUBJECT: "API integration timeline",
});Multiple template types
Keep a templates directory with variants for different outreach scenarios:
templates/
follow-up.txt # Re-engage after a conversation
introduction-request.txt # Ask a mutual connection for an intro
meeting-request.txt # Propose a specific meeting time
cold-outreach.txt # First contact with a new prospectEach template uses the same merge-field syntax. Your batch script picks the right one based on the contact's stage or a column in your CSV.
Context-aware drafts
The best follow-ups reference your last conversation. Use nylas email list --json to pull conversation history and feed it into your templates.
# Find the last email thread with a contact
LAST_EMAIL=$(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')
LAST_SUBJECT=$(echo "$LAST_EMAIL" | jq -r '.subject')
LAST_DATE=$(echo "$LAST_EMAIL" | jq -r '.date')
# Calculate days since last interaction
DAYS_SINCE=$(( ($(date +%s) - $(date -d "$LAST_DATE" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "$LAST_DATE" +%s)) / 86400 ))
echo "Last conversation: $LAST_SUBJECT ($DAYS_SINCE days ago)"Now you have real context — the actual subject line and how many days ago it happened — to populate your template. The draft reads like a genuine follow-up, not a mass email.
Batch draft creation
Combine templates, contact data, and the draft creation command into a single script that generates personalized drafts for your entire outbound list.
#!/bin/bash
# batch-drafts.sh — create personalized drafts for a contact list
TEMPLATE="Hi NAME_PLACEHOLDER,
Following up on our last conversation about SUBJECT_PLACEHOLDER. It has been DAYS_PLACEHOLDER days — wanted to make sure we keep momentum.
Would you have time this week for a quick call?
Best,
Your Name"
CREATED=0
while IFS=, read -r email name company title last_subject days; do
[[ "$email" == "email" ]] && continue # skip header
# Render template
body=$(echo "$TEMPLATE" | \
sed "s/NAME_PLACEHOLDER/$name/g" | \
sed "s/COMPANY_PLACEHOLDER/$company/g" | \
sed "s/TITLE_PLACEHOLDER/$title/g" | \
sed "s/SUBJECT_PLACEHOLDER/$last_subject/g" | \
sed "s/DAYS_PLACEHOLDER/$days/g")
subject="Following up on $last_subject"
nylas email drafts create \
--to "$email" \
--subject "$subject" \
--body "$body"
CREATED=$((CREATED + 1))
echo "[$CREATED] Draft created for $name ($email)"
done < outbound_list.csv
echo "Done. Created $CREATED drafts. Review with: nylas email drafts list"Feed it a CSV with headers email,name,company,title,last_subject,days and you get one draft per row. No emails are sent until you explicitly approve them.
Review workflow
Once drafts are created, review them before sending. You can inspect from the CLI or open your email client where they appear in your Drafts folder.
# List all drafts
nylas email drafts list
# Review a specific draft
nylas email drafts show <draft-id>
# Edit a draft (delete and recreate, or edit in your email client)
nylas email drafts delete <draft-id>
nylas email drafts create --to "..." --subject "..." --body "updated body"
# Send approved drafts
nylas email drafts send <draft-id>
# Bulk send all drafts (careful!)
nylas email drafts list --json | jq -r '.[].id' | while read id; do
nylas email drafts send "$id"
echo "Sent: $id"
sleep 2
doneThe review step is the whole point. Scan subjects and recipients in the list view, spot-check a few bodies with drafts show, delete any that do not look right, then send the rest.
LLM-powered drafts
Instead of static templates, use an LLM to generate personalized draft bodies from conversation context. Pipe the contact's email history into a prompt and let the model write a natural follow-up.
# Pipe contact context into an LLM to generate a personalized draft
CONTEXT=$(nylas email list --json --limit 5 | jq '[.[] | select(.from[0].email == "sarah@acme.com")] | .[0]')
# Use your preferred LLM (example with a generic CLI)
DRAFT_BODY=$(echo "Write a professional follow-up email based on this context:
Last email subject: $(echo $CONTEXT | jq -r '.subject')
Contact: Sarah Chen, VP Engineering at Acme Corp
Tone: warm, professional, brief" | llm generate)
nylas email drafts create \
--to "sarah@acme.com" \
--subject "Following up" \
--body "$DRAFT_BODY"For a full LLM-driven email workflow, see the Build an LLM Agent with Email Tools guide, which covers tool-calling agents that can read, draft, and send email autonomously.
Python version
A complete Python script with a DraftGenerator class, CSV parsing, and template rendering.
#!/usr/bin/env python3
"""batch_drafts.py — generate personalized email drafts from a CSV."""
import csv
import subprocess
from dataclasses import dataclass
from string import Template
FOLLOW_UP = Template("""Hi $name,
I wanted to follow up on our conversation about $last_subject.
It has been $days days and I wanted to make sure nothing fell through the cracks.
As $title at $company, you mentioned some challenges I think we can help with.
Would you have 15 minutes this week to discuss?
Best,
Your Name""")
@dataclass
class Contact:
email: str
name: str
company: str
title: str
last_subject: str
days: str
class DraftGenerator:
def __init__(self, template: Template):
self.template = template
self.created: list[str] = []
def render(self, contact: Contact) -> str:
return self.template.safe_substitute(
name=contact.name,
company=contact.company,
title=contact.title,
last_subject=contact.last_subject,
days=contact.days,
)
def create_draft(self, contact: Contact) -> None:
body = self.render(contact)
subject = f"Following up on {contact.last_subject}"
subprocess.run(
[
"nylas", "email", "drafts", "create",
"--to", contact.email,
"--subject", subject,
"--body", body,
],
check=True,
)
self.created.append(contact.email)
print(f"[{len(self.created)}] Draft created for {contact.name} ({contact.email})")
def batch_create(self, csv_path: str) -> None:
with open(csv_path) as f:
reader = csv.DictReader(f)
for row in reader:
contact = Contact(**row)
self.create_draft(contact)
print(f"Done. Created {len(self.created)} drafts.")
print("Review with: nylas email drafts list")
if __name__ == "__main__":
generator = DraftGenerator(FOLLOW_UP)
generator.batch_create("outbound_list.csv")TypeScript version
A TypeScript implementation with a template registry supporting multiple outreach types.
// batch-drafts.ts — generate personalized email drafts from a CSV
import { readFileSync } from "fs";
import { execFileSync } from "child_process";
interface Contact {
email: string;
name: string;
company: string;
title: string;
last_subject: string;
days: string;
}
type TemplateRenderer = (contact: Contact) => string;
const templates: Record<string, TemplateRenderer> = {
"follow-up": (c) =>
"Hi " + c.name + ",\n\n" +
"I wanted to follow up on our conversation about " + c.last_subject + ".\n" +
"It has been " + c.days + " days and I wanted to make sure nothing fell through the cracks.\n\n" +
"As " + c.title + " at " + c.company + ", you mentioned some challenges I think we can help with.\n\n" +
"Would you have 15 minutes this week to discuss?\n\nBest,\nYour Name",
"meeting-request": (c) =>
"Hi " + c.name + ",\n\n" +
"I have been thinking about what we discussed regarding " + c.last_subject + ".\n\n" +
"Would Thursday or Friday afternoon work for a 30-minute call?\n\nBest,\nYour Name",
};
function createDraft(contact: Contact, templateName: string): void {
const render = templates[templateName];
if (!render) throw new Error("Unknown template: " + templateName);
const body = render(contact);
const subject = "Following up on " + contact.last_subject;
execFileSync("nylas", [
"email", "drafts", "create",
"--to", contact.email,
"--subject", subject,
"--body", body,
]);
console.log("Draft created for " + contact.name + " (" + contact.email + ")");
}
// Parse CSV and create drafts
const lines = readFileSync("outbound_list.csv", "utf-8").trim().split("\n");
const headers = lines[0].split(",");
for (const line of lines.slice(1)) {
const values = line.split(",");
const contact = Object.fromEntries(
headers.map((h, i) => [h, values[i]])
) as unknown as Contact;
createDraft(contact, "follow-up");
}
console.log("Done. Review with: nylas email drafts list");Next steps
Now that you can generate and review drafts at scale, explore these related guides:
- Personalize Outbound Email — when you are ready to send directly without the draft review step, add scheduling and throttling.
- Build Email Autocomplete — speed up manual composition with contact suggestions from your email history.
- Build an LLM Agent with Email Tools — let an AI agent handle the entire read-draft-send loop with tool-calling.
- CRM Email Workflows — the full 8-guide series for turning your inbox into CRM intelligence.