Guide

CLI Mail Merge with Send-Time Optimization

GUI mail merge tools lock you into per-seat pricing and can't integrate with data pipelines. The Nylas CLI lets you run mail merge from a script: variable substitution from enriched contact data, conditional content blocks, timezone-aware scheduling, and throttled sends. Works across all major email providers.

Written by Hazik Director of Product Management

Reviewed by Hazik

VerifiedCLI 3.1.1 · Gmail, Outlook · last tested April 11, 2026

Build the contact CSV with merge fields

Mail merge personalization starts with a CSV that contains enough context to make each message feel hand-written. According to Campaign Monitor’s 2024 Email Marketing Report, emails personalized beyond first name — using company, title, and past conversation context — see 26% higher open rates than first-name-only personalization.

The CSV below includes timezone data for send-time scheduling and a last_subject column that references prior conversations. Each row becomes a fully rendered message when the send loop substitutes variables at runtime.

email,name,company,title,last_subject,days_since,timezone
sarah@acme.com,Sarah Chen,Acme Corp,VP Engineering,API integration timeline,14,America/Los_Angeles
bob@globex.com,Bob Martinez,Globex Inc,Head of Platform,Q2 roadmap review,7,America/New_York
yuki@techco.jp,Yuki Tanaka,TechCo,Engineering Manager,SDK performance,21,Asia/Tokyo

Variable substitution in templates

Variable substitution replaces placeholder tokens like ${NAME} with per-recipient values at send time. The Nylas CLI doesn’t have a built-in template engine, so you handle substitution in your script using standard Unix tools like envsubst. This approach gives you full control over rendering and works with any data source — CSV, JSON, or database query output.

Wrap templates in single quotes to prevent the shell from expanding variables before envsubst processes them. According to GNU coreutils documentation, envsubst replaces only variables that are set in the environment, leaving undefined tokens unchanged — a useful safety net when merge fields are missing from a row.

# Template with merge variables
TEMPLATE='Hi ${NAME},

I wanted to follow up on our conversation about ${LAST_SUBJECT}.

Given your role as ${TITLE} at ${COMPANY}, I think there is a good fit.

Would you have 15 minutes this week to discuss?

Best,
Your Name'

# Render with envsubst
NAME="Sarah" COMPANY="Acme Corp" TITLE="VP Engineering" \
  LAST_SUBJECT="API integration timeline" \
  envsubst <<< "$TEMPLATE"

Subject line patterns that get opened

Subject lines determine whether a personalized email gets read or ignored. According to Yesware’s 2024 Email Analysis of 5 million sales emails, subject lines that reference the recipient’s company name get 22% higher open rates than generic alternatives. Reply-style subjects that reference a previous conversation thread produce a 37% reply rate — the highest of any pattern tested.

The Nylas CLI sends the --subject flag value as-is, so you build personalized subjects in your script before passing them. These three patterns cover the most common outbound scenarios and can pull directly from CSV merge fields.

# Reference last conversation (37% reply rate per Yesware)
SUBJECT="Re: ${LAST_SUBJECT}"

# Company-specific (22% higher open rate)
SUBJECT="Quick question for ${COMPANY}'s ${TITLE}"

# Time-based urgency
SUBJECT="Following up — ${DAYS} days since we spoke"

The dry-run send loop

A dry-run loop previews every outbound message without delivering anything. The script below reads outbound_list.csv, substitutes merge variables into the subject and body, and either prints a preview or calls nylas email send depending on the DRY_RUN flag. A 5-second delay between sends keeps delivery rates under provider thresholds — Gmail’s sending limit for workspace accounts is 2,000 messages per day, but bursts of more than 100 messages per minute can trigger temporary rate limiting per Google’s Bulk Sender Guidelines.

Always run with DRY_RUN=true first. Review the output for correct recipient addresses, variable rendering, and subject lines before switching to live mode. One bad merge field in a batch of 500 emails can’t be recalled.

#!/bin/bash
# personalized-send.sh — mail merge with dry-run safety
DRY_RUN=${DRY_RUN:-true}
DELAY=5

while IFS=, read -r email name company title last_subject days tz; do
  [[ "$email" == "email" ]] && continue
  subject="Following up on ${last_subject}"
  body="Hi ${name},

I wanted to circle back on ${last_subject}. As ${title} at ${company}, you mentioned some challenges I think we can help with.

Would you have 15 minutes this week?

Best,
Your Name"

  if [[ "$DRY_RUN" == "true" ]]; then
    echo "[DRY RUN] To: ${email} | Subject: ${subject}"
  else
    nylas email send --to "$email" --subject "$subject" --body "$body" --yes
    echo "Sent: ${email}"
    sleep "$DELAY"
  fi
done < outbound_list.csv

Toggle the DRY_RUN environment variable to switch between preview and live send. The preview mode outputs one line per recipient so you can pipe it to wc -l and confirm the total count matches your CSV.

DRY_RUN=true bash personalized-send.sh    # preview
DRY_RUN=false bash personalized-send.sh   # send

Timezone-aware send-time optimization

Timezone-aware scheduling delivers each email during the recipient’s local business hours instead of blasting the entire list at once. According to Mailchimp’s Send Time Optimization data from 2024, emails delivered between 9-11 AM in the recipient’s local timezone get 14% higher open rates than emails sent at a single fixed time across all timezones.

The Nylas CLI --schedule flag accepts human-readable time expressions including IANA timezone identifiers. The Python script below reads the timezone column from the contact CSV and schedules each message for 9 AM in the recipient’s local zone. The IANA Time Zone Database contains over 590 timezone entries — use the canonical zone name (e.g., America/Los_Angeles) rather than abbreviations like PST, which are ambiguous.

#!/usr/bin/env python3
"""Send personalized emails with timezone-aware scheduling."""
import csv, subprocess, sys, time

DRY_RUN = "--send" not in sys.argv
DELAY = 5

with open("outbound_list.csv") as f:
    for i, row in enumerate(csv.DictReader(f)):
        subject = f"Following up on {row['last_subject']}"
        body = (f"Hi {row['name']},\n\n"
                f"I wanted to circle back on {row['last_subject']}. "
                f"As {row['title']} at {row['company']}, you mentioned "
                f"some challenges I think we can help with.\n\n"
                f"Would you have 15 minutes this week?\n\nBest,\nYour Name")
        if DRY_RUN:
            print(f"[DRY RUN] To: {row['email']} | TZ: {row['timezone']}")
        else:
            subprocess.run([
                "nylas", "email", "send",
                "--to", row["email"], "--subject", subject,
                "--body", body, "--schedule",
                f"tomorrow 9am {row['timezone']}", "--yes",
            ], check=True)
            print(f"Scheduled ({i+1}): {row['email']} at 9am {row['timezone']}")
            time.sleep(DELAY)

if DRY_RUN:
    print("\nDRY RUN — pass --send to actually send")

Throttling and deliverability safeguards

Throttling controls how fast your script sends messages and prevents provider reputation damage. According to Google’s Email Sender Guidelines (February 2024), senders who suddenly increase volume by more than 2x their normal daily rate trigger spam reputation penalties that can take 1-2 weeks to recover from. These safeguards apply whether you send through Gmail, Outlook, or any other provider the Nylas CLI connects to.

  • 5-second minimum delay between sends. Providers flag rapid-fire sending.
  • Stagger delivery across 2-3 hours with --schedule.
  • Warm up new accounts over 2-4 weeks. Start at 10/day, add 10/day per week.
  • Review the first 3-5 by sending to yourself before the full batch.
  • Monitor bounces: nylas email search "delivery failure" --json

Next steps

The mail merge workflow covered here — CSV contact data, variable substitution, timezone scheduling, and throttled delivery — handles the core outbound use case. These resources extend the workflow with draft review, deliverability verification, and contact enrichment.