Guide
Email Mail Merge from the Command Line
A mail merge is just a loop: read a row, fill in the name, send the message, repeat. You don't need a spreadsheet add-on or a marketing platform to do it — a CSV and the Nylas CLI cover personalized bulk sends from your own account. This walks through the loop, the cleaner hosted-template version, the throttling that keeps you off a spam list, and the dry-run that stops you from sending 500 broken emails at once.
Written by Qasim Muhammad Staff SRE
Command references used in this guide: nylas email send, nylas email drafts create, and nylas template.
How do you do a mail merge from the command line?
A command-line mail merge reads a CSV of recipients and runs nylas email send once per row, substituting each person's fields into the subject and body. There's no add-on and no upload — the script runs from your terminal against your connected account, sending from your own address. The whole pattern is a while loop over the file plus one send command.
Because the CLI sends through your provider's own infrastructure, each message inherits your domain's SPF and DKIM, so personalized sends land like normal mail rather than from a third-party relay. Keep the recipient list to people who expect to hear from you — a merge that looks like a cold blast trips spam filters fast, regardless of how it's sent.
How do you personalize with a CSV loop?
Store recipients in a CSV with a header row, read it line by line, and expand the fields into the message. The loop below skips the header, splits each row on commas, and sends a greeting personalized with the recipient's name. A one-second sleep between sends keeps the loop from hammering the API and spaces delivery out.
# recipients.csv
# email,name,company
# ada@example.com,Ada,Analytical Engines
# alan@example.com,Alan,Bletchley Ltd
tail -n +2 recipients.csv | while IFS=, read -r email name company; do
nylas email send --to "$email" \
--subject "Quick question, $name" \
--body "Hi $name, I'm reaching out about $company. Do you have 15 minutes this week?"
sleep 1 # pace the loop; stay well under provider rate limits
doneHow do you use hosted templates for cleaner merges?
For anything beyond a one-line body, a hosted template beats string interpolation. You author the template once, reference it with --template-id, and pass each row's values as JSON with --template-data. The template renders server-side, so your loop carries data, not HTML, and a formatting change means editing the template rather than the script. Manage templates with nylas template.
Crucially, --render-only previews the rendered message without sending — run it on the first row to confirm the merge fields resolve before you loop over 500 of them. That single check is the difference between catching a broken placeholder and emailing Hi {{ name }} to your whole list.
# Preview the render for one row — sends nothing
nylas email send --to ada@example.com \
--template-id tpl_welcome \
--template-data '{"name":"Ada","company":"Analytical Engines"}' \
--render-only
# Then loop, sending the rendered template per row
tail -n +2 recipients.csv | while IFS=, read -r email name company; do
nylas email send --to "$email" \
--template-id tpl_welcome \
--template-data "{\"name\":\"$name\",\"company\":\"$company\"}"
sleep 1
doneHow do you throttle and stay deliverable?
Respect the daily cap. A personal Gmail account can send to about 500 recipients per 24 hours, and Google Workspace accounts to 2,000, per the Gmail sending limits; exceed it and Gmail temporarily blocks sending. Pace the loop with a sleep, and split a large list across days rather than punching through the limit in one run.
Deliverability rules apply even to personalized mail. If your merge volume crosses 5,000 messages a day, Google and Yahoo require SPF, DKIM, and DMARC and a spam-complaint rate under 0.3%, per the sender guidelines. A merge that reads as unsolicited bulk mail will get filtered no matter how it's sent — see SPF, DKIM, and DMARC explained.
How do you dry-run before sending for real?
Build a safety net into the script before the first real send. Echo each command instead of running it to confirm the field substitution, or compose drafts with nylas email drafts create so you can review them in the mailbox before any go out. A draft-first pass catches a malformed row that would otherwise reach a recipient.
Guard the loop against empty or malformed input, too: a blank $email should skip, not send to nobody and error mid-run. The pattern below dry-runs by printing, so you read the exact 500 messages before committing. See build reliable email automation for the exit-code and idempotency patterns that keep a bulk script from half-finishing.
# Dry run: print what would send, send nothing
tail -n +2 recipients.csv | while IFS=, read -r email name company; do
[ -z "$email" ] && continue # skip blank rows
echo "WOULD SEND -> $email (name=$name)"
doneNext steps
- Email templates from the CLI — author and manage hosted templates
- SPF, DKIM, and DMARC explained — the records bulk sends need
- Build reliable email automation — guards for loops and scripts
- Personalize outbound email — merge fields and tone at scale
- Full command reference — every flag and subcommand documented