Operations manual · WholeReach

The Schneider Manual

How the marketing-agent method works, what we built from it, how to run it, and what it costs. Written from a 44-minute masterclass by Cody Schneider, then from actually building the thing and finding out where it bends.

What changed on 13–14 August

The method as filmed stops once you have a qualified audience. That turned out to be the loudest criticism of it — and the fairest. Sections 4, 5 and 6 are new: the campaign layer that acts on the list, the lead capture that lets any of it be judged, and the tooling that builds a knowledge base for a trade nobody has covered. Section 10 splits everything still missing into what is a week of work and what is genuinely hard.

  1. The one idea
  2. The rule that governs cost
  3. The pipeline, stage by stage
  4. From a list to a department
  5. Knowing a lead happened
  6. A knowledge base for any trade
  7. How to run it
  8. What is built and what is blocked
  9. What we learned the hard way
  10. What is easy, and what is hard
  11. What it costs
  12. The second engine
  13. Where everything lives

1. The one idea

Target what someone just showed interest in, not what industry they happen to work in.

Cold email is collapsing because AI-generated volume has flooded every inbox. Sending more into that is not a strategy. The way through is to only contact people who have just raised their hand.

On LinkedIn, a like or a comment is that hand raise. It says a person cares about a subject right now. Firmographics tell you who someone is; engagement tells you what they currently want. So: watch the accounts your customers read, collect the people who engage, qualify them, find their contact details, and write about the thing they just engaged with.

2. The rule that governs cost

Only use inference when you need it

Do not pay a model to perform each step. Pay it once to help you write the software, then run that software on cheap compute. Per-action tokens are a tax on every activity you automate. This is Cody's own phrasing and it is the difference between a system a small builder can afford and one only an enterprise can.

In our build the model is called in three places, and never once per lead in the pipeline stages:

StageInferenceWhy
Source, dedupenoAPI calls and a set
ICP gate, stage 1noJob titles decide most cases outright
ICP gate, stage 2yesOnly the genuinely ambiguous remainder
Enrich, verifynoOrdered API calls, pass/fail grading
Campaign layer — angles, creative, spendnoDeterministic, and deliberately so — see section 4
Campaign copyyesOnce per campaign; language is the job
Reply intentyesOnce per reply; reading a human

Measured on 962 real people, stage 1 resolved 333 with no model call at all. That saving compounds every single day the system runs.

3. The pipeline, stage by stage

Stage 1 — Choose the source accounts

10–20 accounts whose audience is your customer. Company pages count as much as people — most teams collect only individuals and halve their coverage. Pick them by hand. Cody deliberately does not automate this, and neither do we: a business already knows who its customers follow.

We seeded ours from the knowledge bases, taking each company's LinkedIn URL from its own website rather than guessing it from the name. Guessing produces a page that looks right and silently tracks the wrong firm.

Screen the accounts before you track them. We did not, and paid for it — see the employer-affinity finding in section 9.

Stage 2 — Collect the engagers

Daily: find each tracked page's new posts, pull everyone who reacted or commented, dedupe on the public profile URL. One Apify actor run per company — the company-posts actor scrapes reactions inline, so this is one run rather than one per post.

Stage 3 — The ICP gate, and it goes BEFORE enrichment

This ordering is the whole economics

Enrichment costs money per lookup. Qualifying afterwards means paying to find contact details for people you were never going to write to. Qualifying first is the quality gate and the cost control in one step. It is the detail most people skim past.

Our gate asks two questions rather than one:

Both true is a yes. Role but unclear domain is the ambiguous middle, and that is where stage 2 earns its cost. Domain vocabulary lives in each campaign's config, so magnetics and home building bring their own without touching the classifier.

Current state on the full harvest: home building 135 yes / 522 maybe / 385 no out of 1,042; magnetics 72 / 665 / 225 out of 962. The large middle column is the gate refusing to qualify on role alone. Emptying it is what stage 2 is for.

Stage 4 — Waterfall enrichment, cheapest first

Providers in ascending cost order, only the misses falling through: GitLeads → Apollo → Origami or Prospeo, with LeadMagic for mobile numbers. Roughly an 80% find rate is realistic.

This is written and dry. It does not run.

Every enrichment key was checked on the machine on 13 August and every one is unset. Today the pipeline reaches a LinkedIn profile and stops there. Saying otherwise — which we did, once, in a meeting — is the easiest overstatement to make about this system.

Stage 5 — Verify before sending

Every address through Million Verifier. Enrichment output is not clean enough to send to, and bounce rate is the fastest way to destroy a sending reputation.

Stage 6 — Sending infrastructure

Four domain lanes, kept apart: burner domains for cold, separate for email marketing, separate for transactional, and the real business domain which never sends cold. Ten thousand cold sends from a live domain ruins it — and at our scale that damage would land across the whole network.

Stage 7 — Replies

The sending platform fires a webhook on reply. Classify intent, draft toward a booking link, and connect the calendar back so the system sees whether a meeting actually happened rather than assuming.

4. From a list to a department

New — 13 August

“A list of interested people is not automated marketing.” Tim Costello, thirty years in home building, on the 13 August huddle. His argument: Google, Facebook and LinkedIn all let any business target people interested in a subject, natively, inside their own ad tools. So finding the audience is step one of ten, and nobody pays for step one. He was right, and the other nine steps are this section.

The campaign layer lives in /opt/autoengine/campaign.py, beside the engine rather than inside it, and runs on first activation and on every daily run. It is entirely deterministic — no model calls at all. That is deliberate: a business owner has to be able to ask why a proposal says what it says and get the same answer tomorrow.

It derives the angles the site can actually evidence

Seven candidate propositions — local, speed and guarantee, proof, expertise, price, range, risk-reversal — each declaring what the site must carry before the claim may be made. A speed claim needs the site to already promise speed. A proof claim needs named customers or review data. An angle with no support is not written. A site with no signals at all yields exactly one angle, not seven invented ones.

It writes real creative, four formats per angle

It proposes spending, and asks first

In the shape Tim described: “I want to spend $250 in Google to find out the acquisition cost” → comes back with “it is $32 a lead” → the owner decides. Every proposal names the channel, the amount, the duration, the question it answers, and the rule that stops it. A proposal without a kill condition is a bill.

Both limits are enforced, not advertised

A cost-per-lead cap and a monthly ceiling, exactly as Tim put it: “I will pay $5 a lead, and do not bill me more than $1,000 a month.” An approval that would breach the ceiling is refused with the arithmetic shown. A measured cost per lead above the cap returns a kill verdict rather than a suggestion.

Free channels first — as a gate, not a preference

The engine refuses to ask for money while free work is outstanding

Seven free channels are tracked: on-page fixes, Google Business Profile, organic content, organic social, trade directories, the existing list, and direct outreach. While any is outstanding, propose_experiments() returns nothing and gives free-first as the reason. Paid rents attention and stops the moment you stop paying; the free channels accrue. Running paid first is how a small budget disappears with nothing left behind.

Dawn Jordan arrived at the same rule from the other direction on her first day of testing: “assume we have five dollars and we are starving — how do we get a customer? Ask for ten thousand and someone will suggest a billboard.” So free is the default posture, and staying there is presented as a legitimate outcome rather than a trial.

Onboarding recommends rather than interrogates

Also Dawn's: asking a non-marketer to choose goals and channels is asking them the question they hired you to answer. POST /recommend reads the live audit and answers it — goals and channels already chosen, each carrying the signal it came from. On a real magnetics site it returns “Direct sales, because the site lists 38 products” and “Be findable at all, because there are only 229 words on it.” Everything stays editable: a recommendation you cannot interrogate is a default wearing confidence.

What it is tested against

Twenty assertions cover the guardrails — free-first refuses, the ceiling refuses, the cap judges, a channel with no leads reports no number, and a site with no signals invents no angle. All pass. The layer was then exercised against 259 real workspaces with zero exceptions, because a generator that only works on the site it was developed against breaks 189 others at 6am.

5. Knowing a lead happened

New — 13 August · the keystone

Everything in section 4 — the cap, the kill rule, the cost per lead — enforces against a number that nothing was producing. Until a lead can be counted, a reported cost per lead is a division with an invented numerator.

/opt/wt-leads on port 8956, proxied at /l/ on the client's own domain — so no CORS, no third-party script on a site we manage for someone else, and nothing for an ad blocker to object to.

Two things are recorded, and the difference between them is the point

 What it isWhy it matters
EventSomebody tapped the phone number, the email link or the map link For a trade this is almost the whole story. The lead is a phone call at 7am; we cannot see the call, but we can see the tap that started it — and attribute it.
LeadSomebody typed their details and asked to be called back Rarer, and it creates an obligation. A form nobody answers is worse for the client than no form at all.
A tap is not a lead, and the reporting must never merge them

Some fraction of taps never become calls. Reporting taps as leads makes every cost per lead look better than it is, and that is the single easiest lie a system like this can tell. They are carried as separate numbers with separate words. Likewise observed() returns null, not zero, when nothing is capturing — “no instrumentation” and “no leads” are opposite findings.

Attribution is captured on arrival — utm_source, medium, campaign and content, falling back to the referrer host — and held in sessionStorage for the visit. No cookie, no fingerprint, no identity. The IP is hashed server-side purely to rate-limit. What is stored is: something was tapped, at a time, by someone who arrived from somewhere.

Verified end to end in a browser on 13 August: a tap on the phone number arrived attributed to google / cpc / wells-test / local-angle. Test rows were then deleted so the real count starts clean.

6. A knowledge base for any trade

New — 13 August

A client turned up in a trade we had never covered. The engine did not say so — it borrowed a neighbouring industry's knowledge base and wrote confidently about the wrong field. This is the tooling that means it never has to.

Maps for local trades, LinkedIn for corporate industries

kb_from_maps.py and kb_from_linkedin.py each turn a trade name into a knowledge-base seed. Which one you reach for is not a preference, it is a fact about where that industry exists:

The vocabulary comes from what the industry actually posts

Maps and LinkedIn both tell you who exists. Neither tells you what the trade talks about. That turned out to be sitting unused in data we already had: every engagement row carries the URL of the post that drew it, and a LinkedIn post URL encodes the author's own hashtags.

topics_from_engagement.py reads them and splits the result three ways:

BucketWhat it isWhere it goes
VocabularyThe trade labelling its own postsInto the knowledge base
Repost candidatesOne-off news — an opening, a hire, an award Listed for a human to judge. Never treated as a topic.
Employer affinity#ilovelennar, #webuildpeopletoo, calendar filler Excluded, and the proportion is reported

Magnetics gained fourteen topics this way — Rare Earths weighted 197, Critical Minerals 117, Magnetic Separation, Tramp Metal, Heavy Rare Earths. PolyMagnetics now briefs “The Complete Guide to Rare Earths” instead of something generic.

A borrowed knowledge base contributes nothing

When no knowledge base matches a trade, topics_for() takes nothing from the substitute. Another industry's topics are not this client's subject, and writing articles from them is exactly the failure this tooling exists to prevent.

7. How to run it

# a full dry pass -- touches nothing, proves the wiring
cd /opt/wt-outbound
python3 run.py --domain magnetics.wholereach.com --dry

# harvest engagers across every tracked company page
python3 /root/harvest_all.py        # magnetics
python3 /root/harvest_builder.py    # home building

# qualify by role and domain (breadth ranking is kept, but see section 9)
python3 /opt/wt-outbound/icp_gate.py

# pull the industry's own vocabulary out of the harvest
python3 topics_from_engagement.py magnetics --harvest harvest-magnetics.json
python3 topics_from_engagement.py magnetics --harvest harvest-magnetics.json --write
python3 /opt/autoengine/kb/gather-kb.py magnetics

# build a knowledge base for a trade we have never covered
python3 kb_from_maps.py waterwells --location "Texas" --write

# give already-activated sites the campaign slate (idempotent)
python3 /opt/autoengine/backfill_campaign.py royallswindmill.com --write

# the guardrails -- run these after any change
python3 test_pipeline.py                 # pipeline
python3 /tmp/test_spend.py               # campaign caps, free-first, cost per lead

It runs itself daily at 06:25, before the huddle, via /root/daily-harvest.sh. Yesterday's results are moved aside so new engagement is seen, and fourteen days of snapshots are kept.

8. What is built and what is blocked

StageStateNeeds
Source accountsdone43 magnetics + 60 builders
Apify harvestlive, cappedfree tier exhausted — $39/mo tier
Dedupedone
Breadth rankingkept, distrustednear-worthless in a trade — section 9
ICP gate, stage 1done
ICP gate, stage 2blockeda model API key
Enrichment waterfallwritten, dryOrigami + Million Verifier, ~$85/mo
Verificationwritten, dryMillion Verifier account
Campaign layer — angles, creative, spend proposals, caps, free-firstdone
Lead capture — taps, forms, attributionliveone client site; call tracking for real calls
Onboarding recommendsdone
KB on demand — Maps + LinkedIn + vocabularydoneApify credits per cluster
Campaign copyblockeda model API key
Placing paid adsnot startedGoogle Ads API — about a month
Sendingblockeddomains, inboxes, platform — ~$200/mo
Reply handlingblockedsending platform + model key
Organic enginenextmodel key for drafting

Every unbuilt adapter is written and dry — it does nothing without its key and works the moment one exists. Nothing spends money by accident: an adapter goes live only when its key is present and dry-run has been explicitly disabled. Two conditions, not one.

9. What we learned the hard way

An actor returned the wrong company's data, silently

The first LinkedIn actor we tried — the publisher named in the video — was a profile scraper. Handed a company page it did not error; it returned Satya Nadella's posts. Plausible data for the wrong entity.

Hence the canary

Every harvest now checks that the source post URL actually contains the company we asked for. Rows that fail are discarded and flagged. Without it we would have built a “magnetics people list” out of a Microsoft executive's audience and had no reason to doubt it.

The engine served one client an entirely wrong industry

A water-well drilling client was shown a competitive landscape of Clearscope, Surfer and Frase — SEO software companies. Nothing was fabricated: every company real, every description accurate, the whole thing about the wrong field. That is harder to catch than invention, because nothing looks wrong.

Two causes, compounding. No water-well knowledge base existed. And the engine had read the site's niche as “Royall business” — the company's own name, not its trade — so no keyword could ever have matched.

The engine knew it was guessing and had no way to say so

growth_context() computed a _kb_fallback flag when it substituted a neighbouring industry, and then discarded it. The flag is now returned, the onboarding card warns in plain language when comparisons are borrowed, and a borrowed knowledge base contributes no article topics at all. A system that cannot report its own uncertainty will present a guess as a finding every time.

Breadth is near-worthless as a signal in a trade

We first ranked people by how many different companies they engaged with, on the logic that following several proves industry membership. It surfaced sales staff. We almost concluded the method does not work for our field.

The sort was wrong, not the method — and on the full harvest the size of the error is now measurable. Of the people who genuinely qualify, 86% in home building and 90% in magnetics engaged with exactly one company.

Sales reps follow the whole industry; buyers follow the one supplier they use

The top of the breadth ranking is vendors — a signing service with 85 followers at eleven companies, a Key Accountmanager, a Customer Relationship Manager. Ranking by breadth pushes nearly every real prospect down. Our read is that this is a software-versus-trade split: a software buyer follows many vendors while evaluating, a plant manager follows the one he already deals with. In a big market breadth may work. In a trade, title works.

Most corporate engagement is employees liking their employer

55% of home-builder engagement is employer affinity — #ilovelennar, #thisistmliving, #webuildpeopletoo, national intern day. Those pages are recruiting channels, not trade publications. It also explains why the qualified builder list filled with sales and HR staff rather than buyers.

Magnetics runs the other way: 64% genuine subject matter. Worth measuring before pointing the audience engine at any consumer-facing vertical — and worth screening source accounts on.

Half a local trade has no website at all

Of 56 water-well businesses found in Texas, 31 have no working website — 23 list none at all and 8 list one that does not load. That is 55%, and it is not a gap in the data, it is the state of the trade.

It is also the clearest sales argument the engine has: a business with no site cannot be marketed to by any competitor of ours either, because every marketing tool on the market starts by asking for a URL. The full finding, with the prospect list.

LinkedIn is the wrong instrument for a local trade

Searching it for well drilling returned Tenaris, Schlumberger and 52 other oil-and-gas multinationals. A two-truck outfit in Luling has no company page there at all — the population we wanted is simply not in that corpus, and no filter fixes a missing population.

Statistical significance is a real ceiling, not an engineering gap

A $250 pilot at $32 a lead produces eight leads. You cannot distinguish two creatives from eight leads; telling a 3% conversion rate from a 5% one needs hundreds of conversions. So “test 25 creatives and tell me which works” requires either a much larger budget or a much longer run.

No amount of building fixes this. What a $250 pilot honestly delivers is an approximate cost of acquisition and the ability to kill anything obviously terrible. Promising the first and delivering it beats promising the second.

Read the schema, do not guess the payload

Our first working actor returned nothing because we sent urls where it expected targetUrls. Reading each actor's published input schema also revealed scrapeReactions — which halves the number of runs. Guessing cost an hour; reading saved the budget.

A lexicon must never be built from its own output

The vocabulary extractor splits run-together hashtags back into words using the knowledge base as its dictionary. An earlier run had written raw tags into that same knowledge base as topic names — so rareearths became a dictionary word, matched whole, and never split. It now reads only from prose and ignores anything beginning with a hash.

10. What is easy, and what is hard

Easy — days, and mostly a credential rather than a design problem

MissingEffortWhat it needs
Finding email addresses and phone numbershalf a dayOrigami + Million Verifier, ~$85/mo. The waterfall is written and dry.
Campaign copy, reply handling, deeper qualificationhalf a dayone model credential
A website for a business that has none2–3 daysWe do it in ten minutes by hand. Templating it roughly doubles the addressable market.
Finishing a Google Business Profile2–3 daysHighest free return for a local trade
Vocabulary for the remaining knowledge bases2–3 daysApify credits, not engineering

Hard — and three of these are where a system like this usually starts lying

  1. Knowing a lead happened, properly. Section 5 captures the tap. The call itself needs a tracked number, conversion tracking on a site we may not host, and attribution back to the creative. Several weeks. Every platform that quotes you a cost per lead is making quiet assumptions here.
  2. Significance at pilot budgets. See section 9. Not solvable by building.
  3. Running and optimising paid campaigns. Placing the ad is bounded work. Managing bids, negative keywords and knowing when to kill a loser is judgment. About a month for an honest first version, and expect it to be mediocre until it has run on real accounts.
  4. Publishing to sites we do not host. We write directly to our own network with a snapshot and rollback on every change. Wix, Squarespace or WordPress elsewhere needs a connector each — a week apiece, and they break when the platforms change.
  5. Producing the images and video. The engine writes the shot list. It does not hold a camera. For a trade, a phone photograph of real finished work beats anything generated — so this may be something we ask the client for rather than build.
  6. Vocabulary at real depth. We now extract what an industry hashtags. We do not extract what it argues about. That needs trade publications, forums, and ideally recordings of real sales calls — the difference between an article that is correct and one worth reading.

11. What it costs

ItemCostStatus
Apify harvesting$39/mo tierfree tier exhausted — it died partway through the builder harvest
Origami (enrichment)per lookupnot purchased — costs nothing until used, which is why it is first
Million Verifier~$37/monot purchased — buy the same day as any enrichment
Inboxes and burner domains~$100/monot purchased
Sending platform~$97/monot purchased
Model calls3 points only, never per leadno key yet

$39 a month is what is actually being spent today, across the whole network. Every deferred tool together would add about $410. The engine itself costs nothing per client to run — no model calls in the routine path — which is the single largest cost control in the design and the reason a $140 tier is possible at all.

The full ledger, with what each tool has to return to earn its place. Key state there is read from the machine at build time rather than from memory.

12. The second engine

The other half of the method turns real conversations into published content. Calls, interviews, transcripts, lost deals — the source material becomes posts.

Never ask a model to “write good content.” With nothing to work from it produces the average of everything it has read, which is precisely what platforms now detect. Every piece must start from something a human actually said.

For this network it runs on topic pages rather than personal brands — accounts named for a subject, not a person. That fits our institutional-byline rule exactly, and the evidence says it works at least as well anyway.

The standing question on the huddle page every morning is this engine's intake: one question a day, rotating, because the knowledge that matters keeps arriving.

13. Where everything lives

WhatWhere
Pipeline/opt/wt-outbound/
Campaigns/opt/wt-outbound/campaigns/*.json
Adapters/opt/wt-outbound/adapters/
Vocabulary extractor/opt/wt-outbound/topics_from_engagement.py
KB builders/opt/wt-outbound/kb_from_maps.py, kb_from_linkedin.py
Campaign layer/opt/autoengine/campaign.py
Lead capture/opt/wt-leads/ — port 8956, proxied at /l/
Knowledge bases/opt/autoengine/kb/ — seeds in kb/config/
Ship snapshots/opt/autoengine/ship-backups/<domain>/
Guardrail tests/opt/wt-outbound/test_pipeline.py
Source notes/opt/wt-outbound/source/masterclass-260805.md
Daily cron/root/daily-harvest.sh — 06:25
Skills/opt/wt-marketing-skills/skills/
The method as an appmktgagent.wholereach.com
What the stack costswholereach.com/stack
The trades findingwholereach.com/vertical
Magnetics worksheetwholereach.com/contactsmag
Company indexwholereach.com/huddle/contacts
Daily huddlewholereach.com/huddle