
Every business is sitting on customers it can't see. Not the ones it's ignoring, the ones it has no way to know about yet, because nothing tells it which company needs it this week.
I wanted to see if an agent could catch that signal for a small business, so I picked one to test it on: a commercial cleaning company. I don't own one. I just needed a real niche with a clear signal, and cleaning has an unusually obvious one.
The best sales lead for a cleaner is a business that just posted a job for an in-house janitor.
Think about what that tells the cleaner. That business has a cleaning problem. It has the budget to solve it, enough to put someone on payroll. And it's acting on it this week. Nobody has to be talked into needing cleaning. You just have to get there before they finish hiring and pitch a contract instead.
That edge is real, and it doesn't last long. So I built an agent to catch those postings around my city, score them, and hand back a short list every couple of days.
It works. The agent reads a posting and can tell an actual cleaning lead from a cleaning company hiring its own crew. But when I looked at the leads it found, I learned something about job boards I wasn't expecting, and it's why this post isn't the victory lap I sat down to write.
Why a job posting beats a scraped list
Every "find leads with AI" tutorial does the same thing. Scrape a business directory, filter by category, call the result a lead list. The trouble is a directory only tells you a business exists. It says nothing about whether that business has a problem today, or money, or any reason to answer the phone.
A job posting tells you all of that. It's intent, not fit. "This is an office, offices need cleaning" is a guess. "This office posted a janitor job on Tuesday" is a business with its hand up.
A job posting is a dated event, and being early is the entire point. That's what finally made a recurring agent worth building instead of a one-off scrape.
What I built
An Apify actor searches Indeed for cleaning roles (janitor, custodian, housekeeping, cleaning, and so on) around my city. A script dedupes the results against a list of what it's already seen, so I only ever look at what's new. That part matters more than it sounds. The whole reason to run it on a schedule is the diff. If it keeps showing me yesterday's postings, it's just noise with a cron job.
Then, for each new posting, the agent applies a rubric and makes a call. Real lead, competitor, or junk. For the real ones it drafts a pitch angle. It writes a dated digest with the leads up top and everything it rejected below, each with a reason.
One decision mattered more than the others. The script does the boring work, fetching and deduping and formatting. The agent does the judgment. That's why the whole thing leans on exactly one outside service (the Indeed scraper) and needs no separate AI key. The thing reading the postings is the agent itself.
The first real run
I pointed it at live Indeed data. It pulled 48 postings for my city. Five turned into leads. Forty-three got thrown out.
The 43 are the good part, because they don't scatter. They fall into a few very tidy piles.
Competitors were the biggest, by a lot. Something like a third of the "cleaning" postings were cleaning companies hiring their own people. A national house-cleaning franchise, a couple of local maid services, one of those big facilities firms that staffs hospital housekeeping. Every one matches a keyword search for "cleaner." Every one is worthless as a lead, because they're the competition. Filter by keyword and half your call sheet is people you're competing against.
Then the right-need, wrong-size pile. A children's hospital, three national hotels, a department store. All hiring cleaning in house, all real, all impossible for a local operator to win, because the hospital runs its own environmental services department and the hotels have national contracts.
A smaller pile of hidden employers. Custodial jobs posted through staffing agencies for an unnamed "government client." Real work, no business you can actually call.
And the plain junk. A pipe layer. A leasing consultant. A car-wash attendant. Words that happened to match, and nothing more.
You can't do any of that sorting with keywords. Telling a business hiring a janitor from a janitorial business hiring janitors is a reading problem, not a matching problem. That's where the agent earns its keep, and on the first run it did.
The fight with the noise
One honest detour first, because it's the kind of thing these posts are for.
My first idea for filtering was a denylist. Drop any posting whose title names some other trade. It barely did anything. On a bigger pull, 150 postings, it caught seven.
Here's why. Indeed pads a thin search with whatever's vaguely nearby. Search "cleaning" in a mid-size city and you get nurses, a riverboat captain (twice), CNC machinists, a veterinarian, line cooks. A denylist is a list of things you remembered to exclude, and nobody remembers to exclude "riverboat captain."
So I turned it around. An allowlist. Keep only titles that actually name a cleaning role, janitor, custodian, housekeeping, porter, EVS (environmental services), and drop the rest by default. The same 150 postings went from 103-to-read down to 55, and the 48 it cut were the captains and the nurses. When the junk is unbounded and the thing you want is small and easy to name, an allowlist wins. I keep relearning that one.
The leads, and the thing I didn't see coming
So, five leads. I lined them up feeling good about myself, then read the company names.
Four of the five were national brands. A national gym chain. A franchised gym. A national student-housing operator. A national restaurant chain. Exactly one was a business I'd call locally owned.
That stung, because it's the mistake I'd just congratulated the agent for dodging. I'd tossed the hospital and the hotels for being too big, decision made at corporate. Then I turned around and called four national chains leads. Same problem, opposite answer.
So I made myself get the rule straight, and it isn't national versus local. It's two questions. Is there actually enough to clean here? A gym has floors and locker rooms every single day; a single chain restaurant has a mop closet. And who picks the cleaning vendor, this location or headquarters? An independent decides for itself. A local franchisee usually does too. A big managed property usually decides at the property. A national restaurant chain and a department store decide at HQ, and HQ already signed a contract.
Run the five through that and it gets more honest. The student-housing property actually survives. It's a big site, and the posting itself said the porter fills in "between third-party service visits," which is them telling you they already pay outside cleaners and just want more hands. That's a real lead in a national costume. The chain restaurant doesn't survive: tiny footprint, corporate buying. The gyms land somewhere in the middle, since a franchisee can pick its own vendor.
But sharpening the rubric only treated the symptom. The real problem is bigger, and it's the finding I actually walked away with.
The businesses a local cleaner most wants to win are the ones least likely to be on Indeed.
The small independent shop, the exact customer a local cleaner can win, mostly doesn't post to Indeed. They hire the owner's nephew. They tape a sign to the door. They ask a Facebook group. The businesses that do post to Indeed, constantly and at volume, are national chains with HR departments and cleaning companies staffing their accounts. Which is to say, the targets a local cleaner can't win and the competitors it's already up against.
The signal is real. A job posting genuinely means a business is spending money on a cleaning problem right now. The agent was just reading it off a source that reliably surfaces the businesses a cleaner can't win and hides the ones it can.
What it actually means
I can't filter my way past that. It's the source. If you want local independents at the moment they need a cleaner, a national job board is the wrong window to watch. The right one is wherever local businesses show up because they're local. New business-license filings. Building permits. A brand-new Google listing. Different signal, different plumbing, another post.
I set out to build a lead machine and I built one. It runs, it's cheap, it reads postings better than any keyword filter I could write. And it works. It just works for a specific target. If the cleaner is going after mid-size local sites (gyms, student-housing properties, franchised locations that pick their own vendors), the job board points straight at them, because those are the businesses that post. It's the tiny owner-run shop it can't find. So the honest takeaway isn't "the source is wrong," it's aim the machine at the segment the source can actually show you. The hard part was never finding businesses with a need. It was pointing it at the ones you can reach and win.
Build this yourself
The whole thing is a couple hundred lines of stdlib Python and one prompt. The shape of it:
For the scraper, use an Apify Indeed actor (misceres/indeed-scraper runs about $3 per thousand postings, pay per result, no Indeed login). It takes one job title per run, so loop your keywords and dedupe the overlap. At this volume, one metro, a few cleaning keywords, every couple of days, it stays inside Apify's free tier.
Filter in two passes. Keep the search keywords cleaning-specific; dropping "facilities maintenance" and "groundskeeper" cut a mountain of repair techs and landscapers. Then, before anything reaches the judgment step, run the title allowlist and keep only titles that name a cleaning role. That's the riverboat-captain filter.
The judge is the part worth stealing. The rubric that matters: a lead is a business hiring cleaning staff in house. A competitor is a cleaning or facilities company hiring its own crew, so exclude it, and catch it every time, because it's the most common false positive. Then score a lead on two things, not on the brand. Is there a real, recurring cleaning surface here, and is the vendor chosen at this location (independent, franchisee, property) or up at corporate. A big local site that buys its own cleaning is a real lead even under a national logo; a small corporate-procurement chain isn't. Have it output a lead flag, a competitor flag, a score, a confidence, and a one-line pitch. Keep the confidence honest, because an open req proves the need exists, not that they'll sign a contract instead of hiring. Mine topped out around 55 percent, and that's the truth of it.
One thing that keeps it simple. Don't pay a second AI service to do the classifying. If you build this as an agent skill, the agent already reading the postings is your classifier. One dependency, no extra key.
If you sell to local businesses
I built this for cleaning, but it's the same machine for anyone selling to local businesses. Swap the keywords and the rubric and it's a landscaper's tool, or an MSP's. If you want one aimed at your market, reply and tell me your niche. I'll tell you the one signal I'd watch for it, and whether a job board is even the right place to watch.
— Ben