Skill: gaelbuenobarthe/linkedin-lead-gen-outreach (version 1.0.0)

--- name: linkedin-lead-gen-outreach description: Lightweight LinkedIn prospecting and outreach workflow for researching qualified leads, applying simple prioritization, drafting concise personalized messages, exporting clean CSV or Google Sheets-ready lead lists, and summarizing campaign activity. Use when preparing a compliant LinkedIn lead generation process, refining ICP-based targeting, building review-ready lead sheets, or generating simple outreach dashboards. --- # LinkedIn Lead Gen Outreach Run a clean, review-first LinkedIn prospecting workflow focused on lead quality, concise messaging, and simple export-ready sales operations. Keep every output structured, evidence-based, and easy to review before outreach. ## Workflow Use this sequence for complete requests: 1. define targeting 2. collect prospect data 3. apply simple lead scoring 4. draft short personalized outreach 5. export structured lead data 6. summarize campaign metrics ## 1. Define targeting Capture the search brief before producing leads. Minimum inputs: - keywords - target job titles - seniority - industry or company type - location - exclusions - business objective If the request is underspecified, convert it into a concise ICP before generating leads. ## 2. Collect prospect data Use visible LinkedIn information, user-provided data, or manually reviewed search results. Capture these fields whenever possible: - full name - LinkedIn URL - title - company - location - search match - business potential note - personalization signal - source list or query Useful personalization signals include: - recent post theme - recent promotion or job change - hiring activity - company growth signal Do not invent facts. If evidence is weak, mark it clearly and keep the message more general. ## 3. Apply simple lead scoring Use a lightweight and explainable scoring model. Default scoring dimensions: - role relevance: 0-5 - company fit: 0-5 - likely need: 0-5 - timing signal: 0-5 - personalization depth: 0-5 Total score bands: - 20-25: high priority - 12-19: medium priority - 0-11: low priority Always include a one-line explanation. ## 4. Draft personalized messages Write opening messages that are: - professional - concise - 2-3 lines max - easy to review and edit - grounded in real signals Recommended structure: 1. relevant opener 2. business relevance 3. soft CTA Rules: - keep messages short and polished - avoid hype, pressure, or artificial urgency - avoid unsupported claims - if personalization is weak, prefer a role-based message over forced specificity ## 5. Use message templates Adapt one of the templates in `references/templates.md`. Prefer: - signal-based messages when evidence is strong - role-based messages when evidence is moderate - executive-tone messages for senior stakeholders ## 6. Export format Prefer a flat CSV structure that also imports cleanly into Google Sheets. Recommended columns: - first_name - last_name - full_name - linkedin_url - title - company - location - keyword_match - business_potential_note - personalization_note - score_total - priority - score_reason - message_v1 - campaign_name - owner - source - status - next_action Suggested status values: - to_review - approved - ready_for_outreach - contacted - replied - disqualified ## 7. Dashboard and statistics When the user asks for a dashboard, produce a lightweight summary that can live in Markdown, CSV-derived calculations, or Google Sheets. Include these default metrics: - total leads - high / medium / low priority counts - leads by title - leads by geography - personalization coverage - leads ready for outreach Keep it simple and executive-friendly. ## Google Sheets guidance When preparing a sheet: - freeze the top row - apply filters to all headers - use data validation for `priority`, `status`, and `next_action` - add a summary section above or in a second tab - preserve the original raw data columns ## Compliance standard Operate in a LinkedIn-compliant, review-first manner. Use this skill to support: - profile research - qualification - message drafting - structured exports - reporting Do not rely on deceptive automation, hidden sending loops, or behavior intended to bypass platform safeguards. ## Deliverable order For a complete request, produce outputs in this order: 1. targeting summary 2. scoring rubric 3. lead table or CSV-ready rows 4. message variants 5. dashboard summary 6. Google Sheets notes ## Quality bar A strong result is: - clean and business-ready - grounded in visible evidence - concise enough for sales execution - easy to export or review - compliant and professional ## Community edition note This edition focuses on lightweight prospect research, simple prioritization, concise outreach drafting, and clean CSV or Sheets-ready exports. ## Resources Use bundled resources when useful: - `references/templates.md` for ICP, scoring, and message templates - `scripts/csv_builder.py` to convert JSON leads into CSV - `scripts/sheets_prep.py` to normalize CSV fields for Google Sheets workflows - `scripts/dashboard_stats.py` to compute simple campaign metrics from a CSV file

path: scripts/csv_builder.py
content: #!/usr/bin/env python3
import csv
import json
import sys
from pathlib import Path

FIELDS = [
    "first_name",
    "last_name",
    "full_name",
    "linkedin_url",
    "title",
    "company",
    "location",
    "keyword_match",
    "business_potential_note",
    "personalization_note",
    "score_total",
    "priority",
    "score_reason",
    "message_v1",
    "campaign_name",
    "owner",
    "source",
    "status",
    "next_action",
]


def main() -> int:
    if len(sys.argv) != 3:
        print("Usage: csv_builder.py input.json output.csv")
        return 1

    input_path = Path(sys.argv[1])
    output_path = Path(sys.argv[2])

    if not input_path.exists():
        print(f"Input file not found: {input_path}")
        return 1

    try:
        data = json.loads(input_path.read_text(encoding="utf-8-sig"))
    except json.JSONDecodeError as exc:
        print(f"Invalid JSON in {input_path}: {exc}")
        return 1

    if not isinstance(data, list):
        print("Input JSON must be a list of lead objects")
        return 1

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for row in data:
            if not isinstance(row, dict):
                print("Each lead entry must be a JSON object")
                return 1
            clean = {field: row.get(field, "") for field in FIELDS}
            writer.writerow(clean)

    print(f"Wrote {len(data)} rows to {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/dashboard_stats.py
content: #!/usr/bin/env python3
import csv
import json
import sys
from collections import Counter
from pathlib import Path

READY_STATUSES = {"approved", "ready_for_outreach", "contacted", "replied"}


def main() -> int:
    if len(sys.argv) != 3:
        print("Usage: dashboard_stats.py input.csv output.json")
        return 1

    input_path = Path(sys.argv[1])
    output_path = Path(sys.argv[2])

    if not input_path.exists():
        print(f"Input file not found: {input_path}")
        return 1

    with input_path.open("r", encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        rows = list(reader)

    priority_counter = Counter((row.get("priority") or "").strip().lower() for row in rows)
    title_counter = Counter((row.get("title") or "Unknown").strip() or "Unknown" for row in rows)
    location_counter = Counter((row.get("location") or "Unknown").strip() or "Unknown" for row in rows)
    status_counter = Counter((row.get("status") or "").strip().lower() for row in rows)

    with_signal = 0
    ready_for_outreach = 0
    for row in rows:
        if (row.get("personalization_note") or "").strip():
            with_signal += 1
        if (row.get("status") or "").strip().lower() in READY_STATUSES:
            ready_for_outreach += 1

    total = len(rows)
    stats = {
        "total_leads": total,
        "priority": {
            "high": priority_counter.get("high", 0),
            "medium": priority_counter.get("medium", 0),
            "low": priority_counter.get("low", 0),
        },
        "top_titles": title_counter.most_common(10),
        "top_locations": location_counter.most_common(10),
        "status": dict(status_counter),
        "personalization_coverage_pct": round((with_signal / total) * 100, 2) if total else 0.0,
        "ready_for_outreach": ready_for_outreach,
    }

    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(json.dumps(stats, indent=2, ensure_ascii=False), encoding="utf-8")
    print(f"Wrote dashboard stats to {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/sheets_prep.py
content: #!/usr/bin/env python3
import csv
import sys
from pathlib import Path

VALID_STATUS = {"to_review", "approved", "ready_for_outreach", "contacted", "replied", "disqualified"}
VALID_PRIORITY = {"high", "medium", "low"}


def main() -> int:
    if len(sys.argv) != 3:
        print("Usage: sheets_prep.py input.csv output.csv")
        return 1

    input_path = Path(sys.argv[1])
    output_path = Path(sys.argv[2])

    if not input_path.exists():
        print(f"Input file not found: {input_path}")
        return 1

    with input_path.open("r", encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        rows = list(reader)
        fieldnames = reader.fieldnames or []

    required = ["priority", "status", "next_action"]
    for field in required:
        if field not in fieldnames:
            fieldnames.append(field)

    for row in rows:
        priority = (row.get("priority") or "medium").strip().lower()
        status = (row.get("status") or "to_review").strip().lower()
        row["priority"] = priority if priority in VALID_PRIORITY else "medium"
        row["status"] = status if status in VALID_STATUS else "to_review"
        row["next_action"] = (row.get("next_action") or "review").strip() or "review"

    output_path.parent.mkdir(parents=True, exist_ok=True)
    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)

    print(f"Normalized {len(rows)} rows into {output_path}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: references/templates.md
content: # Templates

## ICP template

- Campaign name:
- Business objective:
- Offer or value proposition:
- Primary audience:
- Seniority:
- Geography:
- Industries:
- Company size:
- Required signals:
- Exclusions:
- Desired CTA:

## Lead object template

```json
{
  "first_name": "",
  "last_name": "",
  "full_name": "",
  "linkedin_url": "",
  "title": "",
  "company": "",
  "location": "",
  "keyword_match": "",
  "business_potential_note": "",
  "personalization_note": "",
  "role_relevance": 0,
  "company_fit": 0,
  "likely_need": 0,
  "timing_signal": 0,
  "personalization_depth": 0,
  "score_total": 0,
  "priority": "medium",
  "score_reason": "",
  "message_v1": "",
  "campaign_name": "",
  "owner": "",
  "source": "LinkedIn",
  "status": "to_review",
  "next_action": "review"
}
```

## Professional message patterns

### Pattern A — role + value

Hi {{first_name}} — your role at {{company}} stood out.
We help teams improve {{outcome}} with a focused and practical approach.
Open to a brief conversation?

### Pattern B — signal + relevance

Hi {{first_name}} — noticed {{signal}} at {{company}}.
That is often when {{problem}} becomes a real operational priority.
Happy to share a few relevant ideas if useful.

### Pattern C — executive tone

Hi {{first_name}} — reaching out because your scope looks highly relevant.
We support teams working on {{priority}} with a low-friction method.
Would it make sense to compare notes briefly?

## Score reason examples

- Strong role match, target company profile, and credible near-term need signal
- Good audience fit with moderate context but limited urgency evidence
- Partial fit only, with weak business potential and minimal personalization context


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)