mattrguerra/meridian-skillsmainPrivate on GitHub, published here in full

client-enrich/

client-enrich

Enrich client stubs in the quarterly-outreach vault and write them back without breaking the vault's contract. This skill is a procedure Claude runs: the data comes from MCP tools (Apollo, Apify, WebSearch) that only Claude can call, and the helper scripts just do the deterministic write-back.

This is the single enrichment skill for this vault, shared to both operators rather than forked per person (Matt, 2026-08-11; see the vault's DECISIONS.md). If it needs to behave differently for someone, change the skill; don't copy it.

Enrichment is pipeline-aware. The recipe is the same everywhere; what changes is which cohort you pull, what counts as a reason to reach out, and how much you're allowed to spend.

Before you start

  1. Read 03-Clients/CLAUDE-clients.md (the scraping-dump contract), root MEMORY.md, and 07-Automation/pipeline-routing-rules.md (which funnels are in scope and who is suppressed). The rules there win if anything here conflicts.
  2. Confirm the active session email for LEDGER attribution: hello@meridiandisplay.example → Matt, accounts@meridiandisplay.example → Hazel, anything else → Unknown (<email>) and flag it.
  3. Know the cost rules: Apollo charges 1 credit per matched person (0 if not found) and 1 credit per found company. You MUST confirm credit cost with the user before any Apollo call, stating the total upfront for a batch (e.g. "Enriching 20 contacts will cost up to 20 credits. Proceed?"). Apify is paid per result (~$0.004/profile), so confirm before using. WebSearch is free.

The four pipelines

Frontmatter / XML key Bitrix funnel cat Scope The angle enrichment must serve
partner_outreach Partner Tier Upgrade 24 routing + audit Why a partner relationship makes sense now
winback Winback 26 routing + audit Is this account still alive, and is this still the right person
quarterly_checkin Quarterly Check-In 36 routing + audit One specific, recent, dateable thing to open with
(no stub key) Abandoned Carts 16 audit only Provenance only (see the cat-16 rule below)

Nothing outside this table is in scope. If someone says "all the pipelines", it means these four.

Step 0: resolve the cohort

Stubs are flat in 03-Clients/client-stubs/ (there are no per-play folders any more; older notes referencing 03-Clients/Check-In/ are stale). Pipeline membership lives in each stub's outreach_pipelines block. Pull a cohort from frontmatter, not from a folder:

import glob, yaml
KEY, STATUS = "winback", "active"      # partner_outreach | quarterly_checkin | winback
hits = []
for p in sorted(glob.glob("03-Clients/client-stubs/*.md")):
    t = open(p, encoding="utf-8").read()
    fm = yaml.safe_load(t.split("---", 2)[1]) if t.startswith("---") else {}
    b = ((fm or {}).get("outreach_pipelines") or {}).get(KEY)
    if b and (STATUS is None or b.get("status") == STATUS):
        hits.append((p, b.get("stage"), fm.get("email"), fm.get("user_group_correct")))

Other valid cohort sources, in order of preference when they exist:

  • A routing run: 05-Outputs/partner-runs/routing-candidates_<date>.json, the approved candidate list. Enriching an approved cohort before it's sent is the intended use.
  • The weekly audit workbook: 05-Outputs/Bitrix_Pipeline_Outreach_Audit_<date>.xlsx, one sheet per funnel. Use it when Matt points at a funnel tab.
  • A hand-supplied list of names/emails/stub paths.

Rules when resolving:

  • A client can hold blocks for several funnels (e.g. an active winback plus a lost partner_outreach). The open block drives the angle; closed blocks are history, useful context, not the play.
  • Check suppression before spending a credit. Skip anyone in 07-Automation/data/suppression_list.json (names + emails) and anyone whose stub shows a Not-Interested closure (cat 24 LOSE, cat 36 "Not Interested"). A sizeable share of clients (roughly 150) are suppressed; they're never getting an email, so research on them is wasted money.
  • Report the resolved count and the skips back to the user before running.

The enrichment recipe (per client)

Run in order and stop early when the data is complete; don't spend credits you don't need.

  1. Read the stub. Pull email, first_name/last_name, company, linkedin, user_group_correct, last_order_date, outreach_pipelines, and the ## Outreach Log. Note which fields are already filled vs [TBD]/empty, and what's already been said to this person. A "reason to reach out" that was used last quarter isn't one.

  2. Apollo people-match by email (apollo_people_match, 1 credit if found). The workhorse. A match returns the person and an embedded organization: person + company in one credit. Extract per the field map below.

    • If person.title / person.id come back but most fields are null (name-only), treat as a miss; that's common for personal/small-org domains (@gmail, nonprofit, indie eCommerce). Go to step 4.
  3. Apollo org-enrich (apollo_organizations_enrich, 1 credit if found), ONLY if people-match matched the person but returned no organization, OR missed the person but you have a real company domain. Skip it when step 2 already returned the org.

  4. Company LinkedIn posts (harvestapi/linkedin-company-posts via Apify call-actor, ~$0.002/post). Best source for the reason to reach out. Derive the slug from company_linkedin (linkedin.com/company/example-brightwater-provisions → example-brightwater-provisions). Input: maxPosts: 5, reactions/comments off, and set postedLimit to the window that matters for the play (see briefs below). Pick the 2-4 most outreach-relevant posts (product launches, retail/store expansion, events, hiring in the contact's department, funding) and write them as dated Signals lines, each with its post URL as source. Skip engagement-bait.

    • The Apify connector has not been available on this account (confirmed on the 2026-08-10 run). If it's still missing, skip this step and lean on WebSearch; say so in the run summary rather than silently returning thinner signals.
  5. WebSearch (free): press not on LinkedIn (funding, trade press, event calendars), and person/company basics when Apollo missed. For Winback, this is also the survival check.

  6. Apify LinkedIn profile fallback (harvestapi/linkedin-profile-scraper, ~$0.004/profile), ONLY when Apollo missed the person AND the stub already has a linkedin URL. Never scrape by name. No known URL → leave [TBD].

Per-pipeline briefs

24: Partner Tier Upgrade (partner_outreach)

Active clients (ordered within 12 months), either never contacted or newly crossed into a higher tier.

  • Lead with what the company is doing that needs displays: new locations, retail rollouts, trade shows and event calendars, sponsorships, rebrands, hiring in marketing/events.
  • Confirm the contact is the right contact. Capture seniority and department; if the stub's person is in ops/finance and the marketing or events owner is findable, note that in Signals. It's the most actionable thing you can hand over.
  • Tier-upgrade cases: say plainly what changed (headcount growth, funding, new sites). The upgrade conversation is easier with a reason attached.
  • postedLimit: "3months". Full recipe including Apollo, since these are often thinly enriched.

26: Winback (winback)

Dormant 12+ months. The copy lives in Bitrix's cat-26 automation, so enrichment is not feeding a drafted email. It's feeding the judgment call on whether to advance the deal. Answer three questions and put each under Signals:

  1. Is the company still operating? Closure, acquisition, rebrand, dead domain. Web search first: it's free, and it stops you buying an Apollo credit on a company that no longer exists.
  2. Is the contact still there? Job change is the single most common reason an account went quiet, and the funnel has a "Changed Companies" stage for exactly this. Compare Apollo employment_history (current: true) against the stub's company. If they've moved, say so, name the new employer, and name a successor at the old company if one is findable. That's two live leads out of one dead one.
  3. Is there a re-activation trigger? New locations, event season, rebrand, funding: anything that means new signage.
  • postedLimit: "12months", because the point is what happened during the silence.
  • Scale guard: the winback-eligible pool runs to several hundred clients. Never enrich the whole cohort. Enrich the slice about to be advanced out of the client bucket, in batches, with the credit total quoted first.

36: Quarterly Check-In (quarterly_checkin)

Active, previously contacted, quiet 3+ months. These stubs usually already have person/company basics.

  • Don't re-buy what you already have. Skip Apollo unless person or company is genuinely [TBD]; run posts + web only.
  • What you're after is one specific, recent, dateable hook. Set postedLimit to cover the span since last_touch (default "3months"), so the hook is newer than the last conversation.
  • Cross-check the ## Outreach Log: don't hand back a signal that was already used as an opener.

16: Abandoned Carts

Audit-only. It's not a routing target, its deals are titled Cart - <id>, and the people in it are often guests who were never partner clients.

  • If there's no stub in client-stubs/, skip them. Don't create a stub, don't write a scratch file, don't research them. At the end of the run, state the count skipped: one line, no report. The funnel is there for provenance; Matt can see it himself in Bitrix.
  • If a cat-16 person does have a client stub, enrich it under whichever brief matches their real standing (active → 24/36, dormant → 26). The cart itself is not enrichment output; its contents and date live in Plinth/Bitrix.
  • Never draft cart-recovery copy here. That's matt-guerra-email, and only if Matt asks.

Field map (Apollo people-match → stub)

Frontmatter + Snapshot (update only if currently empty/[TBD], unless told to overwrite):

  • title ← person.title
  • seniority ← person.seniority
  • city ← person.city, state ← person.state
  • linkedin ← person.linkedin_url
  • company ← person.organization.name
  • company_linkedin ← person.organization.linkedin_url
  • industry, company_website ← the org record. The script can't write these (not in its FM_KEYS), so fill them by hand.

## Research → ### Person / LinkedIn:

  • headline (person.headline), current title + company + start_date (from employment_history where current: true), location, LinkedIn URL.

## Research → ### Company (extract this TIGHT subset; never dump the raw payload):

  • name, primary_domain, website_url, company LinkedIn
  • industry, estimated_num_employees, founded_year
  • HQ city/state
  • short_description (the human-readable summary; keep it)
  • annual_revenue_printed if present
  • headcount growth (organization_headcount_six_month_growth, _twelve_month_growth) as a momentum signal
  • latest funding (latest_funding_stage, date, amount) if present
  • Do NOT copy keywords, technology_names, current_technologies; they're noise.

## Research → ### Recent News & Signals:

  • Most recent funding_events[] (each has a citable news_url)
  • Notable headcount growth
  • Anything from WebSearch (new locations, events, launches), each with its URL
  • Pipeline-specific findings from the brief above (contact departure, company closure, upgrade justification), stated plainly and dated.

## Research → ### Sources:

  • Every claim needs a source. Apollo-derived: Apollo enrichment (people-match / org-enrich), <date>. Funding: the news_url. Web: the page URL. LinkedIn: the profile/post URL. No source, no claim.

Writing it back

Use the helper scripts so the write is deterministic and the contract is honored. --stubdir is always 03-Clients/client-stubs.

python3 scripts/enrich_writeback.py --stub "<path to stub .md>" --payload "<path to payload.json>"
  • enrich_writeback.py: single stub, rebuilds the whole ## Research block from a payload (schema in its header).
  • patch_research.py: surgical: replace or append individual subsections, leave the others byte-for-byte. Use this on re-enrichment so you don't blow away a good Company section to add one signal.
  • batch_enrich.py: batch write-back for posts + frontmatter mode.
  • batch_web.py: batch write-back for web-search signals (no credits).
  • merge_writeback.py: batch write-back combining Apollo + web.
  • _stub.py: the frontmatter helpers the write-back scripts share; not run directly.

All of them:

  • update only empty/[TBD] frontmatter keys (use --force to overwrite),
  • write only inside ## Research, stamped with today's date,
  • never touch ## Human Notes, the title line, ## Order History, ## Outreach Log, or ## Links,
  • leave the tier (user_group) and outreach (outreach_type) wikilinks intact.

Frontmatter date key: fixed 2026-08-11. The scripts stamp the stub's existing date key, preferring last_modified (what every vault stub and the weekly driver use) and falling back to updated. They no longer hardcode updated:, which used to append a second date key that then drifted. If you see both keys on a stub, it was written before this fix, so delete the stray updated.

Two related fixes landed at the same time: FM_KEYS now includes industry and company_website (previously unwritable, filled by hand), and the frontmatter regexes use [ \t]* instead of \s*. \s* also matches a newline, so a bare key: line read the next line as its value, decided the field was already filled, and silently skipped it. Keys written as key: '' were unaffected, which is why this hid for so long.

Still outside the scripts' write scope: the ## Snapshot table's | Title | row. It will read [TBD] even after frontmatter title is populated; update it by hand if it matters.

Editing by hand is fine if you follow the same rules. Either way: confirm ## Human Notes is byte-identical after the write.

Stay out of the XML. partner-client-master-list.xml is derived from stubs plus Bitrix; enrichment doesn't change identity, tier, or pipeline placement, so it doesn't touch the census. If enrichment turns up a genuine identity or tier correction, fix the stub and flag it. Mirroring into the XML is a separate, deliberate change-set.

After writing (required)

Append ONE entry to root LEDGER.md for the change-set (batch many stubs into one entry), attributed to the active session user, timestamped from the system clock (date). Name the pipeline and cohort in the entry ("enriched 40 of the active cat-26 Winback cohort") so the other operator can see what was covered. Follow the format already in LEDGER.md. This is a session requirement, not optional.

Cost-aware batching

  1. Resolve the cohort, drop the suppressed, and report the count.
  2. Split into (a) corporate emails worth an Apollo match and (b) personal/eCommerce domains where Apollo will likely miss.
  3. Quote the Apollo credit ceiling for group (a) and get confirmation. Run group (b) through web search first (free), reaching for Apify only where a LinkedIn URL exists.
  4. For big funnels (Winback especially), enrich in waves sized to what's actually about to be sent. Research ages; buying it months early is buying it twice.