Search LinkedIn posts by keyword and return them enriched with author, text, reactions, comment counts, the COMMENTERS and (optionally) the REACTORS/likers - all via API (name+slug for each person). Paginated, fetched in parallel.
{"query":"string (search text, required)","count":"int optional (default 25, max 500; auto-paginated in pages of 50)","sortBy":"string optional: 'relevance' (default) or 'date_posted'","enrich":"bool optional (DEFAULT FALSE): fetch each post's FULL text + author headline + social counts + COMMENTERS via one /feed/updates call per post. Leave off for the fast sweep - the search response already yields author name+slug, post type and a text preview for free (0 extra calls). Turn on only for the posts you shortlist.","reactors":"bool optional (default false): ALSO fetch the people who reacted/liked each post (name+slug+reactionType+degree). Implies enrich (needs the ugcPost id from /feed/updates). One extra API call per post - enable only on shortlisted posts."}{"count":"int","query":"string","sortBy":"string","results":"array. FAST SWEEP (default): { urn, url, author (first name), authorSlug, type ('share'|'ugcPost'), textPreview (opening words of the post, from the post slug) } - all parsed from the ONE search call, no per-post fetch. ENRICHED (enrich:true) adds: authorHeadline, time, text (full body), reactions (int), reactionTypes ({LIKE,PRAISE,...:count}), comments (int count), commenters (array of { name, slug, headline, text }); and reactors (array of { name, slug, type, degree, headline }) when reactors:true - via the activity-urn reactions endpoint, so it also works on share/repost posts. All people carry a slug for get-profile/connect."}Search LinkedIn posts (content search) by keyword - everything via API, no DOM. TWO-PHASE, cost-aware:
(1) SWEEP (default, 1 call per page of ~50): POST the content-search SDUI action (com.linkedin.sdui.search.contentSearchResults), AUTO-PAGINATING until count. The RSC response embeds, per post, a postSlugUrl (e.g. /posts/{authorSlug}_{hyphenated-text-preview}-{share|ugcPost}-{id}-{code}) next to actorName and the canonical activityId. We parse those inline to return author name+slug, post type, and a textPreview - enough to READ and shortlist real signals WITHOUT any per-post call. This is the big speedup: a 100-post sweep is ~4 calls, not ~100.
(2) ENRICH (enrich:true, only on shortlisted urns): for each post, /voyager/api/feed/updates/{urn} returns clean JSON with the FULL post text, author headline, social counts AND the comment thread with each commenter's miniProfile (name, publicIdentifier slug, occupation). If reactors:true, also GET /voyager/api/feed/reactions?q=reactionType&threadUrn={activityUrn} for the FULL reactor list (name, slug, reaction type, degree, headline). This endpoint keys on the ACTIVITY urn (which every post has), so it works for EVERY post type including share/repost posts - unlike the older ugcPost-only reactors SDUI action, which returned [] on shares. Everyone (author, commenters, reactors) carries a slug → compose with get-profile / connect. Read-only.
Note: the text preview is only the post's opening words. When the preview is just hashtags or too thin to judge, enrich that post for the full body.
params. It does the in-page work and returns the JSON in returns.( async (root, params) => { const q = String((params && params.query) || '').trim(); if (!q) throw new Error('missing query'); let count = parseInt(String((params && params.count) || '25'), 10) || 25; if (count > 500) count = 500; if (count < 1) count = 25; let sortBy = String((params && params.sortBy) || 'relevance').trim(); if (sortBy !== 'date_posted' && sortBy !== 'relevance') sortBy = 'relevance'; const doReactors = !!(params && (params.reactors === true || params.reactors === 'true')); // enrich defaults FALSE (fast sweep). reactors:true forces enrich (needs the ugcPost id from /feed/updates). const doEnrich = doReactors || !!(params && (params.enrich === true || params.enrich === 'true')); const csrf = (document.cookie.match(/JSESSIONID="?([^";]+)"?/) || [])[1] || ''; if (!csrf) throw new Error('re-auth: no JSESSIONID cookie (LinkedIn session not started)'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const dec = (v) => { try { return decodeURIComponent(v); } catch (e) { return v; } }; // Parse /posts/{authorSlug}{hyphenated-preview}-{share|ugcPost}-{id}-{code} → { authorSlug, type, textPreview }. const parseSlugUrl = (url) => { const path = (String(url).match(//posts/([^"?\s]+)/) || [])[1] || ''; const us = path.indexOf(''); const mType = path.match(/-(share|ugcPost)-(\d+)-/); const authorSlug = us > 0 ? dec(path.slice(0, us)) : null; let textPreview = null, type = null; if (mType) { type = mType[1]; if (us > 0) { const end = path.indexOf('-' + type + '-'); if (end > us) textPreview = path.slice(us + 1, end).replace(/-/g, ' ').trim() || null; } } return { authorSlug: authorSlug, type: type, textPreview: textPreview }; }; const fetchPage = async (startIndex, pageSize) => { const uuid = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : (Date.now() + '-' + Math.random().toString(16).slice(2)); const payload = { startIndex: startIndex, keywords: q, count: pageSize, sortBy: [sortBy], postedBy: [], datePosted: [], contentType: [], fromMember: [], mentionsOrganization: [], mentionsMember: [], fromOrganization: [], authorCompany: [], authorIndustry: [], authorJobTitle: [], spellCheckEnabled: true, clusterStartPosition: 0, searchId: uuid }; const args = { $type: 'proto.sdui.actions.requests.RequestedArguments', payload: payload, requestedStateKeys: [], requestMetadata: { $type: 'proto.sdui.common.RequestMetadata' } }; const body = { pagerId: 'com.linkedin.sdui.search.contentSearchResults', clientArguments: { $type: 'proto.sdui.actions.requests.RequestedArguments', payload: payload, requestedStateKeys: [], requestMetadata: { $type: 'proto.sdui.common.RequestMetadata' }, states: [], screenId: 'com.linkedin.sdui.flagshipnav.search.SearchResultsContent' }, paginationRequest: { $type: 'proto.sdui.actions.requests.PaginationRequest', pagerId: 'com.linkedin.sdui.search.contentSearchResults', requestedArguments: args }, trigger: { $case: 'itemDistanceTrigger', itemDistanceTrigger: { $type: 'proto.sdui.actions.requests.ItemDistanceTrigger', preloadDistance: 3, preloadLength: 1500 } }, retryCount: 0 }; const r = await fetch('/flagship-web/rsc-action/actions/pagination?sduiid=com.linkedin.sdui.search.contentSearchResults', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json', 'csrf-token': csrf, 'x-li-rsc-stream': 'true', 'accept': '/' }, body: JSON.stringify(body) }); if (r.status === 401 || r.status === 403) throw new Error('re-auth: HTTP ' + r.status); const t = await r.text(); // Canonical, ordered activityIds (reliable for pagination + completeness). const ids = []; const re = /"feedUpdateUrn":{"updateUrnActivityUrn":{[^}]"activityId":"(\d+)"/g; let m; while ((m = re.exec(t))) ids.push(m[1]); // Free per-post metadata: author name+slug, type, text preview - from the postSlugUrl block next to actorName+activityId. const meta = {}; const bRe = /"actorName":"([^"])","messagingFeedUpdateUrn":[\s\S]{0,160}?"activityId":"(\d+)"[\s\S]{0,120}?"postSlugUrl":"([^"]+)"/g; let b; while ((b = bRe.exec(t))) { const p = parseSlugUrl(b[3]); if (!meta[b[2]]) meta[b[2]] = { firstName: b[1] || null, authorSlug: p.authorSlug, type: p.type, textPreview: p.textPreview }; } return { ids: ids, meta: meta }; }; const PAGE = 50; const urns = []; const seen = new Set(); const metaById = {}; for (let start = 0; urns.length < count && start <= 600; start += PAGE) { let page; try { page = await fetchPage(start, PAGE); } catch (e) { if (String(e.message).indexOf('re-auth') === 0) throw e; break; } const ids = page.ids; Object.assign(metaById, page.meta); let added = 0; for (const id of ids) { const urn = 'urn:li:activity:' + id; if (!seen.has(urn)) { seen.add(urn); urns.push(urn); added++; } } if (ids.length === 0 || added === 0) break; if (urns.length < count) await sleep(150); } if (urns.length > count) urns.length = count; const mk = (urn) => { const id = urn.split(':').pop(); const md = metaById[id] || {}; return { urn: urn, url: 'https://www.linkedin.com/feed/update/' + urn + '/', author: md.firstName || null, authorSlug: md.authorSlug || null, type: md.type || null, textPreview: md.textPreview || null }; }; if (!doEnrich) return { count: urns.length, query: q, sortBy: sortBy, results: urns.map(mk) }; const H = { 'csrf-token': csrf, 'accept': 'application/json', 'x-restli-protocol-version': '2.0.0' }; // Fetch the full reactor list via the REST reactions endpoint. The correct thread urn is // POST-TYPE dependent: ugcPost posts key on the ugcPost urn; share/repost posts key on the // activity urn. So we try the candidates in order and take the first non-empty result - this // covers EVERY post type (the older ugcPost-only SDUI action returned [] on shares). // Each Reaction entity carries name+slug+type+degree+headline. const parseReactions = (j) => { const out = []; const seen = new Set(); for (const e of (j.included || [])) { if ((e.$type || '').indexOf('feed.social.Reaction') === -1 || !e.reactionType) continue; let slug = null; try { const at = (e.navigationContext && e.navigationContext.actionTarget) || ''; const mm = at.match(//in/([^/?#]+)/); if (mm) slug = decodeURIComponent(mm[1]); } catch (er) {} let name = null; try { name = e.name && e.name.text; } catch (er) {} let degree = null; try { degree = e.supplementaryActorInfo && e.supplementaryActorInfo.text; } catch (er) {} let headline = null; try { headline = e.description && e.description.text; } catch (er) {} const key = slug || name; if (!key || seen.has(key)) continue; seen.add(key); out.push({ name: name || null, slug: slug, type: e.reactionType, degree: degree || null, headline: headline || null }); } return out; }; const fetchReactors = async (threadUrns) => { const rh = { 'csrf-token': csrf, 'accept': 'application/vnd.linkedin.normalized+json+2.1', 'x-restli-protocol-version': '2.0.0' }; for (const u of threadUrns) { if (!u) continue; try { const r = await fetch('/voyager/api/feed/reactions?count=100&start=0&q=reactionType&threadUrn=' + encodeURIComponent(u), { headers: rh, credentials: 'include' }); if (!r.ok) continue; const out = parseReactions(await r.json()); if (out.length) return out; } catch (er) {} } return []; }; const enrichOne = async (urn) => { const out = mk(urn); let gotActor = false; try { const r = await fetch('/voyager/api/feed/updates/' + encodeURIComponent(urn), { headers: H, credentials: 'include' }); if (r.status === 401 || r.status === 403) throw new Error('re-auth: HTTP ' + r.status); if (!r.ok) { out.error = 'http ' + r.status; return out; } const j = await r.json(); const walk = (o) => { if (!o || typeof o !== 'object') return; if (!gotActor && o.actor && o.actor.name && o.actor.name.text) { gotActor = true; out.author = o.actor.name.text; // full name overrides the sweep's first-name try { const at = (o.actor.navigationContext && o.actor.navigationContext.actionTarget) || ''; const mm = at.match(//(?:in|company|school)/([^/?#]+)/); if (mm) out.authorSlug = decodeURIComponent(mm[1]); } catch (e) {} try { out.authorHeadline = o.actor.description.text; } catch (e) {} try { out.time = o.actor.subDescription.text; } catch (e) {} } if (out.text == null && o.commentary && o.commentary.text && o.commentary.text.text) out.text = o.commentary.text.text; if (out.reactions == null && o.totalSocialActivityCounts) { const c = o.totalSocialActivityCounts; out.reactions = c.numLikes; out.comments = c.numComments; out.reactionTypes = (c.reactionTypeCounts || []).reduce((a, x) => { a[x.reactionType] = x.count; return a; }, {}); } for (const k in o) walk(o[k]); }; walk(j); // commenters (same payload) const commenters = []; const cseen = new Set(); const cwalk = (o) => { if (!o || typeof o !== 'object') return; if (o.commenterProfileId || (o.commenter && (o.commentaryV2 || o.comment || o.commentV2))) { let mp = null; (function f(x) { if (!x || typeof x !== 'object' || mp) return; if (x.firstName !== undefined && x.lastName !== undefined && x.publicIdentifier !== undefined) { mp = x; return; } for (const k in x) f(x[k]); })(o); let text = null; try { text = (o.commentaryV2 && o.commentaryV2.text) || (o.comment && o.comment.values && o.comment.values.map((v) => v.value).join('')) || (o.commentV2 && o.commentV2.text) || null; } catch (e) {} if (mp && mp.publicIdentifier && !cseen.has(mp.publicIdentifier)) { cseen.add(mp.publicIdentifier); commenters.push({ name: [mp.firstName, mp.lastName].filter(Boolean).join(' '), slug: mp.publicIdentifier, headline: mp.occupation || null, text: text ? String(text) : null }); } } for (const k in o) cwalk(o[k]); }; cwalk(j); out.commenters = commenters; // reactors (optional): try the ugcPost urn (ugcPost posts) then the activity urn (shares). if (doReactors) { try { const ugc = (JSON.stringify(j).match(/urn:li:ugcPost:\d+/) || [])[0]; out.reactors = await fetchReactors([ugc, urn]); } catch (e) { out.reactors = []; } } return out; } catch (e) { if (String(e.message).indexOf('re-auth') === 0) throw e; out.error = String(e).slice(0, 60); return out; } }; const CONC = 6; const results = new Array(urns.length); let idx = 0; const worker = async () => { while (true) { const i = idx++; if (i >= urns.length) break; results[i] = await enrichOne(urns[i]); await new Promise((res) => setTimeout(res, 90 + Math.random() * 160)); } }; await Promise.all(Array.from({ length: Math.min(CONC, urns.length || 1) }, worker)); return { count: results.length, query: q, sortBy: sortBy, results: results }; } )
{
"expr": "typeof result === 'object' && Array.isArray(result.results)",
"type": "value"
}