The Mine Works
← All posts
tutorial October 8, 2026 · 11 min read Updated October 8, 2026

Find the Decision Makers at Your Target Accounts by Job Title

Turn a list of company domains into a CRM-ready CSV of people by job title, with LinkedIn URLs, labeled emails and a company check on every row.

Try the scraper

The actor referenced in this article. Pay only for results delivered.

View the scraper →

You have a list of accounts. Marketing picked them, or sales did, or they came out of a fit model. What you do not have is the people: who runs marketing at each one, who the CEO is, and how to reach them. Filling that in by hand is twenty minutes of LinkedIn searching per account, and the database tools that do it for you sell a seat and a contract.

Try it live: B2B Leads Finder, Like Apollo. Pay per result delivered. Failed and empty results are never charged.

The B2B Leads Finder takes up to five company domains per run, searches the public web for LinkedIn profiles of people at each one, labels every person against the job titles you name, and adds a business email with a plain statement of how it was made. This guide turns that into an account list for a CRM, and it is honest about the rows you should not send to.

What you get and who it is for

FieldWhat it holds
company, domainThe account, as you named it
name, job_title, linkedin_urlThe person and their LinkedIn headline, from search results
title_matchtrue when the headline matches one of your job titles
emailA business email at the company’s domain, when one could be built
email_confidencefound (published on the company site), pattern_matched (built from a format the site revealed) or guessed (built from the most common format)
email_noteThe same thing in words, on built addresses
email_source_urlThe page a found address came from
phone, phone_sourceThe company switchboard from its own site, never a direct line
sourcelinkedin-serp or website-scrape
chargedtrue on person rows, false on free company inboxes like info@

This is for account-based sales and marketing teams who already know which companies they want, agencies building contact lists for a client’s named accounts, and founders doing their own outbound to a short list. It is not a database export: every row is found live, during your run, from public search results and the company’s own website. If the account list itself does not exist yet, US business listings by category and city are one way to build one.

The input

{
  "companies": ["stripe.com", "notion.so", "linear.app", "vercel.com", "supabase.com"],
  "jobTitles": ["CEO", "Head of Marketing", "VP Marketing"],
  "maxLeadsPerCompany": 5,
  "scrapeWebsite": true,
  "monitorMode": false
}
  • companies is the only required field. Give domains, not names: an email is only built when the actor has a domain to build it on. Five companies per run is the limit, and any extra are skipped and never charged.
  • jobTitles sets who you are after. The first three are searched by name, and every lead is then labeled title_match true or false against the full list, with matches delivered first. Leave it empty to get every role the search turns up.
  • maxLeadsPerCompany defaults to 10. A run returns at most 25 leads across all companies, so with five accounts at 10 each, the first two or three can use up the cap. Set it to 5 when you send five companies, so each account gets a share.
  • scrapeWebsite is on by default. The actor reads the home page and the team, contact and about pages it links to (7 pages and 20 seconds at most per company). Addresses published there come back as found, and they teach the actor the company’s real email format.
  • proxy can stay as it is. The residential default is included in the price.

What the output looks like

These rows are from our run yisuDxWL4YOiM3Z5X on October 1, 2026, with stripe.com and notion.so, job titles CEO and Head of Marketing, and maxLeadsPerCompany at 10. Names, emails and profile URLs are masked here.

[
  {
    "company": "Stripe",
    "domain": "stripe.com",
    "name": "P*** C***",
    "job_title": "Stripe CEO",
    "title_match": true,
    "linkedin_url": "https://www.linkedin.com/in/***",
    "email": "p***.c***@stripe.com",
    "email_confidence": "guessed",
    "email_pattern": "first.last",
    "email_note": "Guess: first.last is the most common format and stripe.com publishes no staff address to learn from. Mailbox not verified, check before sending.",
    "phone": "+1 888 926 2289",
    "phone_source": "company-website",
    "source": "linkedin-serp",
    "charged": true
  },
  {
    "company": "Notion",
    "domain": "notion.so",
    "name": "S*** N***",
    "job_title": "CEO at Colorful Notion",
    "title_match": true,
    "linkedin_url": "https://www.linkedin.com/in/***",
    "source": "linkedin-serp",
    "charged": true
  },
  {
    "company": "Notion",
    "domain": "notion.so",
    "name": "E*** R***",
    "job_title": "Notion",
    "title_match": false,
    "linkedin_url": "https://www.linkedin.com/in/***",
    "source": "linkedin-serp",
    "charged": true
  }
]

The run hit the 25-lead cap: 13 Stripe rows (three more than the 10 asked for, because a search page already fetched is delivered in full) and 12 Notion rows. The dataset held 27 items, the extra two being a free summary row and a free info row. Read the rows closely, because this is what an account list built from web search really looks like:

  • Every Stripe email was a guess. 12 of 13 Stripe rows carried a first.last address marked guessed, because Stripe publishes no staff addresses for the actor to learn from. The 13th had no email at all.
  • No Notion row had an email. notion.so has no MX records, so the actor built no addresses there. If a company’s staff use a different mail domain from its website, give that domain instead.
  • Three of the six CEO matches were the wrong company. At Notion, all three rows marked title_match: true ran other businesses: Colorful Notion, Foster That Notion and notion innovators corp. The actor had already rejected 12 wrong-company candidates in this run (the summary row counts them in candidates_rejected_wrong_company), but a short company name lets some through.
  • Headlines are not job titles. job_title is the LinkedIn headline from the search snippet. Five of 25 were just the company name, and five were cut off with .... A person whose headline is only the company name gets title_match: false whatever their real title is.
  • Nobody matched Head of Marketing at either company in this run, so a title you ask for is not a title you are promised.

For a different picture, our run SyXLLtsW1xWUJAs6V on September 30 with nwapllc.com, a small accounting firm, returned five people from the firm’s own team page, each with email_confidence: "found" and the page in email_source_url. Small companies that list their staff are where the emails are real. Large companies rarely publish staff addresses, so expect guesses there.

Run it in Apify Console

  1. Open https://apify.com/themineworks/b2b-leads-finder and click Try for free.
  2. In Target companies, enter up to five domains, one per line, most important first.
  3. In Job titles to target, enter the roles you want, most important first, since only the first three are searched by name.
  4. Set Max leads per company to 5 for a five-company run.
  5. Leave Scrape company website for emails on, then click Start.
  6. Open the Output tab when the run finishes. The last two rows are the run summary and an info row: check companies_skipped, engine_refused and candidates_rejected_wrong_company in the summary.
  7. Click Export and choose CSV. Before importing it anywhere, drop the two report rows and the rows with charged set to false (company inboxes).

Run it from Python

pip install apify-client

Put your accounts in target_accounts.csv with a domain column. The script sends them five at a time, then adds two columns for whoever reviews the list: email_action from the email label, and company_check for headlines that may belong to another business.

import csv
import re
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
JOB_TITLES = ["CEO", "Head of Marketing", "VP Marketing"]

with open("target_accounts.csv", newline="", encoding="utf-8") as f:
    domains = [row["domain"].strip() for row in csv.DictReader(f) if row["domain"].strip()]

CONNECTORS = {"", "at", "@", "of", "for", "in", "|"}
LEGAL = re.compile(r"\b(inc|corp|llc|ltd|gmbh|pvt)\b")

def company_check(headline, company):
    h = re.sub(r"[‎‏]", "", headline or "").lower()
    c = company.lower()
    i = h.find(c)
    if i < 0:
        return "company not in headline"
    before = h[:i].split()
    word = before[-1] if before else ""
    if word not in CONNECTORS:
        return f"another company? '{word} {c}'"
    if LEGAL.search(h[i:].split("|")[0]):
        return "another company? different legal name"
    return ""

EMAIL_ACTION = {
    "found": "ok, published on the company site",
    "pattern_matched": "verify, built from the company's format",
    "guessed": "verify before sending, format guessed",
}

leads, summaries = [], []
for start in range(0, len(domains), 5):
    batch = domains[start:start + 5]
    run = client.actor("themineworks/b2b-leads-finder").call(run_input={
        "companies": batch,
        "jobTitles": JOB_TITLES,
        "maxLeadsPerCompany": 5,
        "scrapeWebsite": True,
    })
    for item in client.dataset(run["defaultDatasetId"]).iterate_items():
        if item.get("_type") == "summary":
            summaries.append((batch, item))
        if item.get("_type") or not item.get("charged"):
            continue  # report rows and free company inboxes
        item["email_action"] = EMAIL_ACTION.get(item.get("email_confidence"), "no email")
        item["company_check"] = (
            company_check(item.get("job_title"), item["company"])
            if item.get("source") == "linkedin-serp" else "")
        leads.append(item)

leads.sort(key=lambda r: (r.get("domain", ""), not r.get("title_match"), bool(r["company_check"])))
fields = ["company", "domain", "name", "job_title", "title_match", "linkedin_url",
          "email", "email_confidence", "email_action", "company_check",
          "source", "email_source_url", "phone"]
with open("decision_makers.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
    writer.writeheader()
    writer.writerows(leads)

for batch, s in summaries:
    if s.get("engine_refused") or s.get("companies_skipped"):
        print(f"Run again later: {batch} (search refused or companies skipped)")
ready = [r for r in leads if r.get("title_match") and not r["company_check"]]
print(f"{len(leads)} leads, {len(ready)} title matches with a clean company check")

On the 25 rows from our October 1 run, company_check flagged six. It caught all three wrong-company CEO matches at Notion, a consultant whose headline says he helps businesses with Notion, one Stripe row whose cut-off headline never names Stripe, and one row that is a real Notion cofounder with the headline “Building Notion”. That last one is why the column is a flag for a person to look at, not a filter that deletes rows. Opening the profile settles each one.

For the CRM import, map name, job_title, linkedin_url and email to contact fields, and keep email_confidence as a custom field so your sequencer can treat a found address differently from a guessed one. Send the guessed and pattern_matched addresses through a verifier first. Our bulk email verification guide shows what a check can and cannot tell you.

Automate the handoff

If your account list lives in a sheet or a CRM view, run the actor from Make, Zapier or n8n: read five domains, start a run, wait for it to finish, and write back only the rows with a charged value of true and no _type. Keep the company_check logic in a code step, or skip it and route every title_match: true row to a review queue before it reaches a sequence.

When the account list is settled and you want to know who joins those companies later, that is a different job with a different setting. See Track New People at Your Target Accounts Every Week.

What it costs

From October 13, 2026 the price is $6.10 per 1,000 leads on the Bronze plan, $4.95 on Silver and $4.00 on Gold and above, plus a flat $0.005 per run. Until October 12 it is $3.75, $3.50 and $3.00 per 1,000 leads with no run fee.

You pay for person rows only. The summary and info rows, company inboxes, companies past the fifth, wrong-company candidates the actor rejects, and searches an engine refuses are never charged.

  • Our October 1 run: 25 leads. From October 13 that is $0.1525 plus $0.005, about $0.16 on Bronze, or $0.105 on Gold.
  • A 40-account list: eight runs of five companies at up to 25 leads each, so at most 200 leads. On Bronze that is $1.22 plus $0.04 in run fees, $1.26 in all. On Silver, $0.99 plus $0.04.

Count the useful rows, not the total. In our run, three of 25 rows were CEO matches that passed the company check, and none had a verified email. Budget for review and verification time as well as for leads.

Limits worth knowing

  • Five companies and 25 leads per run. Longer lists need batches, and a big company can fill its share with people outside your target titles.
  • Most emails at large companies are guesses. The actor confirms the domain accepts mail, but cannot test the mailbox, because that needs a port that cloud platforms block. Treat guessed as a starting point.
  • Generic names pull in other businesses. Notion brought in Colorful Notion and three others. Use the company check, and prefer a distinctive domain.
  • Headlines are not titles. A CEO whose headline is just the company name gets title_match: false. Scan the unmatched rows too.
  • Search can be refused. In our September 30 run, the search engine refused requests for stripe.com, so it returned no leads and was skipped at no charge. The summary row says so in engine_refused; run those companies again later.
  • Phones are the company switchboard. The same number from the company’s site is attached to every row from that company.
  • The data is personal. Names, titles and work emails are personal data under GDPR, CCPA and similar laws, and cold email has its own rules. You are responsible for how you use the list.
Related Actor

Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.

Frequently asked questions

Are the emails verified? +

No. The actor checks that the domain accepts mail before it builds an address, but it does not test the mailbox. Each row says how the address was made in email_confidence, and guessed addresses carry a note that the mailbox is not verified. Run them through a verifier before you send.

How many accounts can I put in one run? +

Five companies and 25 leads per run. For a longer account list, split it into batches of five, as the script in this guide does, and set maxLeadsPerCompany to 5 so the run cap does not run out before the last company.

Why did some rows come back with no email? +

An address is only built when the input gives a domain and that domain has mail (MX) records. In our run, notion.so has no MX records, so none of the 12 Notion rows carried an email. One Stripe row, a profile showing only a first name and an initial, also came back without an email.

Why did I get people from a different company? +

Profiles are found through web search, and a short or common name like Notion matches other businesses with Notion in their name. The actor rejects many of these before charging, but some get through. The company check in the script flags them for a quick look.

Apify Store

Find a ready-made scraper for your job

Apify has over 80,000 scrapers and automations, ours included. Start free with $5 of platform credit every month.