<quick_start>
Manual: "verify phones" / "who can I call" → full pipeline → callable queue in chat + CSV.
Scheduled: Mon & Wed 6:15 AM ET (schedule and task_ids in config.json), after prospect-enrich at 6:00 so freshly enriched leads are verified before the first dials.
Tool order:
execute_sql(Epiphan AI) — leads withphone IS NULL, not customers, USA/Canada; plusenriched[]withsynced:falsefrom~/.claude/skill-state/prospect-enrich/last-run.jsonif present.qualify_lead(Epiphan AI) per contact — Gate Q. Verdict = exclude /STALE AE LEAD/ ATL-GRAY-BTL-NEVER. No Apollo or Clay call before this.apollo_people_match(Apollo plugin, parallel batches) → Clayfind-and-enrich-contacts-at-company/find-and-enrich-list-of-contactsfor misses.hubspot_update_contact+hubspot_create_contact_note(Epiphan AI) per verified phone (loop, ≤100 per run).- Deliver: callable queue (format in
reference/callable-queue-format.md), CSV at~/.claude/skill-state/phone-waterfall/<date>.csv, Slack DM to Tim on scheduled runs. </quick_start>
<house_rules> Customer-facing skill. Before output, run the gates in order (details: ~/.claude/skills/epiphan-house-rules-skill/reference/gates.md): S: search_product_catalog (Epiphan AI) or Epiphan Knowledge search before any product claim. B: check_my_copy on every customer-facing string. Q: qualify_lead right after the candidate pull, before tiering/enrichment/drafts. D: outbound text becomes a Gmail draft, never a send. Identifiers (owner IDs, portal, emails, seats) come from reference/identifiers.md in that skill — do not restate them here. On any gate failure follow reference/self-healing.md (retry, degrade with a flag, alert Tim, halt) and append a line to ~/.claude/skill-runs/.jsonl. </house_rules>
<success_criteria>
- HubSpot pull returns 100+ contacts missing phones across the ICP verticals
- Every contact passed through
qualify_leadbefore any Apollo/Clay call; zero customers, channel partners, device owners, active AE-owned, or suppressed contacts in the queue; stale AE leads listed separately - Apollo phone lookup succeeds for 40-50% (baseline); Clay fills 30-40% of the remainder; 65-70% total
- HubSpot phone field updated for every verified number (or listed as unsynced with reason)
- Final callable queue has 50+ contacts sorted ATL → GRAY → BTL, then ICP score, then intent
- Execution under 8 minutes; Slack alert if longer
- Outcome sidecar + run-log line written </success_criteria>
Pipeline
STAGE 1 PULL STAGE 2 GATE Q STAGE 3 APOLLO STAGE 4 CLAY STAGE 5 SYNC STAGE 6 QUEUE
execute_sql → qualify_lead → apollo_people_match → find-and-enrich-* → hubspot_update_ → ATL-first sort
phone = null exclude / stale / 40-50% (no cost) 30-40% of misses contact (loop) ICP + intent
not customer tier per contact (Clay credits) + note CSV + chat + DM
Stage 1: Pull Leads Needing Phones
MCP Tool: mcp__claude_ai_Epiphan_Ai__execute_sql. Read crm://schema.md / get_schema for hubspot_contacts and hubspot_companies first.
-- strategy: contacts with no phone, not customers, USA/Canada, newest first.
-- Owner, channel, device, product-engager, and suppression verdicts are NOT filtered here:
-- qualify_lead (Gate Q) owns them.
SELECT c.id, c.firstname, c.lastname, c.email, c.jobtitle, c.company, c.country,
c.hubspot_owner_id, c.first_conversion, c.hs_lead_status, c.phone_verified_at, c.createdate
FROM hubspot_contacts c
WHERE (c.phone IS NULL OR c.phone = '')
AND COALESCE(c.lifecyclestage,'') <> 'customer'
AND c.country IN ('United States','USA','US','Canada','CA')
ORDER BY c.createdate DESC
LIMIT 500;
Also merge enriched[] rows with synced:false from the prospect-enrich handoff (phones found but not yet written). Skip contacts whose phone_verified_at is within 30 days (already attempted).
SQL unavailable after one retry → degrade to the previous run's candidate CSV and mark partial (stage: pull); none → error.
Stage 2: Gate Q — qualify_lead (single source for Golden Rules, suppression, and ATL/BTL)
mcp__claude_ai_Epiphan_Ai__qualify_lead with contactId per candidate, writeToHubSpot: false. The verdict decides: customer / device owner, channel partner, product-only engager, AE-owned active (exclude) vs STALE AE LEAD (surface for Tim, not dialed automatically), suppressed, geo, junk, and the ATL / GRAY / BTL / NEVER tier. The rule set, owner IDs, and title tables live in ~/.claude/skills/epiphan-house-rules-skill/reference/gates.md and reference/identifiers.md; this skill no longer carries copies.
Keep per contact: verdict, exclude_reason, tier. NEVER tier → no enrichment spend, listed in the report. qualify_lead unavailable after one retry → halt (list-producing skill), Slack DM Tim, status error (stage: gate_q).
Stage 3: Apollo Phone Lookup
mcp__plugin_sales_apollo__apollo_people_match in parallel batches: first_name, last_name, email, organization_name, domain, reveal_personal_emails: false. Mobile or corporate phone both count. Hit → phone_source = apollo; miss → Stage 4. No cost. Rate limit → exponential backoff, one retry per batch; Apollo down → go straight to Clay, partial (stage: apollo).
Stage 4: Clay Waterfall (Apollo misses only)
Option A (company-level, faster): find-and-enrich-contacts-at-company (companyIdentifier = domain, job_title_keywords from the title, contact data points Email + Summarize Work History) → add-contact-data-points with a custom "phone number from multiple providers" point on the task.
Option B (direct): find-and-enrich-list-of-contacts with {contactName, companyIdentifier} and a custom "verified phone number" point. Poll with get-task.
Expected 30-40% of misses. Clay credits consumed; surface spend. Clay failing / credits exhausted → continue Apollo-only, partial (stage: clay), DM says "Clay failing — check credits".
Stage 5: HubSpot Sync
No batch update tool exists in the Epiphan AI MCP, so the per-contact loop is sanctioned: mcp__claude_ai_Epiphan_Ai__hubspot_update_contact → phone, phone_source (apollo | clay), phone_verified_at = now, phone_waterfall_pass = true; then hubspot_create_contact_note "[phone-waterfall] verified via ". Cap at 100 writes per run; carry the rest to the next run via the CSV with synced:false.
HubSpot writes failing after one retry → keep the queue (it is still dialable from the CSV), list unsynced contacts, partial (stage: sync). All writes failing → Slack DM Tim and halt further writes; queue still delivered.
Stage 6: Callable Queue
Sort: (1) tier from Gate Q — ATL, GRAY, BTL; (2) vertical ICP score (canonical list in sales-revenue-skill: Higher Ed 90 … K-12 65); (3) intent: hiring AV/IT +15, facility expansion +10, Extron/Matrox replacement +12, RFP +10, studio/control-room mention +8, funding +5; (4) engagement recency.
Output: chat table (template reference/callable-queue-format.md, top 10-20), full CSV at ~/.claude/skill-state/phone-waterfall/<YYYY-MM-DD>.csv, by-vertical view, STALE AE LEAD section. Scheduled runs: Slack DM to Tim (own DM) with the completion block below.
Reply-Scan Integration
Reply scans (10 AM, 1 PM, 3 PM) that find leads without phones queue them for the next scheduled run. Urgent path: ICP ≥ 80 and priority high → run Gate Q, then a single apollo_people_match; success → append the phone and urgent_dial = true for today's queue; miss → hold for the next run.
Scheduled Automation
Schedule, timezone, and task IDs are declared once in config.json (schedule, timezone, task_ids); the Monday and Wednesday runs are identical. Order: prospect-enrich 6:00 → this skill 6:15 → prospect-refresh 6:30 (Mon) / morning-brief 7:30.
Operational: >8 min → Slack alert (API slowness or backlog); log the verified rate (aim 65%+); Apollo/Clay rate limits → backoff and retry; Clay error → Apollo-only queue.
Completion DM:
Phone Waterfall Complete (<day> 6:15 AM)
Pulled: {N} · Gate Q excluded: {E} (stale AE: {S}) · Verified: {X+Y} ({%})
HubSpot sync: {synced}/{total} · Queue: ready ({Q} callable) · Status: success|partial
Partial DM adds the degraded stage and the manual action ("continue with Apollo-only queue", "check Clay credits").
<self_healing>
partial = a queue was produced but a stage degraded: SQL pull fell back to the last CSV, Apollo or Clay failed after retry (continue with the other), HubSpot sync failed for some or all contacts (queue still delivered from CSV), run exceeded 8 minutes. error = no candidates could be pulled, or qualify_lead was unavailable (halt: never build a dial queue Gate Q has not seen). Ladder per ~/.claude/skills/epiphan-house-rules-skill/reference/self-healing.md: retry once with backoff → degrade with a flag → Slack DM Tim → halt with state in ~/.claude/skill-state/phone-waterfall/. One JSON line per run to ~/.claude/skill-runs/phone-verification-waterfall-skill.jsonl.
</self_healing>
Emit Outcome Sidecar
Write ~/.claude/skill-analytics/last-outcome-phone-verification-waterfall-skill.json:
{"ts":"[UTC ISO8601]","skill":"phone-verification-waterfall-skill","version":"<frontmatter version>","variant":"default",
"status":"[success|partial|error]","runtime_ms":[ms],
"metrics":{"contacts_pulled":[n],"excluded_by_qualify":[n],"stale_ae_surfaced":[n],"apollo_verified":[n],"clay_verified":[n],
"hubspot_synced":[n],"total_callable":[n],"verification_rate_pct":[n]},
"error":null,"session_id":"[YYYY-MM-DD]"}
partial / error must name the stage in error. No contact names or emails in any field.
Changelog
- v1.0.1 (2026-10-04): Gate Q via
qualify_leadreplaces the three inline / reference copies of Golden Rules + ATL/BTL;golden-rules-filter.mdremoved; IMPLEMENTATION / INTEGRATION_MAP / EXAMPLE_OUTPUTS moved under reference/; schedule + task IDs declared once in config.json; per-contact HubSpot loop sanctioned and capped; sync-halt, run log, typo fix; frontmatter name = directory. - v1.0.0 (2026-03-15): Initial release.