Loading...
Loading...
Map who a person knows from one LinkedIn profile URL, when you cannot see their connection list. Derives a per-person angle set from their own work history, education and posts, fans those angles out to parallel agents, merges and scores every candidate on evidence strength, resolves each row to a real profile and current employer, and ships a spreadsheet you can slice by where each person works today (a filterable people tab plus an employer x tier pivot). Works for any function (engineering, recruiting, sales, research, exec) and any career shape. Use for "who does this person know", "find connections of this LinkedIn URL", "map this person's network", "who is in their orbit", "who do they know at a company", "infer connections from job history and post engagement", /social-connections.
npx skill4agent add crustdata/skills social-connections.xlsxhttps://install.crustdata.com/mcpexecute({ code })await callTool(name, params)// user query: ...// model query: ...r.okparallelMapfieldscredits_usedaccount_creditsphase 0 pull the target profile and posts (1 execute, main loop)
phase 1 derive the angle set, write profile.md + BRIEF.md
phase 2 quote the cost, then fan out one agent per angle (6-10 angles)
phase 3 merge + score (scripts/merge.py)
phase 4 resolution pass (person_enrich, 25 urls per call)
phase 5 re-merge + build the workbook (scripts/build_sheet.py)
phase 6 report, and record what was NOT coveredexecutelinkedin.com/in/...// user query: who does <url> know?
const url = "https://www.linkedin.com/in/<slug>"; // no trailing slash, no query string
const [p, posts] = await parallelMap([
() => callTool("person_enrich", { professional_network_profile_urls: [url],
fields: ["crustdata_person_id", "basic_profile", "professional_network", "experience", "education", "skills", "honors"] }),
() => callTool("social_post_list_live", { professional_network_profile_url: url, limit: 25,
fields: ["share_url", "backend_urn", "date_posted", "post_type", "text", "actor", "reposter", "engagement"] }),
], (f) => f());
if (!p.ok) return { error: p.message };
const row = p.data[0];
if (row.match_status !== "matched") return { status: row.match_status };
const t = row.matches[0].person_data;
const jobs = [...(t.experience?.employment_details?.current || []), ...(t.experience?.employment_details?.past || [])];
return {
person_id: t.crustdata_person_id, name: t.basic_profile?.name, headline: t.basic_profile?.headline,
connections: t.professional_network?.connections,
jobs: jobs.map((j) => pick(j, ["name", "title", "crustdata_company_id", "start_date", "end_date", "function_category", "description"])),
schools: (t.education?.schools || []).map((s) => pick(s, ["school", "degree", "field_of_study", "start_year", "end_year"])),
skills: t.skills?.professional_network_skills,
posts: posts.ok ? posts.data.posts.map((x) => ({ url: x.share_url, urn: x.backend_urn, date: x.date_posted,
type: x.post_type, author: x.actor?.name, author_url: x.actor?.professional_network_url,
author_type: x.actor?.actor_type, reposter: x.reposter?.name,
reactions: x.engagement?.total_reactions, comments: x.engagement?.total_comments,
text: (x.text || "").slice(0, 280) })) : { error: posts.message },
};match_statusredactednot_foundreferences/angles.md<somewhere>/<slug>-connections/raw/run.json{"name": "...", "linkedin_url": "...", "headline": "...", "connections": 500, "slug": "...", "person_id": 12345}profile.mdcrustdata_company_idactor/in/post_type| post_type | whose engagement | use it |
|---|---|---|
| the target's | mine reactors and commenters |
| mixed: the target's comment on someone else's post | mine commenters, read who they address |
| the original author's audience | skip |
original/company/...post_typecrustdata_company_idcompany_identifyBRIEF.mdreferences/output-contract.mdreferences/crustdata-traps.mdcrustdata_person_idprofile.mdperson_searchlimit: 0limit: 1fields: ["crustdata_person_id"]total_countsocial_post_searchlimit: 0credits_usednext_cursorbatch_person_searchraw/<slug>.jsonraw/<slug>.log.mdexecutecrustdata_person_id// model query: size the first-party engagement angle for the target
const id = inputs.person_id;
const q = {
commented_on: and_(eq("comments.crustdata_entity_id", id), eq("comments.actor_type", "person")),
reacted_to: and_(eq("reactors.crustdata_entity_id", id), eq("reactors.actor_type", "person")),
tagged_in: and_(eq("mentions.person_id", id), eq("actor.actor_type", "person")),
};
const out = await parallelMap(Object.entries(q), async ([k, filters]) => {
const r = await callTool("social_post_search", { filters, limit: 0 });
return [k, r.ok ? r.data.total_count : { status: r.status, error: r.message }];
});
return Object.fromEntries(out);tagged_incommented_onreacted_tofields: ["share_url", "date_posted", "actor", "comments"]comments.filter((c) => c.commenter?.crustdata_entity_id === id)comment_textsocial_post_list_livesocial_post_urlreactorscommentsfieldsmax_reactorsmax_commentsreaction_typereactorcommentercomment_textrun.jsonopenpyxlpython3 -c "import openpyxl"pip install openpyxlpython3 scripts/merge.py <run-dir>references/scoring.mdpython3 scripts/shard_resolve.py <run-dir> --shards 4executeperson_enrichparallelMapmatched_ontodo_N.jsoninputs.urlsraw/resolve/done_N.json// model query: resolve one shard of urn-only rows to profile + current employer
const out = await parallelMap(chunk(inputs.urls, 25), async (batch) => {
const r = await callTool("person_enrich", { professional_network_profile_urls: batch,
fields: ["basic_profile.name", "basic_profile.headline", "basic_profile.location",
"social_handles.professional_network_identifier.profile_url", "experience.employment_details.current"] });
if (!r.ok) return batch.map((u) => ({ query_url: u, error: r.message }));
return r.data.map((row) => {
if (row.match_status === "redacted") return { query_url: row.matched_on, redacted: true };
const p = row.matches?.[0]?.person_data;
if (!p) return { query_url: row.matched_on, status: row.match_status };
const cur = p.experience?.employment_details?.current || [];
const job = cur.find((j) => j.is_default) || cur[0] || {};
return { query_url: row.matched_on, resolved_url: p.social_handles?.professional_network_identifier?.profile_url,
name: p.basic_profile?.name, headline: p.basic_profile?.headline, location: p.basic_profile?.location?.raw,
current_company: job.name || "", current_title: job.title || "" };
});
});
return out.flat();query_urltodo_N.jsonerrorpython3 scripts/merge.py <run-dir> # folds in raw/resolve/done_*.json
python3 scripts/build_sheet.py --run-dir <run-dir> --dry-run
python3 scripts/build_sheet.py --run-dir <run-dir><slug>-connections.xlsxby employer--no-tier-dfinal.jsonREADME.mdr.okawait sleep(ms)partialinputsreferences/angles.mdreferences/output-contract.mdreferences/scoring.mdreferences/crustdata-traps.mdevals/test_merge.py