Qualifies inbound leads against full ICP criteria — company size, industry, use case fit, role/seniority of the person. Checks CRM and existing customer base for duplicates and existing relationships. Outputs a scored CSV with qualification status, reasoning, and pipeline overlap flags. Tool-agnostic — works with any CRM, enrichment tool, or data source.
npx gooseworks install --all # then, in Claude Code, Cursor, or Codex: /gooseworks use the inbound-lead-qualification skill
Takes a set of inbound leads and validates each against your full ICP criteria. Not a fast-pass triage (that's inbound-lead-triage) — this is the thorough qualification step that determines whether a lead is genuinely worth pursuing, and produces a scored CSV for the team.
Load this composite when:
[Inbound Leads] → Step 1: Load ICP & Config → Step 2: CRM/Pipeline Check → Step 3: Company Qualification → Step 4: Person Qualification → Step 5: Use Case Fit → Step 6: Score & Verdict → Step 7: Output CSVOn first run, establish the ICP definition and CRM access. Save to the current working directory or wherever the user prefers (e.g., config/lead-qualification.json).
{
"icp_definition": {
"company_size": {
"min_employees": null,
"max_employees": null,
"sweet_spot": "",
"notes": ""
},
"industry": {
"target_industries": [],
"excluded_industries": [],
"notes": ""
},
"use_case": {
"primary_use_cases": [],
"secondary_use_cases": [],
"anti_use_cases": [],
"notes": ""
},
"company_stage": {
"target_stages": [],
"excluded_stages": [],
"notes": ""
},
"geography": {
"target_regions": [],
"excluded_regions": [],
"notes": ""
}
},
"buyer_personas": [
{
"name": "",
"titles": [],
"seniority_levels": [],
"departments": [],
"is_economic_buyer": false,
"is_champion": false,
"is_user": false
}
],
"hard_disqualifiers": [],
"hard_qualifiers": [],
"crm_access": {
"tool": "HubSpot | Salesforce | CSV export | none",
"access_method": "",
"tables_or_objects": []
},
"existing_customer_source": {
"tool": "HubSpot | Salesforce | CSV | none",
"access_method": ""
},
"qualification_prompt_path": "path/to/lead-qualification/prompt.md or null"
}If lead-qualification capability already has a saved qualification prompt: Reference it directly — don't rebuild ICP criteria from scratch.
On subsequent runs: Load config silently.
lead-qualification capability)inbound-lead-triage (already normalized)If >50% of leads are missing critical fields (company name or person title), recommend running inbound-lead-enrichment first. Ask: "Many leads are missing company/title data. Want me to enrich them first, or qualify with what's available?"
For each lead, check against existing data sources to identify overlaps:
Check 1 — Existing customer?
existing_customer with customer details (plan, account owner, contract status)Check 2 — Already in pipeline?
in_pipeline with deal details (stage, owner, last activity)Check 3 — Previous engagement?
previously_contacted with history summary (when, what channel, outcome)Check 4 — Known from signal composites?
signal_flagged with signal type and dateEach lead tagged with:
pipeline_status: new | existing_customer | in_pipeline | previously_contactedpipeline_detail: One sentence explaining the overlap (or null)signal_flags: Any signal composite matchesFor each lead's company, evaluate against every ICP company dimension:
Dimension 1 — Company Size
match | borderline | mismatch | unknownDimension 2 — Industry
match | adjacent (related but not core target) | mismatch | unknownDimension 3 — Company Stage
match | borderline | mismatch | unknownDimension 4 — Geography
match | borderline | mismatch | unknownDimension 5 — Use Case Fit
strong_fit | moderate_fit | weak_fit | no_fit | unknownEach lead gets a company_qualification block:
{
"company_size": { "score": "", "value": "", "reasoning": "" },
"industry": { "score": "", "value": "", "reasoning": "" },
"stage": { "score": "", "value": "", "reasoning": "" },
"geography": { "score": "", "value": "", "reasoning": "" },
"use_case": { "score": "", "value": "", "reasoning": "" },
"company_verdict": "qualified | borderline | disqualified | insufficient_data"
}For each lead's contact person, evaluate against buyer persona criteria:
Dimension 1 — Title/Role Match
exact_match | close_match | adjacent | mismatch | unknownDimension 2 — Seniority Level
match | too_junior | too_senior | unknownDimension 3 — Department
match | adjacent | mismatch | unknownDimension 4 — Authority Type
economic_buyer — Can sign the checkchampion — Wants it, can influence the decisionuser — Would use it daily, can validate needevaluator — Tasked with research, limited decision powergatekeeper — Can block but not approveunknownDimension 5 — Right Person, Wrong Company (or Vice Versa)
right_company_wrong_person — this is a referral opportunityright_person_wrong_company — rare for inbound, but possible with job changersEach lead gets a person_qualification block:
{
"title_match": { "score": "", "value": "", "reasoning": "" },
"seniority": { "score": "", "value": "", "reasoning": "" },
"department": { "score": "", "value": "", "reasoning": "" },
"authority_type": "",
"person_verdict": "qualified | borderline | disqualified | insufficient_data",
"mismatch_type": "null | right_company_wrong_person | right_person_wrong_company"
}This step connects the company's likely needs to your product's actual capabilities. It goes deeper than Step 3's company-level use case check.
Infer the lead's intent from their inbound action:
Map intent to product capabilities:
Assess implementation feasibility:
{
"inferred_intent": "",
"intent_source": "",
"product_fit": "strong | moderate | weak | unknown",
"product_fit_reasoning": "",
"implementation_feasibility": "easy | moderate | complex | unlikely",
"known_blockers": []
}Combine all dimensions into a final qualification verdict.
Composite Score Calculation:
| Dimension | Weight | Possible Values |
|---|---|---|
| Company Size | 15% | match=100, borderline=50, mismatch=0, unknown=30 |
| Industry | 20% | match=100, adjacent=60, mismatch=0, unknown=30 |
| Company Stage | 10% | match=100, borderline=50, mismatch=0, unknown=30 |
| Geography | 10% | match=100, borderline=50, mismatch=0, unknown=30 |
| Use Case Fit | 25% | strong=100, moderate=60, weak=20, no_fit=0, unknown=30 |
| Person Title/Role | 15% | exact=100, close=75, adjacent=40, mismatch=0, unknown=30 |
| Person Seniority | 5% | match=100, too_junior=20, too_senior=60, unknown=30 |
Hard overrides (bypass the score):
disqualified regardless of scorequalified regardless of score (but still show the full breakdown)Verdict thresholds:
qualified — Pursue activelyborderline — Qualified with caveats, may need manual reviewnear_miss — Not qualified now, but close enough to consider (referral or nurture)disqualified — Does not fit ICPSub-verdicts for routing:
qualified_hot — Score ≥ 75 AND Tier 1/2 urgency from triagequalified_warm — Score ≥ 75 AND Tier 3/4 urgencyborderline_review — Score 50-74, needs human judgment callnear_miss_referral — Score 30-49 AND right_company_wrong_person (referral opportunity)near_miss_nurture — Score 30-49, might fit in the futuredisqualified_polite — Score < 30, needs polite declinedisqualified_competitor — Competitor employeeexisting_customer_upsell — Existing customer with expansion signalEach lead gets:
{
"composite_score": 0-100,
"verdict": "",
"sub_verdict": "",
"top_qualification_reasons": [],
"top_disqualification_reasons": [],
"summary": "One sentence: why this lead is/isn't a fit"
}Produce a CSV with ALL input fields preserved plus qualification columns appended:
Core qualification columns:
qualification_verdict — qualified | borderline | near_miss | disqualifiedqualification_sub_verdict — qualified_hot | qualified_warm | borderline_review | near_miss_referral | near_miss_nurture | disqualified_polite | disqualified_competitor | existing_customer_upsellcomposite_score — 0-100summary — One sentence qualification reasoningPipeline check columns:
pipeline_status — new | existing_customer | in_pipeline | previously_contactedpipeline_detail — One sentence on the overlapsignal_flags — Any signal composite matchesCompany qualification columns:
company_size_score — match | borderline | mismatch | unknownindustry_score — match | adjacent | mismatch | unknownstage_score — match | borderline | mismatch | unknowngeography_score — match | borderline | mismatch | unknownuse_case_score — strong | moderate | weak | no_fit | unknownPerson qualification columns:
title_match_score — exact_match | close_match | adjacent | mismatch | unknownseniority_score — match | too_junior | too_senior | unknownauthority_type — economic_buyer | champion | user | evaluator | gatekeeper | unknownmismatch_type — null | right_company_wrong_person | right_person_wrong_companyUse case columns:
inferred_intent — What they seem to needproduct_fit — strong | moderate | weak | unknownimplementation_feasibility — easy | moderate | complex | unlikelyThe current working directory or wherever the user prefers (e.g., leads/inbound-qualified-[date].csv).
After producing the CSV, present a summary:
## Inbound Lead Qualification: [Period]
**Total leads processed:** X
**Qualified:** X (Y%) — X hot, X warm
**Borderline (manual review):** X (Y%)
**Near miss:** X (Y%) — X referral opportunities, X nurture
**Disqualified:** X (Y%)
**Pipeline overlaps:**
- Existing customers: X (route to CS)
- Already in pipeline: X (coordinate with deal owner)
- Previously contacted: X (now warmer — re-engage)
**Top qualification reasons:**
1. [reason] — X leads
2. [reason] — X leads
**Top disqualification reasons:**
1. [reason] — X leads
2. [reason] — X leads
**Data quality:**
- Leads with full data: X
- Leads with partial data (some dimensions scored as 'unknown'): X
- Leads needing enrichment: X
**CSV saved to:** [path]Lead with only an email (no name, no company):
insufficient_data, recommend enrichment or manual reviewSame company, multiple leads:
Contradictory signals:
right_company_wrong_person routes this to referral handling in disqualification-handlingBorderline calls:
Scoring with missing data:
insufficient_data regardless of score — recommend enrichment firstAssemble an expert/educator motion-graphic LISTICLE video ad from a config — a spoken authoritative voiceover carries a numbered listicle while N web-animated hyperframe beats (HTML plus the Web Animations API, one branded design system of alternating tiles, big hero numerals, and glass-pill callouts) are rendered frame-by-frame via Playwright and anchored to the VO's word-level timestamps, periodic color-graded B-roll windows give visual breath, and captions burn ONLY inside those B-roll windows (2-word chunks, ASS Format header carrying a Name field so none drop) with the VO mixed under a low music bed. This is the FREE deterministic assembly stage (Playwright beat render plus ffmpeg concat plus window-masked caption burn plus VO-and-music mix plus final composite) — the VO, the music bed, and the stock B-roll come from create-vo-elevenlabs, create-music-elevenlabs, and media-proxy. Use for the vo-anchored-motion-listicle format.
Assemble a stop-motion hand-swatch-cycle product-demo ad from a config — a sequence of still PLATES (one hand swiping a single-barrel cosmetic across a cream skin-patch, the barrel + swatch changing per plate while the hand, background, crop, and lighting stay locked) is PNG→mp4 loop-encoded at each plate's own stop-motion hold (fast motion frames 150–250ms, per-shade ~380ms, hero beats 1100–1800ms), concat-demuxed with HARD cuts into a silent master, closed on a Playwright HTML-rendered branded end card (serif tagline + sans subtitle + real logo SVG over a hero BG, never AI-rendered text), and muxed with a pre-sourced music track playing under the end card with a fade tail (no VO). This is the FREE deterministic assembly stage (loop-encode + concat-demux + end-card render + music mux); the master-anchor plate, shade plates, and end-card BG come from create-image-gpt-image-fal and the track from create-music-elevenlabs. Use for the stopmotion-hand-swatch-cycle format.
Assemble a split-screen creator ad from a config — a two-zone vertical composite where a supplied AI-creator lip-sync take fills the BOTTOM ~48% while real 16:9 product/demo clips run uncropped in the TOP ~52%, each top clip contain-fit with a darkened blurred cover-scale fill of the same clip (never black bars), a 3px brand-color divider between the zones, the creator slice cover-fit per the per-scene VO timing, scenes hard-concatenated with the body audio being the concatenated creator VO slices, an end card held on the last sharp frame ~3s, then the ASSEMBLED cut transcribed with local Whisper (not the raw VO — concat drops inter-scene silence) and word-level captions burned in the chosen style. This is the FREE deterministic assembly + caption stage (two-zone composite + blurred fill + divider + hard-concat + end card + captions); the VO comes from create-vo-elevenlabs, the anchor from create-image-gpt-image-fal, and the whole-VO lip-sync from a paid VEED Fabric 1.0 take (a no-atom upstream input). Use for the split-screen-creator format.