GTM Engineering · B2B Outbound Operations
Open to GTM / RevOps roles & freelance projects

I build the pipeline
under the tools.

Most outbound breaks upstream of the send. I own the whole loop — ICP → sourcing → enrichment → verification → cold email → CRM — and build the automation when off-the-shelf tools run out.

50,000+ contacts sourced & processed  ·  15,000 cold emails managed
6 production Python tools  ·  565 tests  ·  CI on Linux / Windows / macOS
Gujrat, Pakistan  ·  Remote  ·  UK & US hours

01 — WHAT I DO

Five things,
done properly.

Hire me for one of these, or for the whole loop. Every one has a case study and a client behind it further down the page.

Lead sourcing & qualification

Named decision-makers matching a real ICP, not a directory dump. Hard filters, then a human pass on every row that survives.

50,000+ contacts built

Data cleaning & enrichment

Messy list in, working list out. Deduplicated, geo-filtered, ICP-scored, every contact verified and flagged so you know what is safe to send.

22,700 → 9,567 on one job

Cold email infrastructure

Dedicated sending domains, SPF/DKIM/DMARC, warmup, segmented sequences. The plumbing that decides whether anything lands at all.

15,000 emails managed

Custom GTM automation

When Clay, Apollo or a no-code tool runs out, I build the missing piece in Python — scrapers, enrichers, delivery pipelines.

6 public tools · 565 tests

Market & ICP research

Which industries, company types and titles are actually worth your outreach — with the reasoning written down, before a single email goes out.

Sector mapping & targeting
02 — THE NUMBERS

Every figure here
has a run behind it.

No rounded-up totals, no borrowed team metrics. Where a number needs a breakdown, the breakdown is printed underneath it — including the parts that did not work.

50,000+
B2B contacts sourced and processed
Career total · records built
UK property compliance10,000+
SaaS client10,000
Freelance, mixed ICPs30,000
576
Leads from two sources, deduplicated and delivered
49 minutes · unattended
7%
Reply rate after diagnosis — up from under 1%
15,000 emails · 5 months
25%
Of everyone who replied went on to buy
SaaS client · 70 sent · 70% replied

What those 50,000 actually are

Records I sourced, cleaned and processed across every engagement. Not all of them were mailbox-verified, and saying otherwise would fall apart in the first interview.

The 10,000 from the property job were accurate company records, but most carried a generic inbox — info@, enquiries@. Fine for a switchboard, wrong for cold outreach. Real businesses do use them; they just do not get read by the person who can say yes. That job is the reason every list I have built since is scored by contact type before anything is sent.

How a finished list breaks down
Named + verified · full volume Catch-all · separate domain, low volume Invalid · dropped

Typical split on a finished list. Only the green band gets full sending volume.

03 — CASE STUDIES

Four problems.
What actually broke.

Each one is written the way I'd walk you through it on a call: what the brief was, what went wrong, what I changed, and what it cost me to learn.

UK Property Compliance · Oct 2025 – Mar 2026

The 10,000-contact
database I built by hand

Sole operator of the entire outbound system for a UK property compliance provider — sourcing, enrichment, verification, segmentation, copy, sending, tracking and sales handoff. No team. No licensed CRM. Built from zero.

10,000+
Contacts built solo
The ask

One person to run the entire outbound function. Build the contact database, run the campaigns, keep the CRM straight, maintain sales sheets and invoice logs. No team, no existing list, no tooling budget.

Why it took six months

Because I was the scraper. 150 websites a day. Open site, find contact page, copy email, copy phone, paste into the spreadsheet, repeat. Eight hours a day. Eyes sore by noon.

London property is fragmented and hyperlocal — letting agents, block managers, estate agents, care homes, restaurants — spread across every borough with no single directory covering them, and badly under-indexed by Apollo and Sales Navigator precisely because it's niche. Each borough was its own manual sweep.

The one thing working in my favour: compliance has a natural trigger event built in. Certificates expire on a schedule. A prospect with an expiry coming up is a fundamentally different lead from one without — and targeting on that beat any static "owns property in London" list I could have built.

What broke

Almost every email I collected started with info@.

That's what a website's contact page gives you. Generic inboxes, watched by a receptionist or an auto-responder, nobody with buying authority behind them. I'd built a 10,000-row database that was technically accurate and commercially close to useless for cold outreach. The outreach failed, and for a long time I assumed it was the copy.

The fix

Two changes. First, stop treating "an email exists" as the finish line — the record isn't done until there's a named person with a title attached. Second, source contacts where names actually live: Apollo, company team pages, LinkedIn — not the contact form.

That's also where the three-bucket verification standard came from. Role-based addresses stopped being "a contact with a slightly worse email" and became their own category — dropped from sends entirely, flagged for a different channel if the account was worth chasing another way. Everything in section 04 is the formalised version of what I learned here the hard way.

I asked to automate the sourcing so I could spend that time on qualification instead of copy-paste. The answer was no.

The outcome

They ended up with a 10,000+ contact database segmented by borough, business type and portfolio size, plus a full audit trail across sales sheets, invoice logs and service records — one row per prospect, not per email, so the sales team had a single source of truth rather than a touch log.

What I actually took from it

Living the manual version is why the tools I built later are shaped the way they are. I didn't automate a process I'd read about. I automated the one that had been wrecking my eyes for six months — which is why the first thing my enricher does is go past the contact page.

Cold email · 15,000 sends · 5 months

Under 1% replies.
The list was the problem.

Sequences running, tools configured correctly, 300–400 sends a day for five months. Almost nothing came back. The diagnosis landed around email number 1,000.

<1% ▸ 7%
Reply rate
The symptom

Volume was fine. Deliverability looked fine on the surface. The sequences were built properly and going out on schedule. The replies just weren't there — and the few that came were almost all out-of-office.

Why I checked at email 1,000

A thousand sends is enough that "bad luck" stops being an explanation. At a normal 2–3% you'd expect twenty-odd replies. I had a handful. So I stopped optimising the copy and went back through the list itself.

The diagnosis

99% of those emails were landing in generic inboxesinfo@, contact forms, auto-responders. Not real inboxes. Filters, managed by people with no budget authority and every incentive to mark an unsolicited email as spam.

I proved it by segmenting the sent list by address type. Every reply I'd received — every single one — came from a named contact at a person@company.com address. Same campaign, same product, same period, same copy. Only the recipient type differed.

Underneath that, a second problem: no SPF, DKIM or DMARC checks in place. Every send into a generic inbox that got marked as spam was quietly training filters against the sending domain. Volume wasn't neutral — it was actively making things worse.

The fix
  • Strip every generic address out of the list before a campaign goes live
  • Re-source named contacts by title and company size through Apollo and company sites
  • Verify SPF, DKIM and DMARC before sending anything
  • Segment generic vs named — never send the same sequence to both
  • Personalise by role, company size and a specific pain point, not by mail-merge token

Reply rate on named, personalised contacts: around 7%. Roughly seven times the generic-inbox result.

Same copy. Same tools. Same sender. The only variable that changed was who was on the list.

Context on the number

For reference, my standing benchmark is 8–10% on properly targeted, personalised sends versus 2–3% on bulk — roughly a 3–4× spread. The 7% here is the recovery from a broken campaign, not a best case.

The one-line version

Data quality isn't a nice-to-have, it's the whole campaign. Fix the list before you touch the copy. I burned five months and 15,000 sends learning that.

SaaS client · Narrow ICP

I threw away 930
of 1,000 prospects.

A SaaS company selling into a tightly defined segment. I sourced a thousand prospects. Seventy made the final list, 49 of them replied, and a quarter of those became customers.

25%
Of repliers purchased
Sourcing

Directories and Google Maps for ground-level coverage, because a good part of this segment barely exists on LinkedIn — a Sales Navigator-only approach would have missed it. Then cross-referenced through Sales Navigator and Apollo to attach names and titles.

1,000 raw prospects in the pool. Then the real work started.

Every stage removed prospects. The reply rate is high because the list is small, not the other way round.

Three passes
  • Pass 1 — hard filters. Remove everyone outside the 5–50 employee range, wrong title, wrong geography.
  • Pass 2 — named contact or out. A real person with a real email. Not info@. Not contact@.
  • Pass 3 — manual review. Every remaining contact read individually against the full ICP. Decision-maker title confirmed, company size confirmed, location confirmed.

70 passed. A 7% survival rate.

Why 70 beats 1,000

Because a thousand-row list isn't an asset, it's a liability with a spreadsheet around it. Every unqualified send is a chance to get marked as spam, and that damage lands on the sending domain — which then degrades the sends to the contacts who were qualified.

Volume doesn't just waste effort. It actively taxes the part of the list that would have worked.

The result

Those 70 got a personalised, well-structured email — written against a tight enough ICP that the same pain point was genuinely true for all of them, which is only possible because the list was small.

49 of the 70 replied — 70%. Of the people who replied, a quarter went on to buy. The client was selling a SaaS product, so the purchase rate is the number that mattered to him, and it is the one worth judging the work on.

What counts as a reply here

A human response from the person I emailed. Out-of-office replies, bounces and automated acknowledgements are excluded. A "no thanks" from a real person counts — turning down a relevant offer is still a reply; an autoresponder is not.

The pattern

Most outbound problems are not copy problems. They are not tool problems. They are list problems. Fix the list first.

Internal build · Private repository

4–6 hours per delivery,
down to one command.

Scrape one source. Find the CSV. Clean it in Excel. Import to Sheets. Send the link. Repeat for the next source, then the next client. No history of what had already been scraped. Every job started from zero.

49 min
576 leads, unattended
Before

Four to six hours to deliver a single lead list to one client. Most of it wasn't scraping — it was the cleaning, merging, reformatting and delivering afterwards. And because nothing was stored, I couldn't answer "did we already scrape this?" without opening old folders.

Now
python run_batch.py --tag "digital marketing agencies london" \
       --scrapers google_maps trustpilot

Both scrapers run simultaneously in the background. 118 leads from Google Maps (66% with email, 89% with phone), 458 from Trustpilot (96% with email). Normalised, merged and deduplicated automatically. 576 rows delivered to Google Sheets in 49 minutes, completely unattended.

Architecture
  • Plugin system — any scraper that returns a list of records connects to the pipeline without touching downstream code. Adding a source is one new file.
  • SQLite storage — every run saved with a unique run_id, scraper name and timestamp. Permanent searchable history.
  • FastAPI layer — four endpoints to trigger runs, query results and push to any Sheet.
  • Schema-flexible — Maps returns Rating, Trustpilot returns Trust Score, Yell returns review_count. None of them have to match. The pipeline stores all of them.
  • 100+ automated tests covering the API layer, storage, normalisation and delivery. The captured run in the proof section shows 84 passing in 4.34s — that was the suite at the time of the screenshot; it has grown since.
What broke while building it

The hardest bug wasn't in the scraping — it was a session-timing problem in the multi-engine search tool that feeds this pipeline.

DuckDuckGo uses a POST endpoint. A cold session returns HTTP 202 — not a ban, not an error, just silence. Zero results, no exception, nothing in the logs to tell you anything went wrong.

The first version warmed up every session at startup, then ran Mojeek — which takes 10–15 minutes on a large file. By the time DuckDuckGo's turn came, its session had expired. I was silently losing an entire engine's worth of leads on every run and the tool reported success.

Fix: move the warmup inside the engine loop, immediately before that engine's first request. One line moved. It's in the CHANGELOG.

Second one worth naming: requests.get(url, timeout=10) sets a socket inactivity timeout, not a wall-clock limit. A server that accepts the TCP connection then goes silent will hang the thread indefinitely. On a 1,000-site run, three or four of those stall everything. No error, no exception. The fix is running the request in a daemon thread and joining with a hard wall-clock limit.

Why it stays private

The six tools it orchestrates are public and MIT-licensed. The pipeline that runs them together is not.

It took weeks to get the architecture right and it's the thing that makes a delivery take 49 minutes instead of six hours. That's the product, not a portfolio piece.

The lesson

The bottleneck in most outbound operations is not sourcing. It's everything after the scrape — cleaning, merging, delivering. Build the pipeline around that, and sourcing becomes almost free.

04 — CLIENTS

Six people who
paid me to do this.

Collected and published with permission. Most of this work came through freelance platforms, so I have the feedback but not the right to publish company names — sector and engagement are listed instead.

He ran the whole thing on his own — built the database of letting agents, block managers and estate agents across London, then handled the cold email, the meeting bookings, the invoicing and the sales tracking. Everything sat in sheets we could open any time, so we were never chasing him for an update. We handed him a lot and he got on with it.

UK property compliance company
Outbound & CRM · 6-month engagement

We needed businesses that specifically did not have a website, which is an awkward thing to search for — most lead gen people just could not do it. Afaq worked out how to find and qualify them at scale and we got a steady flow of prospects that actually fit, instead of a generic contact list.

SaaS company
Businesses without a website · Ongoing

He sourced heavy truck repair shops for our export business. It is a narrow market and not an obvious one to search, but he understood the brief straight away and came back with a list we could actually work from rather than a directory dump. Would use him again.

Spare parts exporter
Heavy truck repair sector · Project

I wanted to know which industries would be the best fit for my communication diagnostics course and I honestly did not know where to start. He did the research and came back with a shortlist, and more usefully, the reasoning for each one. Saved me a lot of guesswork before I had even started reaching out.

Corporate trainer
Market & ICP research · Project

He found Shopify and e-commerce store owners who were a genuine fit for our photo editing service — not just a list of online shops. He picked up who we were after quickly, so the leads he sent over were ones we could pitch properly. Easy to brief, and reliable.

Photo editing service
Shopify & e-commerce sellers · Project

We handed over a big messy list and got back something we could use. He cleaned it, stripped the duplicates and checked every contact against our ICP. It is boring work and most people cut corners on it — he did not, and it saved us weeks of working off bad data.

List cleaning & ICP qualification
22,700 records → 9,567 usable

Published with permission · Client names withheld by agreement · References available on request

05 — THE THESIS

Outbound doesn't fail because
people are bad at their jobs.
It fails because the system never learns.

Three teams. One-way information flow. No feedback loop. This is the structural gap I keep finding, and closing it is the actual job.

01 — ICP DEFINITION

Defines the targeting rules

Never sees a reply rate. Defined it once, months ago, and nobody has told them anything since.

02 — SOURCING & QUALIFICATION

Builds the list against those rules

Has no authority to push back, even when the data suggests the ICP is wrong.

03 — EMAIL EXECUTION

Sends and measures replies

Has no idea how the list was built. Knows only that the reply rate is bad — and gets blamed for it.

✕   Reply-rate data never returns to step 01

Why this matters

Nobody in that chain is doing their job badly. It isn't a skills gap — it's a structural one. Nobody owns the loop between ICP, sourcing and outcome.

The fix isn't hiring a better SDR or rewriting the ICP once. It's closing the loop: reply data feeding back into targeting, continuously.

Where I sit

I've sat on all three sides — defining ICPs, sourcing and qualifying against them, and sending the emails myself.

The moments that changed outcomes were never a better subject line. They were catching that the ICP had quietly gone stale before another 1,000 emails went out against it.

06 — HOW I WORK

ICP to inbox,
the whole run.

This is the sequence I run on every engagement. The order matters more than any single tool in it.

Stage 08 loops back to stage 01 when replies go flat — that return path is the part most teams are missing. Full detail on each stage below.

01
Define the ICP
Four things locked before anyone touches a scraper: firmographic filter (sized by the right proxy — a 3-person agency can manage 400 units, so headcount tells you nothing), geography, trigger event — the thing that makes someone need this now — and decision-maker title, which is often two personas, not one. A vague brief just means re-qualifying the ICP mid-project, and nobody wants to pay for that twice.
02
Source
Different sources do different jobs. Association and council directories via my own scrapers. Google Maps for businesses with a physical presence, cross-referenced to confirm they're still active. Sales Navigator to confirm the actual decision-maker rather than guessing. Apollo as secondary, for verified emails and firmographic gaps.

The decision rule: if it's a structured public directory that generic databases under-index — hyperlocal or association-gated — build a scraper. If the market is broad and well-indexed, Apollo or Sales Nav alone is faster and a scraper adds nothing.
03
Enrich — in waterfall order
Fixed order, so I'm not burning credits on data I already have:
1 · Source data first — whatever the pull already gave me
2 · Apollo / Clay — verified email, LinkedIn, title, size
3 · My own enricher for the gap — HTTP pass on the site, Playwright fallback for JS-rendered or Cloudflare-obfuscated pages
4 · SMTP-level check before anything enters a send list
5 · Phone normalised to E.164 so the dialer doesn't choke
04
Verify — the three-bucket system
Every email lands in one of three buckets before it goes near a sequence. This isn't a judgement call per contact — it's a rule the pipeline applies automatically.

VERIFIED — SMTP RCPT confirms the mailbox exists, MX valid, not role-based. Main sequence, full volume.
CATCH-ALL — domain accepts everything, so the mailbox can't be confirmed. Still sent to, but lower volume, separate sending domain, bounce rate watched closely. First place I look if deliverability dips.
INVALID — no MX, hard bounce, or role-based/disposable. Dropped entirely. Flagged for manual research via LinkedIn if the contact is worth chasing another way.
05
Segment — before writing a word
Trigger present vs. not · size tier · decision-maker seniority · engagement history. Anyone who has opened or clicked before goes into warm re-engagement, not back into cold from scratch.
06
Write — three variants per segment
Tested against each other within a segment, never against the whole list at once, or I can't tell whether the result was the segment or the copy.

A — trigger-led: opens on the deadline. Strongest on high-urgency.
B — proof-led: peers of similar size already doing it. Better on larger accounts.
C — short, curiosity-led: three lines, one question, no pitch. Best on cold segments where a hard pitch dies.

What changes: subject, opener, length, CTA. The offer never changes — only the entry point into the reader's attention.
07
Send
New mailboxes warm up before touching real prospects. Dedicated subdomains, never the client's root domain — that domain is needed for the rest of their business. Conservative volume per mailbox, several mailboxes in parallel, rather than pushing one past a safe cap. Catch-all contacts route through their own lower-volume domain so a problem there never touches the verified list.
08
Measure — and know when to kill it
In priority order: bounce rate is the hard stop — a spike is a domain reputation risk, not a data quality annoyance, so it pauses everything. Spam complaints, near-zero tolerance. Open rate, tracked but not trusted (Apple MPP inflates it). Reply rate, the real quality signal. Meetings booked, the only one the client actually cares about.

What sends me back to stage 01 instead of stage 06: volume healthy, bounce fine, opens fine, but replies flat and the few that come are "wrong contact." That's not a copy problem. That's an ICP problem.
09
Hand off
One row per contact — not per email. The sales team needs a single source of truth per prospect, not a log of every touch.

Company · Contact · Title · Verified Email · Verification Bucket · Phone (E.164) · LinkedIn · Source · Segment/Tier · Sequence + Variant · Reply Status · Meeting Booked · Owner · Trigger Event · First Contacted · Last Touch.

Into HubSpot or Zoho the same schema maps cleanly — bucket and tier as custom properties on the Company, sequence and reply status on the Contact, trigger event in a custom field so a rep opening the record immediately knows why this person was contacted. The shape doesn't change when the CRM does. Only where it gets pushed.
A note on tooling

That engagement ran on structured, audit-tracked Excel, because that's what the client had — no licensed CRM. The pipeline logic above doesn't depend on the tool. The repeatable parts of it — sourcing, qualification, storage, dispatch — are what I later built into an n8n workflow so the same sequence runs without me pushing each stage forward by hand.

07 — PROOF OF WORK

Real runs.
Real output.

Screenshots from actual jobs. Client-identifying data is withheld; run metrics are not.

Click any image to view it full size

08 — WHAT I BUILT

Six public tools.
One private pipeline.

Built because I was the one doing the job by hand first. 565 automated tests across the public repositories, CI running on Linux, Windows and macOS.

Google Maps Business Scraper
122 tests

Resumable scraper pulling business contact data from Maps for any query and location. Clean styled XLSX or CSV out. Survives interruption and picks up where it stopped.

PythonSeleniumResumable
View repository →
Email & Phone Enricher
78 tests

Two-pass contact extraction from any list of company websites. Fast HTTP pass first, headless browser for the rest. Decodes Cloudflare-obfuscated addresses. ~90% hit rate; 1,000 sites in under 45 minutes.

PlaywrightCloudflare decodeAuto-save
View repository →
Trustpilot Business Scraper
72 tests

Extracts contact data and reputation signals from any Trustpilot search. Parallel profile fetching, checkpoint and resume, config-driven. 458 records in under 11 minutes on a live run.

ParallelCheckpointTrust signals
View repository →
LeadHunter Pro
121 tests

Four search engines at once — Mojeek, DuckDuckGo, Yahoo, Bing. Each lies differently, so no single one sees the whole market. Enriches and tiers every result HOT / WARM / COLD / NOISE before it reaches the file.

4 enginesLead scoringAbstract base class
View repository →
JSON Directory Harvester
79 tests

Targets directory JSON APIs directly instead of scraping their front ends. Geo-filtering and deduplication built in — it processes tens of thousands of records in seconds.

JSON APIGeo filterDedup
View repository →
HTML Directory Scrapers
93 tests

Dual-engine directory extraction — Playwright for rendered pages, direct WordPress AJAX where the endpoint is exposed. Config-driven, so a new directory is a YAML file rather than a new script.

PlaywrightWP AJAXYAML config
View repository →
FreshPipe — n8n workflow
BUILDING

The repeatable parts of the manual pipeline, running without me: lead sourcing → AI qualification → CRM storage → email dispatch, end to end. Same logic as the nine stages above, just automated. In progress, not oversold.

n8nAI qualificationCRM sync
Follow on GitHub →
lead-pipeline — the orchestration layer
PRIVATE · 100+ tests

The system that runs the others together. Plugin architecture — any scraper returning a list of records plugs in without downstream changes. SQLite for permanent run history, FastAPI for triggering and delivery, automatic normalisation, deduplication and Google Sheets push. One command, 576 verified leads, 49 minutes, unattended.

Not open source, deliberately. The six tools above are public and MIT-licensed. The architecture that makes them a pipeline instead of six scripts is the part that took weeks to get right — that's the product. Happy to walk through the design in a call.

FastAPISQLitePlugin systemGoogle Sheets APIpytest
09 — STACK

What I run,
and what I'm learning.

Sourcing & data

  • Apollo
  • LinkedIn Sales Navigator
  • Clay
  • Google Maps
  • Trustpilot
  • Business directories & JSON APIs

CRM & ops

  • HubSpot
  • Zoho CRM
  • Airtable
  • Google Sheets
  • Excel — audit trails, invoice logs

Build

  • Python
  • SQL
  • FastAPI
  • SQLite
  • Playwright · Selenium
  • pytest · GitHub Actions

Learning now

  • Clay — depth, not surface
  • n8n
  • AI-assisted GTM workflows
  • Salesforce — the honest gap

Salesforce appears in roughly 45% of GTM engineering postings and I haven't worked in it. Listing it as a skill would be a lie you'd find out about in week one, so it's listed here instead.

10 — WHAT I OWN

What I own today,
and what I'm adding.

No roadmap to competence. This is the work I take responsibility for right now, and the tools I am deliberately going deeper on.

Owned today · end to end
  • ICP definition — firmographic rules, trigger events, and the discipline to revisit them when replies go flat
  • Sourcing & qualification — directories, Maps, Sales Navigator, Apollo, plus my own tooling where those stop
  • Enrichment & verification — waterfall enrichment, three-bucket verification, per-row status flags
  • Cold email infrastructure — dedicated domains, SPF/DKIM/DMARC, warmup, segmented sequences
  • CRM handoff — one row per contact, 16-field schema, mapped into HubSpot or Zoho so sales can work it
  • The automation underneath — Python tooling when the off-the-shelf stack runs out
11 — WRITING

I publish the
teardowns too.

Breakdowns of what broke and how it got fixed. If you want to see how I think before you talk to me, start here.

All posts on LinkedIn →

Tell me what's
broken.

Whether it's a list that bounces, an ICP nobody has revisited in six months, or a delivery process eating half a day per client — describe it and I'll tell you straight whether I can help.