Technical

JobPosting Schema for AI Citation: The Complete 2026 Implementation Guide

JobPosting schema is the Schema.org markup that tells a crawler a role is genuinely open right now, who is hiring, where, and for how much. AI job search agents lean on it even harder than Google for Jobs ever did, because an AI answer that recommends an expired or fabricated listing is a trust failure the model cannot recover from mid-conversation. Here is the correct JSON-LD, the expiry trap that quietly disqualifies most careers pages, and where the effort pays off.

Neil Walsh·August 2026·8 min read

JobPosting schema is the Schema.org markup type that structures a single job opening into machine-readable fields: the role title, the hiring organisation, the location, the salary range, and the dates the posting is genuinely open. Where a human reads a careers page and infers all of this from layout and phrasing, an AI system reads only what the JSON-LD explicitly states, and it treats an undated, unstructured job listing very differently from one that states, in structured form, exactly when it was posted and when it closes.

JobPosting schema has existed since Google for Jobs launched it in 2017, but its stakes have changed. A search engine misreading a stale listing wastes a click. An AI job search agent that recommends a closed role, or invents a salary figure because none was stated, fails in the middle of a conversation the user is actively relying on, which is a much sharper penalty than a lost click, and it is exactly the kind of failure that makes an answer engine stop trusting a source at all.

Why AI job search agents depend on structure more than prose

Job listings are one of the few content types where the underlying facts change on a fixed schedule: a role opens, stays live for weeks, then closes, whether or not the page itself is ever edited or removed. Prose alone cannot carry that lifecycle reliably. A page that still says "we are hiring" six months after the role was filled is not lying deliberately, it is simply a static document describing a fact that has since expired, and nothing in the visible text tells a retrieval system the difference between a listing from yesterday and one from last spring.

ChatGPT, Google AI Mode, Perplexity, and the newer agentic job search tools built into LinkedIn and Indeed all answer questions like "is [company] hiring for [role] right now" by trying to establish current state, not just topical relevance. JobPosting schema is the only reliable way to give them that state directly, through the datePosted and validThrough properties, rather than forcing the model to guess from page-edit timestamps or infer freshness from unrelated cues.

  • ChatGPT and Google AI Mode job search flows preferentially surface roles with explicit validThrough dates still in the future, and treat postings with no expiry date as inherently less trustworthy for a "currently hiring" query
  • Perplexity and other citation-heavy engines quote baseSalary, employmentType, and jobLocation directly when they are present as structured fields, rather than paraphrasing an approximate range mentioned somewhere in the body copy
  • Agentic job search tools that can apply on a user's behalf rely on directApply and applicantLocationRequirements to decide whether a role is even eligible to act on, independent of anything the visible page says
  • Every major engine treats an expired or removed JobPosting with no corresponding schema update as a negative freshness signal for the entire careers section, not just the one stale listing

The required and recommended JobPosting properties

Google's job posting guidelines define a strict required set, and AI citation raises the bar further with a recommended set that meaningfully improves how much of a listing an engine can quote directly instead of paraphrasing.

Required properties

  • title: the exact job title as it appears on the page, not an internal requisition code
  • description: the full job description in HTML or plain text, matching what a human visitor sees
  • datePosted: the ISO 8601 date the listing first went live
  • hiringOrganization: an Organization object with at minimum a name and url
  • jobLocation: a Place object with a full PostalAddress, or applicantLocationRequirements for remote-only roles
  • employmentType: FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, PER_DIEM, or OTHER

Recommended properties for AI citation depth

  • validThrough: the closing date; omitting this is the single most common reason a JobPosting reads as stale to an AI engine, covered in detail below
  • baseSalary: a MonetaryAmount with a currency and either a fixed value or a minValue and maxValue range, which is what lets an engine quote a figure at all instead of saying "salary not specified"
  • directApply: a boolean indicating whether a candidate can apply on the page itself without being redirected through several intermediate steps
  • identifier: a PropertyValue linking back to the internal requisition ID, useful for deduplicating the same role posted across a careers page and third-party job boards
  • workHours and jobLocationType (TELECOMMUTE for remote roles): clarifies scheduling and remote eligibility as discrete facts rather than something buried in a paragraph

The correct JSON-LD structure

A minimal but complete JobPosting block includes the full required set plus validThrough and baseSalary, since those two properties carry most of the AI citation value beyond what Google for Jobs alone requires.

JSON-LD
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "JobPosting",
  "title": "Senior Backend Engineer",
  "description": "<p>We are looking for a senior backend engineer to own our ingestion pipeline...</p>",
  "identifier": {
    "@type": "PropertyValue",
    "name": "CiteRank",
    "value": "REQ-2026-014"
  },
  "datePosted": "2026-08-01",
  "validThrough": "2026-09-30T23:59:00+00:00",
  "employmentType": "FULL_TIME",
  "hiringOrganization": {
    "@type": "Organization",
    "name": "CiteRank",
    "sameAs": "https://citerank.dev",
    "logo": "https://citerank.dev/logo.png"
  },
  "jobLocation": {
    "@type": "Place",
    "address": {
      "@type": "PostalAddress",
      "addressLocality": "Remote",
      "addressCountry": "GB"
    }
  },
  "jobLocationType": "TELECOMMUTE",
  "applicantLocationRequirements": {
    "@type": "Country",
    "name": "United Kingdom"
  },
  "baseSalary": {
    "@type": "MonetaryAmount",
    "currency": "GBP",
    "value": {
      "@type": "QuantitativeValue",
      "minValue": 70000,
      "maxValue": 95000,
      "unitText": "YEAR"
    }
  },
  "directApply": true
}
</script>

Implementation in Next.js (App Router)

Generate the schema from the same record that renders the visible listing, the same principle that applies to every other schema type. A careers page backed by an applicant tracking system should read validThrough directly from the ATS record and let the listing disappear from both the page and the schema the moment the role closes, rather than maintaining two separate sources of truth that inevitably drift apart.

tsx
interface JobRecord {
  title: string;
  description: string;
  requisitionId: string;
  datePosted: string;
  validThrough: string;
  employmentType: string;
  locality: string;
  remote: boolean;
  salaryMin: number;
  salaryMax: number;
  currency: string;
}

function JobPostingSchema({ job }: { job: JobRecord }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'JobPosting',
    title: job.title,
    description: job.description,
    identifier: { '@type': 'PropertyValue', name: 'CiteRank', value: job.requisitionId },
    datePosted: job.datePosted,
    validThrough: job.validThrough,
    employmentType: job.employmentType,
    hiringOrganization: {
      '@type': 'Organization',
      name: 'CiteRank',
      sameAs: 'https://citerank.dev',
    },
    jobLocation: {
      '@type': 'Place',
      address: { '@type': 'PostalAddress', addressLocality: job.locality, addressCountry: 'GB' },
    },
    ...(job.remote ? { jobLocationType: 'TELECOMMUTE' } : {}),
    baseSalary: {
      '@type': 'MonetaryAmount',
      currency: job.currency,
      value: { '@type': 'QuantitativeValue', minValue: job.salaryMin, maxValue: job.salaryMax, unitText: 'YEAR' },
    },
    directApply: true,
  };
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

The expiry problem: why stale JobPosting schema actively hurts citation

Most careers pages implement JobPosting schema once, at launch, and never revisit it. The result is a slow accumulation of listings whose validThrough date has quietly passed while the page, and the schema, keep insisting the role is open. This is worse for AI citation than having no schema at all, because a fabricated or hallucinated recommendation that traces back to your domain is exactly the failure mode that makes an AI engine downgrade a source across every future query, not just the one bad listing.

The fix has two parts, and both need to happen automatically rather than as a manual task someone eventually gets to. First, remove or noindex a listing's page the moment the role closes, rather than leaving it live with stale copy. Second, drop the JobPosting schema block entirely, or update validThrough retroactively, so the structured data cannot outlive the page content it describes. Google explicitly recommends returning a 404 or 410 status for expired job pages rather than a soft "this role has closed" banner, and the same logic protects AI citation trust.

Never let validThrough silently pass without the corresponding page and schema being removed or updated. An AI agent that recommends a role based on a validThrough date that already elapsed is not making a minor error, it is presenting false information to a user who may act on it, and that is the single fastest way to get a domain's job listings deprioritised or excluded from an engine's job search feature entirely.

Where JobPosting schema matters most

The return on implementing this schema correctly scales with how much hiring activity a site actually has, and with how directly AI-driven job search traffic already competes for the same candidates.

  • Company careers pages with more than a handful of open roles, where an ATS-driven feed makes automated schema generation and expiry handling far more valuable than a one-off manual implementation
  • Recruiting agencies and staffing firms, where dozens of client roles rotate constantly and stale listings compound quickly across a large catalogue
  • Job board and aggregator sites, where JobPosting schema is close to a genuine requirement, since Google and most AI job search integrations will not surface listings without it
  • Remote-first companies, where applicantLocationRequirements and jobLocationType are the only reliable way to signal eligibility to an AI agent filtering by geography on a candidate's behalf

Validating your JobPosting schema

Structural validity and factual accuracy need separate checks, since a listing can pass every automated validator while still describing a role that closed weeks ago.

  • Google Rich Results Test (search.google.com/test/rich-results): confirms JobPosting eligibility and flags missing required fields immediately
  • Search Console's Job Postings report: surfaces indexing errors and, critically, listings Google has already detected as expired based on validThrough
  • Schema.org validator (validator.schema.org): checks specification compliance for optional properties like baseSalary and identifier that Google's tool does not always flag
  • A scheduled audit against the ATS: confirm every JobPosting still present in the schema corresponds to a role still genuinely open in the source system, not just a page nobody has taken down
  • CiteRank audit: checks JobPosting schema presence, required fields, and validThrough freshness alongside every other AEO signal in a single pass

If your careers pages are hosted entirely on a third-party ATS such as Greenhouse, Lever, or Workday, check whether that platform emits JobPosting schema by default before building your own. Several major ATS providers already generate compliant markup automatically, and duplicating it on a wrapper page you control usually creates conflicting schema rather than adding coverage.

Frequently asked questions

Does JobPosting schema help with ChatGPT and AI Overviews, or only Google for Jobs?

Both. Google for Jobs was the original consumer of the schema, but ChatGPT, Google AI Mode, Perplexity, and the job search features built into LinkedIn and Indeed all parse the same JSON-LD to establish whether a role is currently open, what it pays, and where it is located. A well-formed JobPosting block is now read by considerably more systems than the one it was originally built for.

What happens if I do not set validThrough?

Google treats a missing validThrough as a signal to estimate an expiry itself, typically around 30 days after datePosted, and can stop showing the listing once that estimate passes even if the role is still genuinely open. AI answer engines behave similarly or more conservatively, often treating an undated listing as inherently less trustworthy for a "currently hiring" query than one with an explicit, still-future validThrough date.

Can I use JobPosting schema on a third-party ATS-hosted page such as Greenhouse or Lever?

Yes, and in most cases you should check whether it is already there before adding your own. Major applicant tracking systems increasingly emit compliant JobPosting schema automatically on their hosted job pages. If you embed or link to an ATS-hosted listing from your own careers page, avoid duplicating a conflicting schema block on your side; instead confirm the canonical version, wherever it lives, is complete and current.

Does stating a salary range in baseSalary affect AI citation eligibility?

It substantially improves it. Roles with an explicit baseSalary give an AI engine a discrete, quotable fact rather than forcing it to omit pay information or paraphrase an approximate figure from body copy. Several jurisdictions also now legally require salary disclosure on job postings, which makes baseSalary schema a compliance matter as much as an AEO one in those regions.

How often should JobPosting schema be refreshed?

Ideally on every state change in the source system, not on a fixed calendar. The moment a role closes, fills, or its details change, the schema and the page it lives on should update or be removed in the same action. A weekly automated audit that cross-checks every published JobPosting against the current ATS record catches anything a broken integration missed.

What is the difference between JobPosting and Occupation schema?

JobPosting describes one specific, time-bound opening at one organisation. Occupation describes a general career category, its typical qualifications, salary range, and required experience, independent of any single employer or open role. A careers hub page comparing what a "data analyst" role generally involves would use Occupation; the actual open req for a data analyst position would use JobPosting.

Do remote jobs need different markup than on-site roles?

Yes. Set jobLocationType to TELECOMMUTE and use applicantLocationRequirements to state which countries or regions are eligible to apply, rather than leaving jobLocation to imply a physical office that does not reflect where the work is actually performed. Omitting applicantLocationRequirements on a remote role is a common cause of an AI agent filtering it out entirely when a candidate specifies their location.

Should I keep old, filled JobPosting pages live for SEO value instead of removing them?

No. Google explicitly recommends a 404 or 410 response, or a redirect to a current openings page, once a role closes, rather than leaving a stale listing live with a soft closed-role banner. The same applies to AI citation: a listing an engine can still fetch and read as apparently current, weeks after it closed, is a trust liability, not an SEO asset worth preserving.

Free tool

See your AEO score in seconds

Paste your URL and get a full audit across all 9 AEO signals - schema, crawlers, E-E-A-T, and more.

Audit my site - it's free

Related reading

Technical

Why Schema.org markup is the single biggest lever for AI citation

May 2026
Technical

Is your robots.txt accidentally blocking ChatGPT and Claude?

May 2026