Sheets-driven cold mailing agent: pull leads from Google Sheets → write personalized emails → rotate across multiple SMTP inboxes → send on a cron schedule → track opens/clicks/unsubscribes.
- A Google Sheet of leads
- A Google Cloud service account with access to that sheet
- One or more SMTP mailboxes (Gmail app passwords, etc.)
- Optional: OpenAI API key for AI-written emails
First row = headers. Only Email is required. Flexible header names are accepted:
| Column | Aliases | Required |
|---|---|---|
| email, e-mail | yes | |
| First Name | firstName, name | no |
| Company | organization, org | no |
| Industry | vertical, sector | no |
| Context | notes, personalization, hook | no |
| Class Link | link, url, cta link | no |
| Status | outreach status | no (written back if present) |
| Last Step | step | no (written back if present) |
Share the sheet with your service account email (Editor).
cp .env.example .env
# fill in GOOGLE_*, SMTP_* (or SMTP_INBOXES), and optionally OPENAI_API_KEY
npm installEither use a single mailbox (SMTP_USER / SMTP_PASS) or set SMTP_INBOXES to a JSON array (see config/inboxes.example.json):
# minify the example JSON into one line for .env
SMTP_INBOXES='[{"id":"alex","fromName":"Alex","smtpHost":"smtp.gmail.com","smtpPort":587,"smtpUser":"...","smtpPass":"...","dailyLimit":40,"hourlyLimit":8}]'Behavior:
- Round-robins across healthy inboxes, preferring the least-used today
- Each inbox has its own daily/hourly cap
- Follow-ups stick to the same inbox that sent the first email (so threads stay clean)
- Usage is persisted in
data/inboxUsage.json
npm run dry-run
npm start
npm start -- --name "SaaS founders Q2" --limit 20# Keep this process running — fires on CRON_SCHEDULE (default: weekdays 9am Asia/Kolkata)
# Also starts the tracking server when CRON_WITH_SERVER=true
npm run cron
# Fire one scheduled tick now (useful to test)
npm run cron:once
# Fire immediately on worker boot, then keep schedule
CRON_RUN_ON_START=true npm run cronRe-runs of the same CAMPAIGN_NAME resume paused campaigns and only send the next follow-up when the day offset is due (day 0 → 3 → 7 → 14).
npm run serverEndpoints: /health, /track/open/:emailId, /track/click/:emailId, /unsubscribe, /webhooks/email, /analytics/:campaignId, /inboxes
docker build -t anvil .
# Recommended: cron worker + tracking server together
docker run --env-file .env -p 3000:3000 anvil
# One-shot campaign
docker run --env-file .env anvil node src/workflows/coldMailWorkflow.jssrc/
agents/ # lead extract, write, send, campaign orchestration
services/ # Google Sheets, SMTP, inbox pool, OpenAI
jobs/cronRunner.js
workflows/ # one-shot CLI entrypoint
app.js # tracking + unsub HTTP server
config/inboxes.example.json
data/ # local JSON logs (campaigns, sends, inbox usage, unsubs)