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

SKILL.md

Language Markdown16.0 KB180 lines

skills/client-enrich/SKILL.md

  1. ---
  2. name: client-enrich
  3. description: >-
  4. Enrich Meridian Display Co. quarterly-outreach client stubs with Apollo + web data, per-pipeline,
  5. and write it back per the vault's scraping-dump contract. Use when Matt says "enrich this client",
  6. "enrich the winback cohort", "enrich the check-in list", "enrich the cat 24 batch", or "run
  7. enrichment". Resolves cohorts from `outreach_pipelines` in stub frontmatter across the four
  8. in-scope funnels (24 Partner Tier Upgrade, 26 Winback, 36 Quarterly Check-In, 16 Abandoned Carts),
  9. checks suppression before spending Apollo credits, and tailors the research angle to the play.
  10. Writes only under `## Research`, never `## Human Notes`, sources every claim, and appends one
  11. LEDGER entry. Not for drafting emails (that's matt-guerra-email) or pricing lookups.
  12. ---
  13. # client-enrich
  14. 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.
  15. 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.
  16. 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.
  17. ## Before you start
  18. 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.
  19. 2. Confirm the active session email for LEDGER attribution: `hello@meridiandisplay.example` → **Matt**, `accounts@meridiandisplay.example` → **Hazel**, anything else → `Unknown (<email>)` and flag it.
  20. 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.
  21. ## The four pipelines
  22. | Frontmatter / XML key | Bitrix funnel | cat | Scope | The angle enrichment must serve |
  23. |---|---|---|---|---|
  24. | `partner_outreach` | Partner Tier Upgrade | 24 | routing + audit | Why a partner relationship makes sense **now** |
  25. | `winback` | Winback | 26 | routing + audit | Is this account still alive, and is this still the right person |
  26. | `quarterly_checkin` | Quarterly Check-In | 36 | routing + audit | One specific, recent, dateable thing to open with |
  27. | *(no stub key)* | Abandoned Carts | 16 | audit only | Provenance only (see the cat-16 rule below) |
  28. Nothing outside this table is in scope. If someone says "all the pipelines", it means these four.
  29. ## Step 0: resolve the cohort
  30. 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:
  31. ```python
  32. import glob, yaml
  33. KEY, STATUS = "winback", "active" # partner_outreach | quarterly_checkin | winback
  34. hits = []
  35. for p in sorted(glob.glob("03-Clients/client-stubs/*.md")):
  36. t = open(p, encoding="utf-8").read()
  37. fm = yaml.safe_load(t.split("---", 2)[1]) if t.startswith("---") else {}
  38. b = ((fm or {}).get("outreach_pipelines") or {}).get(KEY)
  39. if b and (STATUS is None or b.get("status") == STATUS):
  40. hits.append((p, b.get("stage"), fm.get("email"), fm.get("user_group_correct")))
  41. ```
  42. Other valid cohort sources, in order of preference when they exist:
  43. - **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.
  44. - **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.
  45. - **A hand-supplied list** of names/emails/stub paths.
  46. Rules when resolving:
  47. - 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.
  48. - **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.
  49. - Report the resolved count and the skips back to the user before running.
  50. ## The enrichment recipe (per client)
  51. Run in order and **stop early when the data is complete**; don't spend credits you don't need.
  52. 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.
  53. 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.
  54. - 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.
  55. 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.
  56. 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.
  57. - **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.
  58. 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.
  59. 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]`.
  60. ## Per-pipeline briefs
  61. ### 24: Partner Tier Upgrade (`partner_outreach`)
  62. Active clients (ordered within 12 months), either never contacted or newly crossed into a higher tier.
  63. - 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.
  64. - 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.
  65. - Tier-upgrade cases: say plainly what changed (headcount growth, funding, new sites). The upgrade conversation is easier with a reason attached.
  66. - `postedLimit: "3months"`. Full recipe including Apollo, since these are often thinly enriched.
  67. ### 26: Winback (`winback`)
  68. 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:
  69. 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.
  70. 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.
  71. 3. **Is there a re-activation trigger?** New locations, event season, rebrand, funding: anything that means new signage.
  72. - `postedLimit: "12months"`, because the point is what happened during the silence.
  73. - **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.
  74. ### 36: Quarterly Check-In (`quarterly_checkin`)
  75. Active, previously contacted, quiet 3+ months. These stubs usually already have person/company basics.
  76. - **Don't re-buy what you already have.** Skip Apollo unless person or company is genuinely `[TBD]`; run posts + web only.
  77. - 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.
  78. - Cross-check the `## Outreach Log`: don't hand back a signal that was already used as an opener.
  79. ### 16: Abandoned Carts
  80. 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.
  81. - **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.
  82. - 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.
  83. - Never draft cart-recovery copy here. That's `matt-guerra-email`, and only if Matt asks.
  84. ## Field map (Apollo people-match → stub)
  85. Frontmatter + Snapshot (update only if currently empty/`[TBD]`, unless told to overwrite):
  86. - `title` ← `person.title`
  87. - `seniority` ← `person.seniority`
  88. - `city` ← `person.city`, `state` ← `person.state`
  89. - `linkedin` ← `person.linkedin_url`
  90. - `company` ← `person.organization.name`
  91. - `company_linkedin` ← `person.organization.linkedin_url`
  92. - `industry`, `company_website` ← the org record. **The script can't write these** (not in its `FM_KEYS`), so fill them by hand.
  93. `## Research → ### Person / LinkedIn`:
  94. - headline (`person.headline`), current title + company + start_date (from `employment_history` where `current: true`), location, LinkedIn URL.
  95. `## Research → ### Company` (extract this TIGHT subset; never dump the raw payload):
  96. - name, primary_domain, website_url, company LinkedIn
  97. - industry, estimated_num_employees, founded_year
  98. - HQ city/state
  99. - `short_description` (the human-readable summary; keep it)
  100. - `annual_revenue_printed` if present
  101. - headcount growth (`organization_headcount_six_month_growth`, `_twelve_month_growth`) as a momentum signal
  102. - latest funding (`latest_funding_stage`, date, amount) if present
  103. - Do NOT copy `keywords`, `technology_names`, `current_technologies`; they're noise.
  104. `## Research → ### Recent News & Signals`:
  105. - Most recent `funding_events[]` (each has a citable `news_url`)
  106. - Notable headcount growth
  107. - Anything from WebSearch (new locations, events, launches), each with its URL
  108. - Pipeline-specific findings from the brief above (contact departure, company closure, upgrade justification), stated plainly and dated.
  109. `## Research → ### Sources`:
  110. - 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.**
  111. ## Writing it back
  112. Use the helper scripts so the write is deterministic and the contract is honored. `--stubdir` is always `03-Clients/client-stubs`.
  113. ```
  114. python3 scripts/enrich_writeback.py --stub "<path to stub .md>" --payload "<path to payload.json>"
  115. ```
  116. - `enrich_writeback.py`: single stub, rebuilds the whole `## Research` block from a payload (schema in its header).
  117. - `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.
  118. - `batch_enrich.py`: batch write-back for posts + frontmatter mode.
  119. - `batch_web.py`: batch write-back for web-search signals (no credits).
  120. - `merge_writeback.py`: batch write-back combining Apollo + web.
  121. - `_stub.py`: the frontmatter helpers the write-back scripts share; not run directly.
  122. All of them:
  123. - update only empty/`[TBD]` frontmatter keys (use `--force` to overwrite),
  124. - write only inside `## Research`, stamped with today's date,
  125. - never touch `## Human Notes`, the title line, `## Order History`, `## Outreach Log`, or `## Links`,
  126. - leave the tier (`user_group`) and outreach (`outreach_type`) wikilinks intact.
  127. **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`.
  128. 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.
  129. 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.
  130. Editing by hand is fine if you follow the same rules. Either way: confirm `## Human Notes` is byte-identical after the write.
  131. **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.
  132. ## After writing (required)
  133. 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.
  134. ## Cost-aware batching
  135. 1. Resolve the cohort, drop the suppressed, and report the count.
  136. 2. Split into (a) corporate emails worth an Apollo match and (b) personal/eCommerce domains where Apollo will likely miss.
  137. 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.
  138. 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.