LinkedIn

user-posts

linkedin.com
InstallationInstalls just this skill for your agent
Side effectread
Authsession
Needsnavigate · evaluate
Summary

Fetch a LinkedIn person's recent posts (activity) given their slug (vanityName, e.g. 'gastonfrancois').

Params
{"slug":"string (the profile vanityName, the segment of linkedin.com/in/<slug>, e.g. 'gastonfrancois')"}
Returns
{"slug":"string (the slug queried)","count":"int (number of posts parsed from the first render)","posts":"array of { text:string|null (full text), urn:string (urn:li:activity:...), url:string (permalink /feed/update/<urn>/), reactions:int, comments:int, reposts:int, time:string|null (relative age, e.g. '10 months'), isRepost:bool }"}
SKILL.md71 lines

Intent

Fetch a LinkedIn person's recent posts (activity) given their slug (vanityName, e.g. 'gastonfrancois'). Read-only. Navigates to https://www.linkedin.com/in/{{slug}}/recent-activity/all/ (the All activity > Posts page), waits for the Voyager feed to render and parses each update card (div.feed-shared-update-v2 with data-urn=urn:li:activity:...). For each post it returns: text (full text, stripping the truncation suffix '…more'/'see more'), urn (urn:li:activity:...), url (permalink /feed/update/<urn>/), reactions (int, parses '1.234'/'2 thousand'/'1,2 k'), comments (int from the aria-label 'N comments'), reposts (int from 'N reposts'/'reposts' if present, otherwise 0), time (relative age like '10 months', '1 w.'), isRepost (bool: detects a 'reposted'/'shared' header or a reshare container). PARAM: slug = vanityName (the segment of linkedin.com/in/<slug>, comes from the profileUrl of linkedin-search-people). Returns { slug, count, posts:[{text,urn,url,reactions,comments,reposts,time,isRepost}] }. NOTE: a DOM extractor over the rendered cards was chosen (not direct HTTP) because the trace did not capture network -> there is no queryId for the profileUpdates fetch (it rotates on every LinkedIn deploy); the DOM is more stable for this case. It only fetches the posts from the first render (no infinite scroll). Requires a logged-in LinkedIn session. If LinkedIn changes the card classes (feed-shared-update-v2 / social-details-social-counts / update-components-*), recapture.

Preconditions

Execute

  1. navigate → https://www.linkedin.com/in/{{slug|encode}}/recent-activity/all/
  2. evaluate the Extractor below with params. It does the in-page work and returns the JSON in returns.

Extractor

( async (root, params) => { const slug = String((params && params.slug) || '').trim(); if (!slug) throw new Error('missing slug'); const scope = root && root.querySelectorAll ? root : (typeof document !== 'undefined' ? document : root); const toInt = (s) => { if (s == null) return 0; let t = String(s).toLowerCase().trim(); if (!t) return 0; let mult = 1; if (/\bmil\b|\bk\b/.test(t)) mult = 1000; if (/\bmill|\bm\b/.test(t)) mult = 1000000; const m = t.match(/([\d.,]+)/); if (!m) return 0; let num = m[1]; if (mult > 1) { num = num.replace(/./g, '').replace(',', '.'); return Math.round(parseFloat(num) * mult) || 0; } num = num.replace(/[.,]/g, ''); return parseInt(num, 10) || 0; }; const txt = (el) => el ? (el.innerText || el.textContent || '').trim() : ''; let cards = Array.from(scope.querySelectorAll('div.feed-shared-update-v2[data-urn^="urn:li:activity"], div[data-urn^="urn:li:activity"]')); const seen = {}; cards = cards.filter((c) => { const u = c.getAttribute('data-urn') || ''; if (!u || seen[u]) return false; seen[u] = 1; return true; }); const posts = cards.map((card) => { const urn = card.getAttribute('data-urn') || null; const url = urn ? ('https://www.linkedin.com/feed/update/' + urn + '/') : null; let textEl = card.querySelector('.update-components-text, .feed-shared-update-v2__description, .feed-shared-inline-show-more-text, .update-components-update-v2__commentary'); let text = txt(textEl); text = text.replace(/\s*[….]{1,3}\s*(m[áa]s|see more|ver m[áa]s|more)\s*$/i, '').trim(); const headerTxt = txt(card.querySelector('.update-components-header, .update-components-actor__description, .feed-shared-actor__description')); const isRepost = /reposte|compart|repost|shared this/i.test(headerTxt) || !!card.querySelector('.feed-shared-reshared-update, .update-components-mini-update-v2, [data-test-reshared]'); let time = txt(card.querySelector('.update-components-actor__sub-description, .feed-shared-actor__sub-description, .update-components-actor__sub-description-link')); time = time.replace(/\s*•.$/, '').trim() || null; let reactions = 0; const socialCounts = card.querySelector('.social-details-social-counts, .social-details-social-activity, .feed-shared-social-action-bar-counts'); if (socialCounts) { const reactBtn = socialCounts.querySelector('.social-details-social-counts__reactions-count, .social-details-social-counts__reactions, [aria-label="reacc" i], [aria-label*="reaction" i]'); if (reactBtn) { const al = reactBtn.getAttribute('aria-label') || txt(reactBtn); reactions = toInt(al); } if (!reactions) { const rspan = socialCounts.querySelector('.social-details-social-counts__reactions-count'); if (rspan) reactions = toInt(txt(rspan)); } } let comments = 0, reposts = 0; const countItems = socialCounts ? Array.from(socialCounts.querySelectorAll('li, button, span')) : []; countItems.forEach((it) => { const lab = ((it.getAttribute && it.getAttribute('aria-label')) || txt(it) || '').toLowerCase(); if (!lab) return; if (/coment|comment/.test(lab) && !comments) comments = toInt(lab); if (/(repost|veces compart|compartid|share)/.test(lab) && !reposts) reposts = toInt(lab); }); return { text: text || null, urn, url, reactions, comments, reposts, time, isRepost }; }).filter((p) => p.urn); return { slug, count: posts.length, posts }; } )

Success assertion

{
  "expr": "div.feed-shared-update-v2[data-urn^=\"urn:li:activity\"], div[data-urn^=\"urn:li:activity\"]",
  "type": "dom"
}