Systems Lab

Agent skill

outreach-find-phone

Find direct mobile numbers for the people on a lead list with Emelia's phone finder, from their LinkedIn profile URL, one at a time or in bulk.

activeNeeds a keyActs undeclared2,105 words

Filed under Outbound email.

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

What it does when it runs

Find direct mobile numbers for the people on a lead list with Emelia's phone finder, from their LinkedIn profile URL, one at a time or in bulk. States the credit cost before spending, polls each job to completion, handles found and not found honestly, and writes the numbers into outreach/leads.csv and outreach/enrichment.json. Emelia does not place the calls: the number goes to your phone or your CRM. Triggers on: find phone, phone finder, find mobile, mobile number, direct dial, phone number, cell number, find the number of, phone enrichment, cold call list, get phone numbers.

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
  • EMELIA_API_KEY
Hosts it reaches
  • api.emelia.io
  • www.linkedin.com
Tool permissions it declares
No allowed-tools in the frontmatter. It does act, so it runs under whatever permissions your session already grants.
Actions present in the files
shell

Ask about outreach-find-phone

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-find-phone"
mkdir -p ~/.claude/skills/outreach-find-phone
cp -R "/tmp/claude-outreach/skills/outreach-find-phone/." ~/.claude/skills/outreach-find-phone/

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 EMELIA_API_KEY, which you have to obtain separately.

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

Find direct mobile numbers

What this does

Turns a LinkedIn profile URL into a direct mobile number, using Emelia's phone finder. It counts the rows, states the credit cost before spending anything, runs the lookups, and reports found and not found without rounding either up.

The number is data. Emelia does not dial it, does not record calls, and does not run a dialler. You call from your own phone, or you push the number into your CRM and your team calls from there.

When to use it

Use it when a segment is worth a call: high value accounts, a warm reply that went quiet, an executive who never answers email, a market where email is weak.

Do not use it as a default step on a whole list. A mobile costs roughly fifty times what an email costs, so a phone run on 500 rows is a budget decision, not a formality. Enrich the top slice, not the list.

Use a different skill when:

  • The list is not filtered. outreach-filter first, always.
  • You want email addresses. That is outreach-find-email, and it is far cheaper.
  • You want the full waterfall with one budget for the run. That is outreach-enrich, which calls this skill last and only on the rows you flagged.

Inputs

The finder takes exactly one input.

FieldRequiredNotes
linkedinUrlyesA public LinkedIn profile URL: https://www.linkedin.com/in/<slug>.

There is no name plus company path for phones. If a row has no LinkedIn URL, it cannot be looked up. Say so, and count those rows separately in the summary.

URL shapes that work and do not work:

  • https://www.linkedin.com/in/marie-dupont-1a2b3c works. This is the shape to use.
  • linkedin.com/in/marie-dupont-1a2b3c works, but add the scheme yourself so the row is unambiguous later.
  • A Sales Navigator lead URL (.../sales/lead/ACwAAA...) is not a profile URL. Get the public profile URL first, in the source data or through outreach-leads.
  • A company page URL is not a person. Skip the row.

Files: outreach/leads.csv in, outreach/leads.csv and outreach/enrichment.json out.

Access: EMELIA_API_KEY exported in the shell. The REST API is the documented path and this skill assumes it. If the Emelia MCP server is also configured, its find_phone tool wraps the same two calls. Without either, run in dry run: count the eligible rows, quote the cost, spend nothing.

How to do it

1. Count, quote, wait for a yes

At the time of writing, Emelia bills the phone finder 50 credits per number actually found. The credits are taken when the job starts and given back when the job ends not_found, so a miss is free and a hit is expensive. Confirm the current rate on your plan page before a large run.

Print this and stop:

Phone finder on outreach/leads.csv

  Rows in the file            412
  No LinkedIn URL             147   cannot be looked up
  Already have a phone          6   skipped
  Eligible                    259

  Cost: 50 credits per number found, nothing for a miss.
  Worst case 12,950 credits. At a realistic 25 to 40% hit rate,
  expect 3,200 to 5,200 credits.
  Your balance: 6,000 credits.

That is most of your balance for 259 rows. Two other options:
  - the 40 rows flagged "priority" in the list: 500 to 800 credits
  - a 20 row sample to measure your real hit rate: about 300 credits

Which one?

Always offer the smaller option when the full run is over a quarter of the balance. This is the step where users lose money they did not mean to spend.

2. One lookup

Two REST calls: create the job, then poll it every 2 to 3 seconds.

curl -s -X POST https://api.emelia.io/tools/find/phone \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"linkedinUrl":"https://www.linkedin.com/in/marie-dupont-1a2b3c"}'
# {"success":true,"jobId":"66f0d1..."}

curl -s https://api.emelia.io/tools/find/phone/66f0d1... \
  -H "Authorization: $EMELIA_API_KEY"
# {"success":true,"data":{"linkedinUrl":"https://www.linkedin.com/in/marie-dupont-1a2b3c",
#   "phoneNumber":"+33612345678","country":"FR","qualification":"found",
#   "status":"done","date":"..."}}

If the MCP server is configured, one call does both:

find_phone
  linkedinUrl: "https://www.linkedin.com/in/marie-dupont-1a2b3c"

It posts the job, polls it for up to 90 seconds, and returns the same data object.

3. Read the result

FieldValuesMeaning
statusrunning, done, errorThe job. error means no result, retry once at most.
qualificationfound, not_foundThe number. There is no middle value here, unlike the email finder.
phoneNumberE.164 stringPresent only when found.
countryISO 2 letter codeThe country the number belongs to, present when found.

Phone jobs finish later than email jobs. The result arrives at Emelia through a callback from the data provider, so the job can sit in running well past the 90 seconds an MCP call waits. A result of {"jobId": "...", "status": "running", "note": "..."} is normal here. Keep the jobId and collect it later, with a plain GET /tools/find/phone/{jobId} or with the MCP tool built for it:

get_enrichment_result
  type: "find_phone"
  jobId: "66f0d1..."

Never resubmit a lookup because your client stopped waiting. A second submission on the same profile is charged again, even when Emelia already has the number: results are cached on the server side, and a cached hit is still billed. One lookup per person, ever. This is why you write every result to disk as it arrives.

4. Bulk: the loop

There is no bulk endpoint. Bulk is the single lookup repeated, so the loop carries the discipline.

  1. Build the work list: filtered rows, with a LinkedIn profile URL, with no phone number yet, and inside the segment the user approved. Nothing else.
  2. Deduplicate on the profile slug (the part after /in/), not on the full URL. The same person with and without a trailing slash or a ? tracking parameter is one person and must cost you one lookup.
  3. Run one lookup at a time, or at most three in parallel. Roughly one new job per second. Your plan's ceiling is 100 requests per minute on Start, 300 on Grow, 1,000 on Scale, and 30 with no subscription.
  4. Write every result to outreach/enrichment.json as it arrives, before moving to the next row.
  5. Because results come back late, run the loop in two passes: submit and collect what is ready, then come back for the jobId values still running. Two minutes between passes is enough in most cases.
  6. Print a progress line every 25 rows: done, found, not found, credits spent.
  7. On a credit error (HTTP 402, or a message mentioning credits), stop. Report where you stopped. Do not retry.
  8. On HTTP 429, wait 60 seconds and resume from the same row.

Set a hard stop before you start: a number of credits, not a number of rows. When the running total reaches it, stop the loop, write the file, and report. Ask before going further.

5. Mobile or landline, and what the number is worth

The API returns a number and its country. It does not return a line type, so treat "this is a mobile" as something you check, not something you were told.

  • The finder targets direct mobile numbers, which is the point: a switchboard number is not worth 50 credits.
  • In France, a mobile starts +336 or +337. Anything starting +331 to +335 or +339 is a landline or a special number.
  • In most of Europe the mobile prefix is distinct in the same way (+447 in the UK, +3936 in Italy, +491 in Germany).
  • In North America there is no prefix that separates mobile from landline. A +1 number cannot be classified from its digits. Say so rather than guessing.

Write phone_type as mobile when the prefix proves it, unknown when it does not. Never write mobile because you assume it.

Two rules for what happens next:

  • Numbers go to a human or to a CRM, not to an autodialler you built in a script.
  • Cold calling rules differ from email rules. In France, check the Bloctel opt-out covers the market you are calling.

6. Realistic hit rates

Rules of thumb, not measured Emelia figures.

PopulationExpect
French and European B2B profiles with a complete LinkedIn page25 to 40%
US profilesusually a few points higher
Executives of large groups, public sector, regulated industries10 to 20%
Sparse profiles, no recent activity, no current employerlow, and the profile is the reason

If a 50 row sample comes back under 10%, the segment is not a phone segment. Say that plainly and stop, rather than spending another 10,000 credits to prove it.

Output

outreach/leads.csv keeps every original column and gains four:

first_name,last_name,company_name,linkedin_url,source,phone,phone_country,phone_type,phone_status
Marie,Dupont,Emelia,https://www.linkedin.com/in/marie-dupont-1a2b3c,basile,+33612345678,FR,mobile,found
Paul,Martin,Emelia,https://www.linkedin.com/in/paul-martin-9z8y7x,basile,,,,not_found
Sofia,Neri,Kotive,https://www.linkedin.com/in/sofia-neri,basile,+15125550142,US,unknown,found
Julien,Roche,Emelia,,basile,,,,no_linkedin_url

outreach/enrichment.json, the slice this skill owns:

{
  "run": {
    "id": "2026-09-08-1530",
    "step": "find_phone",
    "list": "outreach/leads.csv",
    "segment": "priority accounts",
    "started_at": "2026-09-08T15:30:02Z",
    "finished_at": "2026-09-08T15:58:40Z"
  },
  "cost": {
    "rate_at_run_time": "50 credits per number found, refunded on a miss",
    "budget_credits": 2000,
    "credits_spent": 1650,
    "credits_before": 6000,
    "credits_after": 4350,
    "stopped_on_budget": false
  },
  "counts": {
    "rows_in": 412,
    "skipped_no_linkedin_url": 147,
    "skipped_already_had_phone": 6,
    "eligible": 259,
    "looked_up": 96,
    "found": 33,
    "not_found": 62,
    "error": 1,
    "pending": 0
  },
  "rates": { "discovery": 0.344, "note": "33 of 96 looked up" },
  "line_types": { "mobile": 27, "unknown": 6 },
  "notes": [
    "Run limited to the 96 rows tagged priority, agreed with the user.",
    "163 eligible rows were not looked up and are still available.",
    "6 US numbers cannot be classified as mobile or landline from the prefix."
  ]
}

Then say it in words:

96 lookups, 33 numbers found (34.4%), 62 not found, 1 error. 1,650 credits spent,
4,350 left. 27 are certainly mobiles, 6 US numbers cannot be classified.
163 eligible rows were not touched.

Checks before finishing

  • The cost was stated, with a smaller alternative when the run was large, and the user said yes before the first paid call.
  • Every eligible row was looked up at most once. No profile slug appears twice in the job log.
  • leads.csv keeps every original column, in order, unchanged.
  • No number was written for a row whose job returned not_found. An empty cell is the correct answer.
  • phone_type says mobile only where the prefix proves it.
  • No jobId is left unresolved without being listed as pending.
  • The summary gives the hit rate, the credits spent, and how many eligible rows were deliberately not touched.

Failure modes

400 on submission. The URL is not a profile URL: a Sales Navigator lead link, a company page, a search results page, or an empty string. Fix the input. No credits were taken because no job was created.

402, or an error mentioning credits. The balance cannot cover the next lookup. Stop the loop and report the resume point. At 50 credits a hit, this arrives faster than people expect.

429. Over the plan's request ceiling. Wait 60 seconds and resume from the same row.

Everything stuck in running. Expected for this tool, not a bug. Collect with get_enrichment_result in a second pass. If a job is still running after 30 minutes, record it as pending and tell the user. Never resubmit.

A number found for the wrong person. Happens when the profile URL in the row was copied from a search result and does not belong to the person named on the row. Check the profile slug against the name before you call. If they disagree, drop the number rather than calling a stranger.

A number that looks like a switchboard (ends in several zeros, matches the company's public number). Keep it, mark phone_type: unknown, and do not present it to the user as a direct line.

The user asks you to call. Refuse politely: this skill produces numbers, it does not place calls, and no part of this repository does.

Limits

This skill does not call anyone, does not send SMS, does not connect to a dialler, and does not check a number against a do-not-call register. It cannot tell you whether a number is still in service, and it cannot classify a North American number as mobile or landline.

It cannot work without a LinkedIn profile URL: there is no name plus company path for phones.

A found number is a number the data provider associated with that profile. It is not a promise that the person will answer, that it is their work phone, or that they are happy to be called. Treat it accordingly, and say so to the user.

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-find-phone 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.