Verify whether a supplied LinkedIn job posting is accessible and live, then extract the visible job title, company, location, work mode, compensation, posting age, responsibilities, requirements, job criteria, and complete visible description. This is read-only and does not apply to the job or interact with the employer.
Use Cases
Use when the caller provides a LinkedIn job URL or numeric job ID and wants to confirm the listing details or retrieve its visible job description. Treat login walls, consent pages, unavailable postings, CAPTCHA, checkpoints, and rendering failures as explicit access or verification errors rather than empty fields.
Callers who start from a search rather than a known job ID can collect URLs with Find Most Recent LinkedIn Job Links and open each one here.
Automation Flow
- Construct the canonical posting URL directly; do not visit LinkedIn's homepage or search results:
https://www.linkedin.com/jobs/view/{job-id}/If the caller supplies a canonical LinkedIn job URL, normalize it to the/jobs/view/{job-id}/path while removing query parameters and fragments. Never invent a job ID. - In one browser-agent call, navigate directly to the canonical URL with
waitUntil: "domcontentloaded", wait about 1.5–3 seconds for rendering, then run the evaluator below. It dismisses visible blocking dialogs, expands the description, uses visible DOM and JSON-LD fallbacks, verifies live-page evidence, and extracts the requested fields.
Evaluator:
(() => {
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
const visible=el=>!!el&&(el.offsetWidth||el.offsetHeight||el.getClientRects().length)>0;
const abs=href=>{try{const u=new URL(href,location.href);u.search='';u.hash='';return u.href}catch{return null}};
[...document.querySelectorAll("button[aria-label='Dismiss'],button[aria-label='Close']")].filter(visible).forEach(b=>b.click());
[...document.querySelectorAll("button[aria-label='Show more'],button[aria-label*='Show more' i],button")].filter(b=>visible(b)&&/^(show more|see more)$/i.test(clean(b.innerText||b.getAttribute('aria-label')))).forEach(b=>b.click());
const json=[...document.querySelectorAll('script[type="application/ld+json"]')].map(s=>{try{return JSON.parse(s.textContent)}catch{return null}}).find(o=>o&&((o['@type']==='JobPosting')||(Array.isArray(o['@graph'])&&o['@graph'].some(x=>x&&x['@type']==='JobPosting'))));
const ld=json?.['@type']==='JobPosting'?json:json?.['@graph']?.find(x=>x&&x['@type']==='JobPosting')||{};
const url=abs(location.href)||location.href,pathId=(location.pathname.match(/\/jobs\/view\/(\d+)/)||[])[1]||null;
const body=clean(document.body?.innerText||'');
const title=clean(document.querySelector('h1')?.innerText)||clean(ld.title);
const header=clean(document.querySelector('h4')?.innerText);
const companyLink=document.querySelector('a[href*="/company/"]');
const company=clean(companyLink?.innerText)||clean(ld.hiringOrganization?.name);
const companyUrl=companyLink?abs(companyLink.href):null;
const locationText=clean(document.querySelector('.top-card-layout__second-subline,.job-details-jobs-unified-top-card__primary-description-container,.topcard__flavor--bullet')?.innerText)||clean(ld.jobLocation?.address?.addressLocality||ld.jobLocation?.address?.addressRegion||ld.jobLocation?.name);
const criteriaNode=document.querySelector('.description__job-criteria-list,.job-details,section.job-details');
const descriptionNode=document.querySelector('.show-more-less-html__markup,.description__text,section.description');
const criteriaText=clean(criteriaNode?.innerText);
const visibleDescription=clean(descriptionNode?.innerText);
const description=visibleDescription||clean(String(ld.description||'').replace(/<[^>]+>/g,' '));
const rawSectionText=[descriptionNode?.innerText||'',criteriaNode?.innerText||''].join('\n');
const posted=clean(document.querySelector('time')?.innerText)||document.querySelector('time')?.getAttribute('datetime')||ld.datePosted||null;
const salary=ld.baseSalary||{};
const range=salary.value||{};
const payBlock=[...document.querySelectorAll('.compensation__salary,.salary,[class*="salary" i],[class*="compensation" i],section,div')].find(e=>visible(e)&&/base pay range|pay range|salary range|compensation/i.test(clean(e.innerText))&&/\p{Sc}\s*[\d.,]|\d[\d.,]*\s*(?:per|\/)\s*(?:hour|year|month)/iu.test(clean(e.innerText))&&clean(e.innerText).length<300);
const compensationVisible=clean(payBlock?.innerText);
const ldCurrency=salary.currency||range.currency||'';const ldAmount=[range.minValue,range.maxValue].filter(x=>x!=null).join('–')||(range.value!=null?String(range.value):'');const ldUnit=range.unitText||salary.unitText||'';const compensation=compensationVisible||clean([ldCurrency,ldAmount,ldUnit].filter(Boolean).join(' '))||null;
const workMode=ld.jobLocationType||((locationText.match(/remote|hybrid|on-site|onsite|in office/i)||[])[0]||null);
const text=description+' '+criteriaText;
const section=(patterns)=>{const lines=rawSectionText.split(/\r?\n+/).map(clean).filter(Boolean);const i=lines.findIndex(x=>patterns.some(p=>p.test(x)));return i<0?[]:lines.slice(i+1).filter(x=>!(/^(about|responsibilities|requirements|qualifications|preferred|benefits|what you|who you)/i.test(x)&&x.length<80)).slice(0,40)};
const responsibilities=section([/responsibilit|what you.?ll do|role overview|key duties/i]);
const requirements=section([/requirements|qualifications|what you bring|must have|preferred qualification/i]);
const hardWall=/uas\/login|authwall|checkpoint|captcha|security verification/i.test(location.href+' '+body.slice(0,2500));
const signInPrompt=/\bsign in\b|join linkedin/i.test(body.slice(0,2500));
const unavailable=/job no longer available|no longer accepting applications|this job is no longer available|page not found|couldn.?t find this job|job has been removed/i.test(body);
const hasPostingEvidence=!!title&&!!pathId&&!!(description||criteriaText);
const visibleEvidence=!!clean(document.querySelector('h1')?.innerText)&&!!pathId&&!!(visibleDescription||criteriaText);
const loginOrChallenge=hardWall||(signInPrompt&&!hasPostingEvidence);
const live=visibleEvidence&&!loginOrChallenge&&!unavailable;
return {success:!loginOrChallenge,live,jobId:pathId,url,title:title||null,header:header||null,company:company||null,companyUrl,location:locationText||null,workMode:workMode||null,compensation:compensation||null,postedAt:posted,responsibilities,requirements,description,criteriaText,fullJd:[description,criteriaText].filter(Boolean).join('\n\n'),unavailable,signInPrompt,accessError:loginOrChallenge?'authentication-or-security-wall':(!visibleEvidence?'rendering-or-missing-content':null),verificationNote:unavailable?'LinkedIn visibly indicates that this posting is unavailable.':(live?'Posting page contains live-page evidence.':'Required posting content was not visible; rendering or availability could not be confirmed.')};
})()- Return
live:trueonly when the canonical job path, title, and description or criteria are visible and no login, challenge, or unavailable-posting indicator is present. A page that loads but lacks required posting content is a rendering or verification failure, not a confirmed live posting.
Possible Friction Points
- The durable direct route is
https://www.linkedin.com/jobs/view/{job-id}/; a supplied numeric ID can be used without a search round-trip. - LinkedIn often hides part or all of the JD behind
button[aria-label='Show more']; expand it before reading the description. - A dismissible consent, sign-in, or overlay dialog may obscure the page. Dismiss only visible
DismissorClosecontrols; never submit credentials. - The description may use
.show-more-less-html__markup,.description__text, orsection.description; preserve visible text rather than relying on one fragile class. - Job criteria and metadata can appear separately from the description. Return both
descriptionandcriteriaText, and combine them asfullJd. - Responsibilities and requirements are parsed from the raw node text, before whitespace collapsing. Collapsing first destroys the line breaks those headings sit on, and the sections then come back empty on a description that plainly contains them.
- A public job page can show "Join or sign in to find your next job" while the posting itself is readable. Treat a sign-in prompt as a wall only when the posting content is also missing;
uas/login,authwall,checkpoint, a CAPTCHA, or a security-verification page is a wall on its own. The returnedsignInPromptrecords the prompt either way. liveis decided on visible title and description or criteria, not on JSON-LD. A page can carryJobPostingmetadata and still fail to render the posting, and that is a rendering failure, not a live listing. JSON-LD stays as an extraction fallback for the returned fields.- The visible compensation must come from LinkedIn's pay block ("Base pay range" and similar) and must contain an actual amount. Taking the first short element that merely mentions pay returns a description bullet such as "Competitive salary and benefits" as the compensation. With no such block, fall through to JSON-LD
baseSalary. Currency is matched as\p{Sc}, so non-Latin currencies are read too. - A JSON-LD fallback is returned as currency, amount, and period together, such as
USD 50000–70000 YEAR. A bare50000–70000does not say whether it is dollars a year or euros an hour, which is the difference between an attractive offer and a poor one. - Compensation may be absent from the visible page. Prefer a visible compensation string, then JSON-LD
baseSalary; returnnullwhen unavailable and do not infer pay from title or location. - Work mode may appear in the location/header text or JSON-LD
jobLocationType; return the visible value and do not classify an unspecified listing. - Responsibilities and requirements are best-effort arrays derived from visible section text;
description,criteriaText, andfullJdremain authoritative when headings or list structure vary. - Relative posting times are returned exactly as visible; do not infer a precise timestamp when LinkedIn exposes only relative text.
- A login wall, CAPTCHA, checkpoint, consent gate, “job no longer available” notice, or missing required content is an explicit access or verification error, not a live posting or empty JD.
- Keep navigation and evaluation read-only and same-origin. Do not collect credentials, cookies, tokens, application data, or unrelated page content.