linkedin-enrich — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited linkedin-enrich (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Given N LinkedIn profile URLs, this skill returns enriched data: current role, company, location, experience timeline, skills, and email when findable. Output as a markdown table, CSV, or JSON.
linkupapi_list_accounts.linkupapi_login directly (platform=linkedin, with email+password OR a login_token). On checkpoint_required → run linkupapi_checkpoint.status = connected account exists, present the connected accounts via AskUserQuestion (single-select). Each option label is the account display name; description shows email + country. The chosen account_id is used for the linkedin_profiles get calls in Stage 2 (which consume the 100 profile gets / day cap on that account). The linkupapi_enrich actions (find_email, validate_email) don't require an account_id — they use LinkUp's managed pipeline.Before Stage 1, run linkupapi_get_logs for last 24h on the chosen account, count today's linkedin_profiles/get. The skill enriches only min(N, 100 - already_used_today). If user provides 50 URLs but 80 profile gets are already used today, enrich the 20 most-prioritized and defer the rest.
If linkupapi_get_logs is unavailable, fall back to scanning today's ./campaigns/*.md and ./enrichments/*.md for linkedin_profiles/get evidence and counting.
AskUserQuestion:
.csv / .txt / .md — agent reads via Read)./enrichments/{date}-{slug}.csv./enrichments/{date}-{slug}.jsonEcho the plan with daily-cap math:
URLs provided: 50
URLs after dedupe: 47
Profile budget today: 100 used / 100 cap → 0 remaining 🚫 STOP
OR
Profile budget today: 20 used / 80 remaining → enrich up to 47 ✓
Output format: CSV → ./enrichments/2026-04-27-leads-batch-1.csvWait for "yes" before continuing.
Extract the LinkedIn handle from each URL (/in/<handle> or /in/<handle>/). Drop:
/company/ URLs (this skill is profiles only — surface them in a separate "skipped: company URLs" list)LinkedIn Member anonymized URLs (URL contains headless) — cannot be enrichedShow the user the parsed count vs original. If >20% were dropped, ask for confirmation before continuing (likely a bad input list).
Tool: linkedin_profiles get with identifier=<handle>, sequential with 2-3s sleep between calls (longer than outreach skill — bulk enrichment patterns are more closely watched by LinkedIn).
HARD CAP at remaining 100/day budget. If reached mid-run, save partial results, mark remaining as "deferred to tomorrow", and stop gracefully.
For each profile, extract per the user's chosen output fields. Always capture:
first_name, last_name, headlineexperience[0].company → current_companyexperience[0].title → current_titleexperience[0].company_url → derive company_domain for email lookup (extract last path segment of LinkedIn company URL → match against known domain registry, OR pass the company name to email finder)location, industryMark a profile as enrichment_failed (not budget-exhausted) if the API returns 404 / private / member-removed. Don't retry.
Tool: linkupapi_enrich action find_email.
Mode A — by LinkedIn URL (cleanest):
{"action": "find_email", "params": {"linkedin_url": "<full url>"}}Mode B — by name + company domain (fallback when Mode A returns nothing):
{"action": "find_email", "params": {
"first_name": "...", "last_name": "...",
"company_domain": "<domain>"
}}No LinkedIn-side cap (it's not a LinkedIn API call), but enforce sane volume — stop if more than 200 in a single run unless user explicitly approves.
For each profile that returns an email, run linkupapi_enrich validate_email if user opted in (catches typos and dead inboxes). Skip validation on profiles where find_email returned nothing.
Build the chosen output format.
Markdown table (≤50 URLs, default):
| # | Name | Title | Company | Location | Email | Valid |
|---|------|-------|---------|----------|-------|-------|
| 1 | Oleksii Kratko | Founder/CEO | Snov.io | Toronto | [email protected] | ✓ |CSV (>50 or chosen): write to ./enrichments/{date}-{slug}.csv with header row, one row per profile (including failed enrichments with reason).
JSON: array of objects with all enriched fields, suitable for re-import into other workflows.
Show the user the output path + a preview of the first 5 rows. For inline markdown, render the table directly.
Always write a log regardless of output format:
./enrichments/{YYYY-MM-DD}-{slug}.md:
url | reason (private / 404 / removed)The log lets the user re-run failed/deferred ones tomorrow without losing track.
Before Stage 2, glob ./enrichments/*.md and ./enrichments/*.json for any profile_url already enriched in the last 30 days. Reuse the cached enrichment instead of re-paying for the get call. Surface the dedup count and savings in the Stage 0 echo.
This is the single biggest cost saver — repeat enrichments are a waste.
/in/headless?... can't be enriched. Filter at Stage 1.identifier, not the URL.linkedin-outreach Stage 4 verification. The outreach skill should check ./enrichments/ first to avoid re-paying for the same get.find_email sometimes returns personal email (e.g. [email protected]) instead of professional. Surface both when both exist and let the user choose downstream.| Tool | Action | Daily cap |
|---|---|---|
linkedin_profiles | get | 100/day shared |
linkupapi_enrich | find_email | — |
linkupapi_enrich | validate_email | — |
linkupapi_enrich | reverse_email | (not used here) |
linkupapi_get_logs | — | run at Stage 0 |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.