Open-source CLI cold email sequence engine. Single binary, SQLite storage, no SaaS.
Built on gws for Gmail API access. Works great with coding agents (Claude Code, Cursor, etc.) or directly from the terminal.
go install github.com/anders/cold-cli/cmd/cold-cli@latestRequires gws for Gmail integration.
# Initialize
cold-cli init
# Check domain deliverability
cold-cli doctor
# Add a sending account (opens browser for Google OAuth)
cold-cli account add you@company.com
# Scaffold example sequence + leads files (optional)
cold-cli campaign init
# Create a campaign
cold-cli campaign create \
--name "q1-outreach" \
--sequence sequence.yml \
--leads leads.csv \
--accounts you@company.com
# Review the full schedule before sending anything
cold-cli campaign preview q1-outreach
# Activate when ready
cold-cli campaign activate q1-outreach
# Send due emails (run manually or via cron)
cold-cli tick
# Check results
cold-cli stats q1-outreach
cold-cli stats q1-outreach --variants # A/B test results
cold-cli log # recent activitySequences are YAML files with steps, delays, and optional A/B variants:
name: Q1 Agency Outreach
defaults:
from_name: "Anders"
steps:
- step: 1
delay: 0
subject: "{{first_name}}, quick question about {{company}}"
body: |
Hi {{first_name}},
Saw that {{company}} is growing fast...
variants:
- subject: "{{company}} + lifecycle emails"
body: |
Hi {{first_name}}, wanted to reach out...
- step: 2
delay: 3
body: |
Hey {{first_name}}, circling back...
- step: 3
delay: 5
body: |
Last note — just wanted to make sure this didn't get buried.delayis in days after the previous step- Steps without a
subjectsend as replies in the same thread {{placeholders}}are replaced from CSV columnsvariantsenable A/B testing (assigned per lead at creation)
email,first_name,company
john@acme.com,John,Acme Inc
jane@bigcorp.com,Jane,BigCorpemail is the only required column. All other columns are driven by what {{placeholders}} your sequence uses. cold-cli validates this at campaign creation.
cold-cli init # set up ~/.cold-cli/ directory, database, config
cold-cli doctor [domain...] # check MX, SPF, DKIM, DMARC, domain age
cold-cli account add <email> # add sending account with OAuth
cold-cli account add <email> --no-login # add without OAuth (already authed)
cold-cli account list # list accounts
cold-cli account update <email> # update settings (--daily-limit)
cold-cli account pause <email> # deactivate, cancel pending sends
cold-cli account resume <email> # reactivate a paused account
cold-cli account remove <email> # deactivate (re-add later with account add)
cold-cli campaign init [directory] # scaffold example sequence.yml + leads.csv
cold-cli campaign create --name --sequence --leads --accounts [--start-date YYYY-MM-DD]
cold-cli campaign create --name --sequence-inline '...' --leads-inline '...' --accounts # no files needed
cold-cli campaign clone <source> --name <new> --leads <csv>
cold-cli campaign add-leads <name|id> --leads <csv> # or --leads-inline '...'
cold-cli campaign remove-lead <name|id> <email> # remove one lead from a campaign
cold-cli campaign preview <name|id> # see full schedule before activating
cold-cli campaign preview <name|id> --render # see rendered emails for first lead
cold-cli campaign preview <name|id> --render --lead <email> # render for specific lead
cold-cli campaign activate <name|id> # start sending
cold-cli campaign pause <name|id> # stop sending
cold-cli campaign resume <name|id> # resume
cold-cli campaign status <name|id> # details + reply rate + next/last send
cold-cli campaign list # list all campaigns (with send window + days)
cold-cli campaign update <name|id> # update sequence, send window, timezone, gaps
cold-cli campaign delete <name|id> # delete campaign and all data
cold-cli campaign retry <name|id> # reset failed sends back to pending
cold-cli campaign retry <name|id> --step N # retry only failed sends for step N
cold-cli tick # process replies, bounces, send due emails
cold-cli tick --dry-run # show what would happen
cold-cli stats [campaign] # sent/replied/bounced per campaign
cold-cli stats <name> --leads # per-lead breakdown
cold-cli stats <name> --variants # A/B test results with reply rates
cold-cli log [campaign] # recent activity (sends, replies, bounces)
cold-cli log --limit 50 # show more events
cold-cli lead list # list all leads
cold-cli lead list --domain <domain> # filter by domain
cold-cli lead list --status <status> # filter by status
cold-cli lead pause <email> # pause across all campaigns
cold-cli lead resume <email> # undo pause, restore pending sends
cold-cli lead blacklist <email|domain> # blacklist + cancel pending sends
All commands support --json for programmatic use.
All send times are pre-computed when you create a campaign. Each send becomes a row in SQLite with a specific send_at timestamp, assigned account, and variant. This means:
campaign previewshows the exact schedule before you activatetickjust queries for rows wheresend_at <= now- Agents can review and approve the full timeline
tick is a single idempotent command that does everything per invocation:
- Poll inbox for replies → match via In-Reply-To headers → pause lead
- Poll inbox for bounces → detect via thread matching → mark bounced
- Detect unsubscribe requests → auto-blacklist lead globally
- Find sends where
send_at <= nowand campaign is active - Send each email via gws with 90-140 second random gaps
- Respect daily limits, send windows, and send days
Run it manually, via cron (*/10 * * * *), or have an agent call it. All tick activity is logged to ~/.cold-cli/tick.log as structured JSON.
Matches inbox messages to sent emails using In-Reply-To headers. When a reply is detected, remaining sends for that lead are cancelled. With stop_on_domain_reply, all leads on the same domain are paused.
Unsubscribe requests ("unsubscribe", "remove me", "opt out", etc.) are auto-detected and blacklist the lead globally across all campaigns.
Three-strategy fallback:
- Thread matching — NDR shares a Gmail thread with our sent email (catches all formats)
- X-Failed-Recipients header — standard MTA header
- Snippet parsing — extract bounced email from NDR text
Each account gets its own OAuth credentials. Campaigns can use one account or rotate across multiple:
cold-cli account add sender1@company.com
cold-cli account add sender2@company.com
# Single account
cold-cli campaign create --accounts sender1@company.com ...
# Round-robin across accounts
cold-cli campaign create --accounts sender1@company.com,sender2@company.com ...When round-robin is used, all steps for a given lead use the same account (required for Gmail thread continuity).
Clone a campaign with new leads. Copies sequence, settings, and accounts:
cold-cli campaign clone q1-outreach --name q2-outreach --leads new-leads.csvAdd more leads to a running campaign:
cold-cli campaign add-leads q1-outreach --leads more-leads.csvAutomatically skips leads already in the campaign, blacklisted, or bounced.
Check your sending domains for deliverability issues:
cold-cli doctor # auto-checks all account domains
cold-cli doctor example.com # check specific domainChecks MX records, SPF, DKIM (19 common selectors), DMARC, and domain age via WHOIS.
~/.cold-cli/config.yml:
default_timezone: America/New_York
default_daily_limit: 50
min_gap_seconds: 90
max_gap_seconds: 140
send_window_start: "09:00"
send_window_end: "17:00"
send_days: "1,2,3,4,5"
# Unsubscribe reply detection is always on.
# List-Unsubscribe header is off by default (not needed for cold email from personal Gmail).
unsubscribe_header: false
unsubscribe_subject: Unsubscribe- Go — single binary, no runtime deps
- SQLite —
~/.cold-cli/data.db, pure Go driver (no CGO) - gws CLI — subprocess calls for Gmail API (send, list, get)
- Cobra — CLI framework
- log/slog — structured JSON logging to
~/.cold-cli/tick.log
See ARCHITECTURE.md for data model, tick flow diagrams, and design decisions.
MIT