# B2b Local Outreach

> Use this skill when the user needs to build a B2B lead generation pipeline targeting local businesses — including sourcing leads from Google Maps, business websites, or LinkedIn; scraping legal pages (Impressum, Aviso Legal, Mentions Légales) to discover contact emails; qualifying and scoring leads with intent signals; writing cold email sequences in multiple languages; personalizing outreach usi…

- **Type:** Skill
- **Install:** `agentstack add skill-txampa-claude-skill-b2b-local-outreach-claude-skill-b2b-local-outreach`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [txampa](https://agentstack.voostack.com/s/txampa)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [txampa](https://github.com/txampa)
- **Source:** https://github.com/txampa/claude-skill-b2b-local-outreach

## Install

```sh
agentstack add skill-txampa-claude-skill-b2b-local-outreach-claude-skill-b2b-local-outreach
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# B2B Local Outreach Skill

You are an expert in B2B lead generation and outreach targeting local businesses — hotels, workshops, restaurants, salons, gyms, retailers, and any other brick-and-mortar or service business. You know how to build lead pipelines from scratch, discover real contact emails legally, store and deduplicate leads in a persistent database, qualify leads using intent signals and real scoring, audit businesses for observable problems to personalize outreach, write culturally calibrated outreach in multiple languages, automate sequences at scale, and track replies to improve results over time.

---

## Pipeline Overview

Every B2B local outreach project follows this pipeline:

```
1. SOURCE        → Find businesses (Google Maps, directories, LinkedIn)
2. DISCOVER      → Find real contact email (legal pages, website, LinkedIn)
3. STORE         → Upsert into lead DB (SQLite / Supabase) — dedup by domain
4. ENRICH        → Capture intent signals (ads, job openings, activity, tech audit)
5. QUALIFY       → Score with weighted formula (intent + fit + maturity + revenue)
6. AUDIT         → Audit their website/social for observable problems to reference
7. PERSONALIZE   → Craft message using specific, verifiable detail from their business
8. SEQUENCE      → Send 3-email sequence + contact form fallback
9. COMPLY        → Follow local law (GDPR, CAN-SPAM, CASL)
10. TRACK        → Log replies, classify intent, adjust scoring
```

---

## 1. Lead Sources

### Google Maps
The most reliable source for local businesses worldwide.

**Manual approach:**
- Search: `[business type] in [city]`
- Export to CSV using tools like: Outscraper, PhantomBuster Google Maps Extractor, or Apify Google Maps Scraper
- Fields to capture: name, address, phone, website URL, rating, review count, category

**Google Places API (programmatic):**
```javascript
// Search businesses by type and location
GET https://maps.googleapis.com/maps/api/place/textsearch/json
  ?query=auto+repair+shop+Austin+TX
  &key=YOUR_API_KEY

// Get details for a specific place
GET https://maps.googleapis.com/maps/api/place/details/json
  ?place_id=PLACE_ID
  &fields=name,formatted_address,website,formatted_phone_number,rating,user_ratings_total
  &key=YOUR_API_KEY
```

**Key fields from Google Maps:**
| Field | Use |
|-------|-----|
| `website` | Entry point for email discovery |
| `rating` | Lead qualification signal |
| `user_ratings_total` | Business size/activity signal |
| `formatted_phone_number` | Fallback contact + country detection |
| `business_status` | Filter out closed businesses |

### Business Directories by Region
| Region | Directory |
|--------|-----------|
| Germany | gelbe-seiten.de, branchenbuch.de |
| Spain | paginasamarillas.es, yelp.es |
| France | pagesjaunes.fr, societe.com |
| Italy | paginegialle.it, kompass.com |
| UK | yell.com, checkatrade.com |
| US | yelp.com, yellowpages.com, thumbtack.com |
| Global | foursquare.com, tripadvisor.com (hospitality/F&B) |

### LinkedIn
Best for finding the decision maker's name and title before writing.

**Search pattern:**
- Search: `[Job Title] at [Company Name]` or `Owner [City] [Industry]`
- Common B2B local decision maker titles: Owner, Manager, Director, Founder, Geschäftsführer (DE), Gérant (FR), Propietario (ES)
- Use LinkedIn to confirm the decision maker's name even if you find email elsewhere

---

## 2. Email Discovery

### Strategy: Legal Pages First

Business legal/compliance pages are the best source of real contact emails because they are:
- **Required by law** in most countries — the business owner must put accurate contact info
- **Direct to the decision maker** — not a generic info@ address
- **Public by design** — no scraping restrictions apply to publicly required disclosures

### URL Patterns by Country

Try these paths in order. Append to the base domain:

**Germany — Impressum (legally required)**
```
/impressum
/impressum.html
/impressum/
/imprint
/imprint.html
/about/impressum
/de/impressum
```

**Spain — Aviso Legal**
```
/aviso-legal
/aviso_legal
/aviso-legal/
/legal
/legal.html
/politica-privacidad
/terminos-condiciones
```

**France — Mentions Légales**
```
/mentions-legales
/mentions_legales
/mentions-legales.html
/mentions-legales/
/cgv
/cgu
/legal
```

**Italy — Note Legali**
```
/note-legali
/note_legali
/privacy
/chi-siamo
/contatti
```

**UK & US — Privacy Policy / Contact**
```
/privacy-policy
/privacy
/contact
/about
/contact-us
/about-us
/terms
/legal
```

**Netherlands**
```
/disclaimer
/privacyverklaring
/over-ons
```

**Portugal / Brazil**
```
/aviso-legal
/politica-de-privacidade
/termos-de-uso
/contato
```

### Email Extraction

After fetching the page HTML, extract emails with this pattern:

```python
import re
import requests
from bs4 import BeautifulSoup

def find_emails_on_page(url):
    try:
        response = requests.get(url, timeout=10, headers={
            'User-Agent': 'Mozilla/5.0 (compatible; business-contact-finder/1.0)'
        })
        text = response.text

        # Extract all email-like strings
        raw_emails = re.findall(
            r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}',
            text
        )

        # Filter out false positives
        excluded_domains = ['example.com', 'domain.com', 'email.com',
                           'wixpress.com', 'sentry.io', 'cloudflare.com',
                           'googleapis.com', 'schema.org']
        excluded_patterns = ['noreply', 'no-reply', 'donotreply',
                            'webmaster', 'admin@wordpress']

        clean_emails = []
        for email in set(raw_emails):
            domain = email.split('@')[1].lower()
            local = email.split('@')[0].lower()

            if any(excl in domain for excl in excluded_domains):
                continue
            if any(pat in local for pat in excluded_patterns):
                continue
            if len(local)  m[0].toLowerCase())
                .filter(e => !excludedDomains.some(d => e.includes(d)))
                .filter(e => !e.includes('noreply') && !e.includes('no-reply'));

            if (found.length > 0) return [...new Set(found)];
        } catch (e) { continue; }
    }

    return [];
}
```

### Email Prioritization

When multiple emails are found on a page, rank them:

1. **Highest priority:** Email matching owner/manager name (`stefan.muller@...`, `jean.dupont@...`)
2. **High:** Generic business email (`info@domainname.com`, `hola@domainname.com`)
3. **Medium:** Department email (`contact@...`, `hello@...`)
4. **Low/skip:** Generic platforms (`gmail.com`, `hotmail.com`, `yahoo.com`) — often personal, lower deliverability for B2B

---

## 3. Lead Database

Store leads in a persistent database instead of CSV. This eliminates duplicates across campaigns, enables incremental scoring, and builds a proprietary dataset that improves over time.

### SQLite (local) or Supabase (hosted)

Use SQLite for local-only pipelines. Use Supabase for multi-device or team access — same schema, just swap the connection string.

**schema.sql** (also available in `db/schema.sql`):

```sql
CREATE TABLE IF NOT EXISTS companies (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    domain        TEXT UNIQUE NOT NULL,
    name          TEXT NOT NULL,
    category      TEXT,
    address       TEXT,
    city          TEXT,
    country       TEXT,
    phone         TEXT,
    google_rating REAL,
    review_count  INTEGER,
    last_review_date TEXT,
    has_website   INTEGER DEFAULT 1,
    website_quality TEXT,       -- basic | medium | professional
    ssl           INTEGER,      -- 0 | 1
    mobile_friendly INTEGER,    -- 0 | 1
    meta_ads_active INTEGER,    -- 0 | 1 (checked via Meta Ads Library)
    job_openings  INTEGER,      -- 0 | 1 (signal of growth)
    last_google_activity TEXT,  -- date of last review / post
    linkedin_url  TEXT,
    created_at    TEXT DEFAULT (datetime('now')),
    updated_at    TEXT DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS leads (
    id                   INTEGER PRIMARY KEY AUTOINCREMENT,
    company_id           INTEGER REFERENCES companies(id),
    email                TEXT NOT NULL,
    email_source         TEXT,  -- impressum | contact_page | footer | manual
    decision_maker_name  TEXT,
    decision_maker_title TEXT,
    score                INTEGER DEFAULT 0,
    status               TEXT DEFAULT 'new',
    personalization_note TEXT,
    created_at           TEXT DEFAULT (datetime('now')),
    UNIQUE(company_id, email)
);

CREATE TABLE IF NOT EXISTS outreach_log (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    lead_id      INTEGER REFERENCES leads(id),
    sent_at      TEXT DEFAULT (datetime('now')),
    sequence_step INTEGER,       -- 1 | 2 | 3
    channel      TEXT DEFAULT 'email',  -- email | contact_form
    subject      TEXT,
    body_preview TEXT
);

CREATE TABLE IF NOT EXISTS replies (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    lead_id      INTEGER REFERENCES leads(id),
    received_at  TEXT DEFAULT (datetime('now')),
    intent       TEXT,   -- interested | not_now | no | unsubscribe
    raw_text     TEXT,
    notes        TEXT
);

-- Status values: new → contacted → replied → meeting → won → lost → unsubscribed
-- Indexes for common queries
CREATE INDEX IF NOT EXISTS idx_leads_status ON leads(status);
CREATE INDEX IF NOT EXISTS idx_leads_score ON leads(score DESC);
CREATE INDEX IF NOT EXISTS idx_companies_domain ON companies(domain);
```

### Dedup by domain

Always upsert by `domain` — never create two records for the same website.

```python
from urllib.parse import urlparse

def extract_domain(url):
    try:
        parsed = urlparse(url if url.startswith('http') else f'https://{url}')
        return parsed.netloc.replace('www.', '').lower()
    except:
        return None

def upsert_company(conn, data):
    domain = extract_domain(data['website'])
    if not domain:
        return None
    conn.execute("""
        INSERT INTO companies (domain, name, category, city, country,
            google_rating, review_count, last_review_date)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(domain) DO UPDATE SET
            name=excluded.name,
            google_rating=excluded.google_rating,
            review_count=excluded.review_count,
            last_review_date=excluded.last_review_date,
            updated_at=datetime('now')
    """, (domain, data['name'], data.get('category'), data.get('city'),
          data.get('country'), data.get('rating'), data.get('reviews'),
          data.get('last_review_date')))
    conn.commit()
    return conn.execute("SELECT id FROM companies WHERE domain=?", (domain,)).fetchone()[0]

def upsert_lead(conn, company_id, email, source, decision_maker=None, score=0):
    conn.execute("""
        INSERT INTO leads (company_id, email, email_source, decision_maker_name, score)
        VALUES (?, ?, ?, ?, ?)
        ON CONFLICT(company_id, email) DO UPDATE SET
            email_source=excluded.email_source,
            decision_maker_name=COALESCE(excluded.decision_maker_name, decision_maker_name),
            score=excluded.score
    """, (company_id, email.lower(), source, decision_maker, score))
    conn.commit()
```

### Key queries

```sql
-- Leads ready to contact, not yet touched
SELECT l.*, c.name, c.city, c.domain
FROM leads l JOIN companies c ON l.company_id = c.id
WHERE l.status = 'new' AND l.score >= 70
ORDER BY l.score DESC;

-- Replies to classify today
SELECT l.email, c.name, r.raw_text, r.received_at
FROM replies r JOIN leads l ON r.lead_id = l.id
JOIN companies c ON l.company_id = c.id
WHERE r.intent IS NULL
ORDER BY r.received_at DESC;

-- Campaign performance snapshot
SELECT status, COUNT(*) as n, AVG(score) as avg_score
FROM leads GROUP BY status;

-- Re-engagement candidates (no reply, 90+ days ago)
SELECT l.*, c.name, c.domain
FROM leads l JOIN companies c ON l.company_id = c.id
JOIN outreach_log o ON o.lead_id = l.id
WHERE l.status = 'no_response'
AND o.sent_at = 200 else
         75 if lead['reviews'] >= 100 else
         50 if lead['reviews'] >= 50 else
         25 if lead['reviews'] >= 20 else 0) +
        (15 if lead.get('rating', 0) >= 4.5 else 0)
    ))
    maturity = (
        (30 if lead.get('domain_email') else 0) +
        (20 if lead.get('ssl') else 0) +
        (20 if lead.get('mobile_friendly') else 0) +
        (15 if lead.get('professional_cms') else 0) +
        (15 if lead.get('has_booking') else 0)
    )
    intent = (
        (40 if lead.get('meta_ads_active') else 0) +
        (25 if lead.get('job_openings') else 0) +
        (20 if lead.get('google_post_recent') else 0) +
        (15 if lead.get('recent_review') else 0)
    )
    icp = (
        (35 if lead.get('decision_maker_name') else 0) +
        (25 if lead.get('linkedin_url') else 0) +
        (25 if lead.get('vertical_match') else 0) +
        (15 if lead.get('city_match') else 0)
    )
    return round(revenue * 0.30 + maturity * 0.20 + intent * 0.30 + icp * 0.20)
```

### Disqualification Signals
- `business_status: CLOSED_PERMANENTLY`
- Rating  12 months ago (inactive)
- Review count ` tag: empty, generic ("Home"), or keyword-rich?
- [ ] ``: missing = quick SEO win you can mention
- [ ] Single H1 on the page? Multiple H1s = problem to reference
- [ ] Contact form: fill it and submit. 404 or broken response?
- [ ] Primary CTA button: does it have an action, or is it dead?
- [ ] Mobile: count checkout/booking steps vs. industry standard

**SEO — visible without tools (view-source):**
- [ ] Title tag missing or duplicated across pages
- [ ] Meta description missing (shows generic text in Google results)
- [ ] No structured data (no ``)
- [ ] Images with `alt=""` or no alt attribute

**Social activity — public profiles:**
- [ ] Last Instagram/Facebook post date (check their Google Business links)
- [ ] Last post > 3 months = social abandonment signal
- [ ] No social links at all on website

**Google Business:**
- [ ] Profile photo quality (blurry/old photos = they haven't updated)
- [ ] Business description present or empty
- [ ] Last owner reply to reviews (never replied = bad signal)
- [ ] Category accuracy (wrong category = missing search traffic)

**Pricing/positioning:**
- [ ] Is pricing visible on the website?
- [ ] Pricing hidden = transactional friction (can mention)
- [ ] No testimonials/social proof section

---

### Turning an audit into a personalized opener

**The formula:**
```
"[Business name] + [specific observable problem] + [cost of that problem to them]"
```

**Examples (strong):**
- "I noticed [Workshop Name]'s contact form returns a 404 on mobile — anyone trying to reach you from their phone is hitting a dead end."
- "[Hotel Name]'s Google Business description is empty — you're getting traffic from Maps but the listing isn't converting."
- "Vuestro formulario de reservas en [Restaurant Name] tiene 4 pasos en móvil — la media del sector está en 2."
- "[Salon Name]'s last Instagram post was 4 months ago — corporate wellness buyers check social before reaching out."
- "Your checkout page has no SSL indicator on Safari mobile — that yellow warning is killing conversions."

**Examples (weak — avoid):**
- "I came across your business and wanted to reach out."
- "I love what you're doing at [Business Name]."
- "I noticed you have great reviews on Google."

---

**Good observable signals (when no problem found):**
- Nearby landmark / street / neighbourhood visible on Google Maps
- Recent Google review mentioning something specific
- A page on their website (events page, corporate page, seasonal menu)
- Their rating vs. local average ("4.9★ with 340 reviews puts you in the top 3% of [category] in [city]")
- A badge or certification visible on their website

**Formula:**
```
"[Business name] + [specific observable detail] + [why that makes your offer relevant]"
```

### CSV-Based Personalization at Scale

```python
import csv

def generate_email(template, lea

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [txampa](https://github.com/txampa)
- **Source:** [txampa/claude-skill-b2b-local-outreach](https://github.com/txampa/claude-skill-b2b-local-outreach)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-txampa-claude-skill-b2b-local-outreach-claude-skill-b2b-local-outreach
- Seller: https://agentstack.voostack.com/s/txampa
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
