Cold Email Engine - 14,000 Leads, Zero Duplicate Sends by Joshua BrownCold Email Engine - 14,000 Leads, Zero Duplicate Sends by Joshua Brown

Cold Email Engine - 14,000 Leads, Zero Duplicate Sends

Joshua Brown

Joshua Brown

The problem
Outbound to trade contractors sounds simple until you build it. You have to find the companies, find a real email, work out what trade they are in, write something that does not read like a template, send it without burning the domain, and never contact the same person twice. Six jobs. Any one of them failing quietly poisons everything downstream, and quiet failure is the default.
How it works
Four scheduled n8n scrapers pull contractors by trade and county and write into a PostgreSQL warehouse. Every record is classified into one of 33 trade buckets from its business name, its category, and its state license prefix, and is left untagged when the evidence is not there, because a guess is worse than a blank. Missing emails are backfilled from the source. A format gate screens addresses before anything expensive touches them. The AI writer is handed the trade as a hard fact and writes to that trade's urgency instead of a generic one. Sends go out through Brevo. Bounces and complaints come back into a suppression table that the next run reads before it spends a dime.
The loop closing is the whole trick. Most outbound setups send forward and never learn anything back.
What it actually does
13,920 contractors in the warehouse, growing about 700 a day. 3,587 of them with a verified email. 2,572 currently claimable, and 95.5 percent of those carry a trade. 847 sends all time with zero duplicates, which is not a rounding claim, it is a query that returns nothing. One hard bounce in the last thirty days.
Three bugs worth more than the features
A claim written before the work it claims. Three separate places in one system marked a record as done before the thing that finishes it had run. When that step failed, the record still looked handled. One of them left a feeder stalled for five days while every dashboard read green. The fix was to make the claim and the work a single statement that returns the rows it claimed, so a claim cannot exist without the work behind it.
A Postgres node handed two statements returns success, not rows. n8n gives you a success object instead of a result set, and an item count of one looks exactly like one row of data. I read that count as a row count twice before I learned to open the payload. Same failure class as the bug I was hunting, which is the honest part of this story.
158 leads that were not dead. A backlog of 267 contractors had no phone number and had been written off. Nobody had gone back and asked the source a second time. I asked once, honestly, and 158 of them came back with a working number.
Stack
n8n, PostgreSQL, GoHighLevel, Brevo, Google Places API, Claude, Railway.
What this means if you are hiring
If you are running outbound, lead generation, or any scraping and enrichment pipeline and you cannot answer how many went out, to whom, and whether any of them bounced from a single query, the system is not finished. I build the part most people skip, which is the loop coming back. And I am the one who gets the alert when it breaks, so it is built to tell me.
Like this project

Posted Sep 24, 2026

An outbound system I built and run: four scrapers, a Postgres warehouse, trade-aware AI copy, and a suppression loop that cut wasted verification 6 to 1.