Systems Lab

Agent skill

outreach-icp

Turn a vague outbound brief such as 'sell our tool to SaaS companies' into a targeting spec that a data source can actually execute: job titles with their real per-country variants, industries with NAF, NAICS and SIC codes, headcount band, geography, technographics, buying signals, and the exclusions that keep customers, competitors and unpayable prospects out of the list.

activeNeeds a keyInstructions only3,431 words

Filed under Prospecting and list building and Outbound email.

From emelia-io/claude-outreach · 17 skills · 12 · pushed 2026-09-09

What it does when it runs

Turn a vague outbound brief such as 'sell our tool to SaaS companies' into a targeting spec that a data source can actually execute: job titles with their real per-country variants, industries with NAF, NAICS and SIC codes, headcount band, geography, technographics, buying signals, and the exclusions that keep customers, competitors and unpayable prospects out of the list. Estimates the addressable market with free count calls before a single credit is spent, and splits the brief into separate campaigns when it covers too many audiences. Reads a brief in plain language, writes outreach/icp.json. Triggers on: ICP, ideal customer profile, targeting, target audience, who should I target, define my market, segment, segmentation, persona, buyer persona, job titles, NAF code, NAICS, SIC, headcount, firmographics, technographics, buying signals, exclusion list, TAM, addressable market, market sizing.

Read from the skill and the 0 files bundled beside it. A skill’s own description is written to be selected by an agent, so it describes the job and not the dependencies.

Keys and connectors you must supply
  • BASILE_API_KEY
Hosts it reaches
  • api.basile.cc
Tool permissions it declares
No allowed-tools in the frontmatter. It only issues instructions, so there is nothing to bound.
Actions present in the files
None. Instructions only.

Ask about outreach-icp

Opens your assistant with this page's verified links already in the prompt.

Is this safe to install?ClaudeChatGPT
Adapt it to my stackClaudeChatGPT
What else do I need for it to workClaudeChatGPT
Rather ask a human? Talk to Cheetah
git clone --depth 1 --filter=blob:none --sparse https://github.com/emelia-io/claude-outreach.git /tmp/claude-outreach
git -C /tmp/claude-outreach sparse-checkout set "skills/outreach-icp"
mkdir -p ~/.claude/skills/outreach-icp
cp -R "/tmp/claude-outreach/skills/outreach-icp/." ~/.claude/skills/outreach-icp/

Picked up without a restart. A project skill of the same name is shadowed by your personal one. For one repository only, swap ~/.claude/skills for .claude/skills. Claude Code docs ↗

Or take the whole library

This repo ships a .claude-plugin manifest, so Claude Code can install all 17 skills at once. Plugin skills are invoked as /<plugin>:<skill>, so they never collide with your own.

/plugin marketplace add emelia-io/claude-outreach
/plugin

The folder is the same in every client that implements the format — 46 of them — so if yours is not above, only the destination changes.

Before you install: this skill will not complete its job on a bare agent. It needs BASILE_API_KEY, which you have to obtain separately.

Reproduced in full from emelia-io/claude-outreach/blob/585061e4d78fe14a70701fbc5aec5d1eeb3d580a/skills/outreach-icp/SKILL.md, which is licensed MIT (skill frontmatter). 3,431 words, 19 headings.

Define the ICP

What this does

Takes a one line brief and turns it into outreach/icp.json, a targeting spec precise enough that outreach-leads can run it against Basile, LinkedIn or a CSV without guessing. It writes down the titles, the industry codes, the size band, the geography, the signals and, above all, the exclusions. It counts the market for free before anything is extracted, and it refuses to hide three audiences inside one campaign.

No credits are spent here. Counting is free on every source this repository uses.

When to use it

Use it at the start of every campaign, and again whenever a campaign underperforms and you suspect the list rather than the copy. A 1% reply rate with good deliverability is almost always a targeting problem.

Use a different skill when:

  • You already have outreach/icp.json and want contacts: go to outreach-leads.
  • You already have a list and want it cleaned: go to outreach-filter.
  • You know exactly who you want by name (a named account list): skip the ICP, write the account list straight into the leads step, and come back here only if it stops scaling.

Inputs

InputRequiredIf missing
The brief, in plain languageyesAsk for it in one question: what do you sell, to whom, and what does a deal cost you?
What the product costsyesAsk. Price sets the headcount floor. Without it the list will be full of companies that cannot pay.
Existing customer domainsstrongly recommendedAsk for a CRM export, any CSV with a domain column. If there is none, say the list will contain customers and that you will catch them later in outreach-filter.
Competitor domainsrecommendedAsk for three or four names, derive the domains.
Basile API key (BASILE_API_KEY)optionalWithout it you cannot count the French market. Say so, keep the spec, mark size_estimate.method as "not measured".
Sales Navigator seatoptionalWithout it you cannot count the LinkedIn market. Same treatment.

Never invent a headcount band, a price point or a list of customers. Ask, or record the gap in open_questions inside the output file.

How to do it

1. Read the brief back, narrower

Restate the brief as a single sentence in this shape, and get a yes before continuing:

We sell <what> to <title> at <kind of company> of <size> in <geography>, who have <problem>, and a deal is worth about <amount>.

Anything the user cannot fill in is an open question, not a value you choose for them.

2. Titles, with the variants that actually exist

A title filter written in one language and one country finds a fraction of the market. Write every variant a real person puts on a real profile, in every language of the target geography. These are starting points, not a nomenclature.

FunctionEnglishFrenchOther
General managementCEO, Chief Executive Officer, Founder, Co-Founder, Owner, Managing Director, PresidentPDG, President, Directeur General, DG, Gerant, Fondateur, Cofondateur, DirigeantGeschaftsfuhrer, Inhaber (DE), Director General, Gerente (ES), Amministratore Delegato (IT), Zaakvoerder (NL)
SalesVP Sales, Head of Sales, Sales Director, CRO, Sales ManagerDirecteur Commercial, Responsable Commercial, Directeur des VentesVertriebsleiter (DE), Director Comercial (ES)
Marketing and growthCMO, VP Marketing, Head of Marketing, Head of Growth, Demand GenerationDirecteur Marketing, Responsable Marketing, Responsable AcquisitionMarketingleiter (DE)
TechnologyCTO, VP Engineering, Head of Engineering, Head of Platform, Head of InfrastructureDirecteur Technique, DSI, Directeur des Systemes d'Information, Responsable InformatiqueTechnischer Leiter (DE)
FinanceCFO, Finance Director, Head of Finance, Financial ControllerDAF, Directeur Administratif et Financier, Directeur FinancierKaufmannischer Leiter (DE)
PeopleCHRO, HR Director, Head of People, Talent AcquisitionDRH, Responsable RH, Responsable des Ressources HumainesPersonalleiter (DE)

Write the accented and unaccented spelling of every French title, because sources differ on both. Basile matches accent insensitively, LinkedIn does not always.

Three traps worth writing into the spec:

  • Directeur is not Directeur General. A bare Directeur Commercial is a sales lead; Directeur General is the company boss. A substring filter on Directeur returns both, plus Directeur Adjoint. Put the full phrases in include.
  • In France the legal title and the job title differ. Gerant is the statutory officer of a SARL, President of a SAS. On small companies that person is the buyer, and they may describe themselves on LinkedIn as Fondateur. Search both the legal role and the free text role, then deduplicate.
  • Junior lookalikes must be excluded, not ignored. Always populate titles.exclude with at least: assistant, adjoint, adjointe, stagiaire, alternant, apprenti, intern, junior, charge de, chargee de, freelance, independant, consultant, retraite, open to work.

3. Industry, with codes and with doubt

Write the industry in three ways: as words, as codes, and as the filter your source actually accepts.

France, NAF codes. NAF revision 2 (2008) is still the reference for the APE code assigned to a company until 1 January 2027, when NAF 2025 takes over; during 2026 the Sirene registry shows both codes (source: INSEE, insee.fr/fr/information/8181066). Accept both revisions in a spec written now. Software and IT examples: 62.01Z programmation informatique, 62.02A conseil en systemes et logiciels informatiques, 62.02B tierce maintenance, 62.03Z gestion d'installations informatiques, 62.09Z autres activites informatiques, 63.11Z traitement de donnees et hebergement, 63.12Z portails internet, 58.29A 58.29B 58.29C edition de logiciels, 70.22Z conseil pour les affaires, 73.11Z agences de publicite.

United States, NAICS. 511210 software publishers, 541511 custom computer programming services, 541512 computer systems design services, 518210 computing infrastructure, data processing and hosting, 541611 management consulting.

United States, legacy SIC. 7372 prepackaged software, 7371 custom computer programming, 7379 computer related services. Some databases still only carry SIC.

United Kingdom, SIC 2007. 62012 business and domestic software development, 62020 IT consultancy, 63110 data processing and hosting.

The doubt matters as much as the codes. A company's NAF or SIC code is declared once at registration and almost never updated, so a ten year old SaaS is routinely filed under 70.22Z (consulting) or 62.01Z (programming) rather than under software publishing. Never let an industry code be the only industry filter. Pair it with at least one of: the Basile activity concept (which merges NAF, LinkedIn and Google categories), the LinkedIn industry, a headcount band, or a keyword on the company name or website.

4. Headcount, set by your price and not by your taste

The floor is arithmetic. A company will not buy a tool that costs more than a small share of what it already spends on the problem. Work it backwards:

  1. What does one deal need to be worth for the campaign to pay for itself?
  2. If you charge per seat, how many seats does that need?
  3. How many employees does a company need for that many seats to exist?

That number is headcount.min, and you write the reasoning into headcount.basis so the next person does not relax it by accident. Two practical markers, offered as rules of thumb rather than measurements: below roughly 10 employees the founder is the buyer, the cycle is short and the budget is small; above roughly 500 there is a procurement process that cold email does not survive on its own.

The ceiling is about who answers. Write it down even when it feels obvious.

5. Geography

Record countries as ISO 3166-1 alpha-2 codes, because that is what both Emelia's email finder and Basile use. Add regions, cities or postal codes only when they change the message. Add exclude_countries when a market is served by someone else or is out of scope for legal reasons.

For France specifically: department is the first two digits of the postal code, except Corsica (2A, 2B, postal codes starting 20) and the overseas departments, whose codes are three digits (971 to 978). Get this wrong and you silently drop or add a whole region.

6. Technographics

Useful when your product replaces or plugs into something. Be honest about detection: neither Basile nor a Sales Navigator search carries a technology filter. You detect a technology from the prospect's own site, their job postings, or a public directory, and you record how in technographics.how_detected. If you cannot name the detection method, drop the criterion instead of pretending it is filterable.

7. Buying signals

A signal is only worth listing if you can name the filter that implements it. For each one record the source, the filter, the window in days, and whether it is strong.

SignalImplementable withReality
Company recently createdBasile created_since_months on companiesDirect filter, reliable
New in roleBasile current_tenure_years (LinkedIn source), or the Sales Navigator changed-jobs filterDirect, but LinkedIn source only
Alumni of a company you knowBasile past_employerDirect, LinkedIn source only
Headcount growthSales Navigator company growth filterPick it in the interface, no public API
Hiring for a role you serveJob boards, read manually or from your own scraperNot a filter, an input list you build
Raised fundingPublic announcements, your own listNot a filter on either source. Bring the account names in as a CSV
Leadership changePress, LinkedIn postsNot a filter

Signals that cannot be filtered are still valuable: they become a named account list fed into outreach-leads, and they become the first sentence of the email.

8. Exclusions, the part that saves the campaign

An ICP without exclusions produces a list that emails your own customers. Fill every one of these, and write "none" explicitly rather than leaving a blank:

  • Existing customers. By domain, ideally from a CRM export. Also add their parent and subsidiary domains if you know them.
  • Open opportunities. Emailing a live deal from a cold sequence is worse than emailing a customer.
  • Competitors. By domain. They sign up for everything.
  • Forbidden or pointless sectors. By code where possible. Typical ones: public administration, primary and secondary education, hospitals, religious and political organisations, and any sector your legal or brand rules exclude. Write the reason.
  • Too small to pay. This is exclusions.min_headcount, and it is the same number as headcount.min. Duplicating it here is deliberate: the filter step reads exclusions.
  • Already contacted. A recontact window in days (90 is a common default, offered as a rule of thumb). Anyone in a campaign more recently than that is out.
  • Unsubscribes and hard bounces. These live in the Emelia blacklist and in your local mirror outreach/blacklist.txt. Never re-add them.
  • Role addresses. Decide now: drop or keep. Drop them when you target a named role at a company with real staff. Keep them (as their own segment, with their own message) when you target micro-businesses where contact@ is the only address the company has.

9. Size the market before spending anything

Counting is free on both sources. Do it as a ladder, one filter at a time, and write every step into size_estimate.ladder.

Basile (France). POST https://api.basile.cc/people/find with header Authorization: <your key> (the raw key, no Bearer prefix) and Content-Type: application/json. Send {"countOnly": true, "filters": {...}}. The response carries total and an empty leads array, and nothing is charged. Do not use limit: 1 to count: that returns one record and costs one credit. Run one call per rung:

  1. Geography only.
  2. Geography plus industry.
  3. Plus headcount.
  4. Plus titles.
  5. Minus the exclusions you can express as filters.

LinkedIn. Build the Sales Navigator search described in outreach-leads and read the result count above the list. It is free and it is the same ladder.

Then read the ladder rather than only the last number. The rung that divides the count by more than about ten is the rung that defines your campaign; check it says what you meant. A title list that cuts a market by fifty is usually missing variants, not finding a niche.

From addressable to reachable. The count is profiles, not inboxes. Do not budget on it. Take a random sample of 100 rows, run the email finder on it (that costs 100 credits, announce it and wait for an explicit yes), and measure your own find rate. Record it as assumed_email_rate with the date. Rates vary widely by source and country, so a measured rate on your own sample beats any published average.

10. The three segment rule

If the spec needs more than three segments, it is more than one campaign.

A segment is a group that deserves a different first sentence. Two groups that would receive the same opening line are one segment, whatever the filters say. A CTO at a 40 person SaaS and a CTO at a 40 person e-commerce company are two segments if your value proposition differs, one segment if it does not.

When you count four or more, stop and say so: propose the top three by expected value, and put the rest in a second campaign to run afterwards. Explain the reason plainly: a campaign with four messages is four campaigns with a quarter of the volume each, and none of them will have enough replies to tell you anything.

Also enforce a floor. Below roughly 150 contacts a segment produces single digit replies, and single digit replies cannot separate a good message from a bad one. Merge segments that small.

Output

Write outreach/icp.json. Never overwrite a previous run silently: if the file exists, show what changes and ask, or write outreach/icp-<name>.json.

Schema, every key required unless marked optional:

version              integer, always 1
created_at           ISO date
brief                the original sentence, verbatim
offer                what, problem, proof, price_point, minimum_viable_deal
segments[]           1 to 3 entries, each:
  id                 slug, used as the segment value in leads.csv
  label              human name
  why_different      one sentence: what changes in the message for this group
  priority           1 is first
  titles             include[], exclude[], seniority[] (optional), notes
  industry           naf[], naics[], sic[], basile_activity[], linkedin_industry[], notes
  headcount          min, max, basis
  geography          countries[], regions[], cities[], postal_codes[], exclude_countries[]
  technographics     uses[], does_not_use[], how_detected     (optional)
  signals[]          name, window_days, source, filter, weight (strong|weak)
  size_estimate      counted_at, source, ladder[{step,total}], addressable,
                     assumed_email_rate, reachable, method
exclusions           customer_domains[], customer_domains_file, competitor_domains[],
                     forbidden_industries{naf[],naics[],reason}, blacklist_file,
                     min_headcount, recontact_window_days, role_addresses (drop|keep),
                     notes
message_hooks[]      optional, phrases the copy step can reuse
open_questions[]     anything you had to leave undecided, in plain language

A filled example:

{
  "version": 1,
  "created_at": "2026-09-08",
  "brief": "sell our API monitoring tool to CTOs of French SaaS, 20 to 200 people",
  "offer": {
    "what": "API uptime and latency monitoring with alerting",
    "problem": "outages found by customers before the team sees them",
    "proof": "3 minute setup, 40 teams switched from a homemade cron",
    "price_point": "90 EUR per month per project",
    "minimum_viable_deal": "1080 EUR per year"
  },
  "segments": [{
    "id": "fr-saas-cto",
    "label": "CTO at French SaaS, 20 to 200 employees",
    "why_different": "opens on their public status page, or the absence of one",
    "priority": 1,
    "titles": {
      "include": ["CTO", "Chief Technology Officer", "Directeur Technique",
                  "VP Engineering", "Head of Engineering", "Head of Platform",
                  "Responsable Technique"],
      "exclude": ["assistant", "adjoint", "stagiaire", "alternant", "junior",
                  "charge de", "freelance", "consultant"],
      "seniority": ["C-Level", "VP", "Director", "Head"],
      "notes": "DSI excluded on purpose: internal IT, not product engineering"
    },
    "industry": {
      "naf": ["62.01Z", "62.02A", "58.29C", "63.11Z"],
      "naics": [], "sic": [],
      "basile_activity": ["<concept ids from GET /companies/activity-suggest?q=logiciel>"],
      "linkedin_industry": ["Software Development", "IT Services and IT Consulting"],
      "notes": "NAF alone misses SaaS filed under 70.22Z, so activity is ORed with it"
    },
    "headcount": {
      "min": 20, "max": 200,
      "basis": "below 20 no dedicated engineering owner and no budget line; above 200 an observability vendor is already in place"
    },
    "geography": {
      "countries": ["FR"], "regions": [], "cities": [],
      "postal_codes": [], "exclude_countries": []
    },
    "technographics": {
      "uses": ["public status page", "public REST API"],
      "does_not_use": ["Datadog", "New Relic"],
      "how_detected": "manual check of the company site on the top 200 rows only. Not filterable at source"
    },
    "signals": [
      { "name": "new CTO in the last 12 months", "window_days": 365,
        "source": "basile", "filter": "current_tenure_years <= 1", "weight": "strong" },
      { "name": "raised a seed or series A in the last 6 months", "window_days": 180,
        "source": "manual account list", "filter": "none, brought in as a CSV of company names",
        "weight": "strong" }
    ],
    "size_estimate": {
      "counted_at": "2026-09-08", "source": "basile",
      "ladder": [
        { "step": "FR only", "total": 4210000 },
        { "step": "+ software and IT activity", "total": 96400 },
        { "step": "+ headcount 20 to 200", "total": 11800 },
        { "step": "+ CTO title variants", "total": 3140 },
        { "step": "- customers and competitors", "total": 3082 }
      ],
      "addressable": 3082,
      "assumed_email_rate": 0.58,
      "reachable": 1788,
      "method": "countOnly on POST /people/find, five free calls. Email rate measured on a random sample of 100 rows on 2026-09-08, 58 found"
    }
  }],
  "exclusions": {
    "customer_domains": ["acme.fr", "beta-labs.io"],
    "customer_domains_file": "outreach/customers.csv",
    "competitor_domains": ["datadoghq.com", "newrelic.com", "checkly.com"],
    "forbidden_industries": {
      "naf": ["84.11Z", "85.31Z", "86.10Z"], "naics": [],
      "reason": "public sector and health, procurement incompatible with self serve"
    },
    "blacklist_file": "outreach/blacklist.txt",
    "min_headcount": 20,
    "recontact_window_days": 90,
    "role_addresses": "drop",
    "notes": "customers exported from the CRM on 2026-09-05, 214 domains"
  },
  "message_hooks": [
    "you found out from a customer, not from a dashboard",
    "the cron job that checks the API and nobody maintains"
  ],
  "open_questions": [
    "no list of open opportunities was provided, so live deals may be in the list"
  ]
}

Then tell the user, in three lines: how many contacts the spec is worth, what the biggest single cut was, and what you had to guess. Wait for a yes before anyone runs outreach-leads.

Checks before finishing

  • outreach/icp.json parses as JSON and has between 1 and 3 segments.
  • Every segment has a non empty titles.include and a non empty titles.exclude.
  • Every segment has at least two independent industry signals, or an explicit note saying why one is enough.
  • headcount.min is a number and headcount.basis explains it in terms of price.
  • geography.countries uses ISO 3166-1 alpha-2 codes.
  • Every entry in signals[] names a real filter or says "none" and how the list is built instead.
  • Every key of exclusions is filled, "none" included, and min_headcount matches headcount.min.
  • size_estimate.ladder has at least three rungs, or method says "not measured" and the reason.
  • assumed_email_rate is either measured on a real sample (with the date) or absent. Never invent it.
  • The user has confirmed the restated brief and the segment count out loud.

Failure modes

The brief is a product description, not a market. "We sell an AI agent platform" has no buyer in it. Ask who is currently doing the work by hand, and target their manager.

Titles return almost nothing. Usually a missing language or a full phrase used where a fragment was needed. Run the title list through a suggestion endpoint before trusting it: Basile exposes GET /people/roles/suggest?q= for exactly this.

The count collapses at the industry rung. The industry code is doing all the work and it is probably wrong for this market. Loosen the code, tighten the headcount, and check ten known companies: if fewer than seven of them appear, the filter is broken.

The count is enormous. Anything above a few hundred thousand people means a filter did not apply. A common cause on Basile is sending a filter the API ignores; the public documentation flags headquarters_region_code as removed at validation, so a region filter written that way restricts nothing. Use headquarters_postal_code or the region name instead, and recount.

Four or more segments. Do not compromise by merging them into a vague message. Say it is three campaigns and propose an order.

The user pushes back on exclusions. "We can email our customers, it is fine" is the one to resist. It is the fastest way to a complaint, and complaints follow the domain, not the campaign.

Limits

This skill does not buy or verify data, does not check whether a company can actually afford your product, and does not know your win rates. The industry code lists above are starting points, not an exhaustive nomenclature: verify a code before you build a campaign on it. Buying signals that are not filterable stay manual, and this skill says so rather than implying a filter exists. Nothing here estimates revenue: an addressable count is a count of people, not of pipeline.

Other skills for the same job

Different authors, same problem. Matched on the words in the skill name, across every library in the catalogue except this one.

Need help setting it up?

This page tells you what outreach-icp does and what it needs. Cheetah builds the agent setup it runs inside: data, CRM, sequencing and the guardrails.

Book a call →

The directory stays free. There is nothing gated behind this.