Job Posting Schema Framework

Use conditional-logic schema generation to keep Job Posting markup aligned with real page state and avoid stale or invalid static templates.

When To Use It

Google Search CentralJob Posting documentation

Use on the leaf page of a single open job posting that names the hiring company and where the employee works, or which countries a fully remote role accepts.

Key Implementation Documentation Highlights

  • Required properties: title, description, datePosted, hiringOrganization, jobLocation. For fully remote jobs, applicantLocationRequirements can replace jobLocation. Every jobLocation.address needs addressCountry.
  • Recommended properties: validThrough, baseSalary, employmentType, directApply, identifier, applicantLocationRequirements, jobLocationType, plus hiringOrganization.sameAs and hiringOrganization.logo. educationRequirements, experienceRequirements and experienceInPlaceOfEducation are beta.
  • description is the full job in HTML (responsibilities, qualifications, skills, hours, education, experience) with <p>, <ul>, <li> or <br> breaks, and can't be the same as title. title is the job title only: no codes, addresses, dates, salaries or company names.
  • datePosted and validThrough are ISO 8601. A past validThrough marks the job expired: expired jobs must lose active markup (past validThrough, 404/410, or remove JobPosting). Omit validThrough when the posting never expires.
  • Fully remote jobs: jobLocationType TELECOMMUTE plus at least one country through applicantLocationRequirements (preferred) or a jobLocation addressCountry, and the description must state the eligible locations.
  • employmentType: FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, PER_DIEM or OTHER (case-sensitive, multiple allowed). baseSalary: 3-letter ISO 4217 currency, a value or a minValue to maxValue range, unitText HOUR, DAY, WEEK, MONTH or YEAR. Only employers can provide baseSalary.
  • hiringOrganization is the company offering the job, not the hiring location; use "confidential" for anonymous hiring. A third-party logo needs a width to height ratio between 0.75 and 2.5.
  • One JobPosting per page, on the most detailed leaf page, never on job listing pages. Google recommends the Indexing API over sitemaps for job URLs. Available in parts of Asia, Europe, MENA, all of Latin America, North America and Sub-Saharan Africa.
  • Google expects content parity: every marked-up value must match what users can see on the page.
  • Validate with the Rich Results Test and URL Inspection, then monitor the related Search Console report for errors and warnings after deployment.

Template Approach

const pageData = {
  title: "Senior Technical SEO Analyst",
  descriptionHtml: "<p>Own technical SEO for a 40,000 page ecommerce catalog.</p>",
  datePosted: "2026-09-28",
  company: "Example Retail Group",
  city: "Waltham",
  region: "MA",
  postalCode: "02453"
};

const staticTemplate = {
  "@context": "https://schema.org",
  "@type": "JobPosting",
  "title": pageData.title,
  "description": pageData.descriptionHtml,
  "datePosted": pageData.datePosted,
  "employmentType": "FULL_TIME",
  "hiringOrganization": { "@type": "Organization", "name": pageData.company },
  "jobLocation": {
    "@type": "Place",
    "address": {
      "@type": "PostalAddress",
      "addressLocality": pageData.city,
      "addressRegion": pageData.region,
      "postalCode": pageData.postalCode,
      "addressCountry": "US"
    }
  }
};

Conditional-logic Framework

const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$/;

function isAbsoluteUrl(value) {
  return typeof value === "string" && /^https?:\/\/[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$/i.test(value);
}

// Returns the ISO 8601 string unchanged (timezone offset kept), or null if unparseable.
function toIsoDateTime(value) {
  if (typeof value !== "string") return null;
  const text = value.trim();
  const m = text.match(ISO_DATE_TIME);
  if (!m) return null;
  const [, y, mo, d, h = "00", mi = "00", s = "00"] = m;
  const date = new Date(Date.UTC(+y, +mo - 1, +d));
  if (date.getUTCMonth() !== +mo - 1 || date.getUTCDate() !== +d) return null;
  if (+h > 23 || +mi > 59 || +s > 59) return null;
  return text;
}

function toIsoDate(value) {
  const iso = toIsoDateTime(value);
  return iso ? iso.slice(0, 10) : null;
}

// Drops null, undefined, "", [] and {} recursively.
function compact(value) {
  if (Array.isArray(value)) {
    const items = value.map(compact).filter((v) => v !== undefined);
    return items.length ? items : undefined;
  }
  if (value && typeof value === "object") {
    const out = {};
    for (const [key, v] of Object.entries(value)) {
      const c = compact(v);
      if (c !== undefined) out[key] = c;
    }
    return Object.keys(out).length ? out : undefined;
  }
  return value === null || value === undefined || value === "" ? undefined : value;
}

function passesPageGates(source) {
  if (source.indexable === false) return false;
  if (source.canonicalUrl && source.url && source.canonicalUrl !== source.url) return false;
  return source.contentVisible !== false;
}


const EMPLOYMENT_TYPES = ["FULL_TIME", "PART_TIME", "CONTRACTOR", "TEMPORARY", "INTERN", "VOLUNTEER", "PER_DIEM", "OTHER"];
const SALARY_UNITS = ["HOUR", "DAY", "WEEK", "MONTH", "YEAR"];
const CREDENTIAL_CATEGORIES = ["high school", "associate degree", "bachelor degree", "professional certificate", "postgraduate degree"];

function toCountryCode(value) {
  const code = String(value || "").toUpperCase();
  return /^[A-Z]{2}$/.test(code) ? code : null;
}

function buildJobPlace(location) {
  const country = toCountryCode(location.country);
  if (!country) return null; // addressCountry is required on every jobLocation
  return {
    "@type": "Place",
    address: {
      "@type": "PostalAddress",
      streetAddress: location.street,
      addressLocality: location.locality,
      addressRegion: location.region,
      postalCode: location.postalCode,
      addressCountry: country
    }
  };
}

function buildBaseSalary(salary) {
  if (!salary) return null;
  const currency = String(salary.currency || "").toUpperCase();
  const unitText = String(salary.unit || "").toUpperCase();
  if (!/^[A-Z]{3}$/.test(currency) || !SALARY_UNITS.includes(unitText)) return null;
  const { value, min, max } = salary;
  if (value == null && (min == null || max == null || min > max)) return null; // a value, or a valid range
  return {
    "@type": "MonetaryAmount",
    currency,
    value: {
      "@type": "QuantitativeValue",
      value: value ?? null,
      minValue: value == null ? min : null,
      maxValue: value == null ? max : null,
      unitText
    }
  };
}

function buildJobPostingSchema(source, now = new Date()) {
  // (a) Page gates: indexable, self-canonical, a single job visible on this leaf page.
  if (!passesPageGates(source)) return null;

  // (b) Required: title, description (HTML, not the title), datePosted, hiringOrganization.
  const datePosted = toIsoDateTime(source.datePosted);
  const org = source.hiringOrganization || {};
  if (!source.title || !source.descriptionHtml || !datePosted || !org.name) return null;
  if (source.descriptionHtml.trim() === source.title.trim()) return null;

  // Expired postings must not keep active markup.
  const validThrough = toIsoDateTime(source.validThrough);
  if (validThrough && Date.parse(validThrough) < now.getTime()) return null;

  // Either-or: jobLocation, or applicantLocationRequirements for fully remote jobs.
  const jobLocation = (source.locations || []).map(buildJobPlace).filter(Boolean);
  const applicantCountries = (source.remoteEligibleCountries || []).map(toCountryCode).filter(Boolean);
  const isRemote = source.fullyRemote === true;
  if (isRemote && !applicantCountries.length && !jobLocation.length) return null; // TELECOMMUTE needs a country
  if (!isRemote && !jobLocation.length) return null;

  // (c) Required + recommended properties, only when the source has them.
  const schema = {
    "@context": "https://schema.org",
    "@type": "JobPosting",
    title: source.title,
    description: source.descriptionHtml,
    datePosted,
    validThrough,
    hiringOrganization: {
      "@type": "Organization",
      name: org.name,
      sameAs: isAbsoluteUrl(org.url) ? org.url : null,
      logo: isAbsoluteUrl(org.logo) ? org.logo : null
    },
    jobLocation,
    jobLocationType: isRemote ? "TELECOMMUTE" : null,
    applicantLocationRequirements: isRemote
      ? applicantCountries.map((code) => ({ "@type": "Country", name: code }))
      : null,
    // (d) Custom rules: enums, salary range, booleans.
    employmentType: (source.employmentTypes || []).filter((t) => EMPLOYMENT_TYPES.includes(t)),
    baseSalary: source.postedByEmployer ? buildBaseSalary(source.salary) : null, // only employers provide baseSalary
    directApply: typeof source.directApply === "boolean" ? source.directApply : null,
    identifier: source.requisitionId && {
      "@type": "PropertyValue",
      name: org.name,
      value: String(source.requisitionId)
    },
    educationRequirements: CREDENTIAL_CATEGORIES.includes(source.education) ? {
      "@type": "EducationalOccupationalCredential",
      credentialCategory: source.education
    } : null,
    experienceRequirements: Number.isInteger(source.monthsOfExperience) && source.monthsOfExperience >= 0 ? {
      "@type": "OccupationalExperienceRequirements",
      monthsOfExperience: source.monthsOfExperience
    } : null,
    experienceInPlaceOfEducation: typeof source.experienceInPlaceOfEducation === "boolean"
      ? source.experienceInPlaceOfEducation
      : null
  };

  // (e) Strip empty values.
  return compact(schema);
}

// Usage:
// const source = {
//   "url": "https://www.example.com/careers/senior-technical-seo-analyst",
//   "canonicalUrl": "https://www.example.com/careers/senior-technical-seo-analyst",
//   "indexable": true,
//   "contentVisible": true,
//   "title": "Senior Technical SEO Analyst",
//   "descriptionHtml": "<p>Own technical SEO for a 40,000 page ecommerce catalog.</p><ul><li>Audit crawling, rendering and indexing</li><li>Ship structured data with engineering</li></ul><p>Hybrid schedule, three days a week in our Waltham office.</p>",
//   "datePosted": "2026-09-28",
//   "validThrough": "2026-12-31T23:59:00-05:00",
//   "hiringOrganization": {
//     "name": "Example Retail Group",
//     "url": "https://www.example.com",
//     "logo": "https://www.example.com/images/logo-square.png"
//   },
//   "locations": [
//     {
//       "street": "50 Main Street",
//       "locality": "Waltham",
//       "region": "MA",
//       "postalCode": "02453",
//       "country": "US"
//     }
//   ],
//   "fullyRemote": false,
//   "employmentTypes": [
//     "FULL_TIME"
//   ],
//   "postedByEmployer": true,
//   "salary": {
//     "currency": "USD",
//     "min": 110000,
//     "max": 135000,
//     "unit": "YEAR"
//   },
//   "directApply": true,
//   "requisitionId": "SEO-2026-114",
//   "education": "bachelor degree",
//   "monthsOfExperience": 48,
//   "experienceInPlaceOfEducation": true
// };
// const schema = buildJobPostingSchema(source); // null when a gate or required property fails

The Output As An Entity Graph

The conditional-logic output above, parsed in your browser and drawn as the graph a crawler builds from it. Every nested object is a node and the property that holds it is the edge. Tap a node to inspect its properties, or drag to rearrange.

Why Conditional Logic Is Better Than Static Templates

  • Prevents emitting empty, null, or stale fields that frequently trigger rich result warnings.
  • Supports multiple page states (for example: no rating yet, no image yet, no offers yet) without duplicate templates.
  • Lets engineering teams centralize schema policy checks and validation in reusable code paths.
  • Makes large-scale schema maintenance safer when content models evolve over time.

Property Reference

What Google's Job posting documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.

Required, one of: Include jobLocation, or applicantLocationRequirements for fully remote jobs.

Job posting properties in Google's documentation
PropertyGoogleExpectsNotes
datePostedRequiredDate or DateTimeThe original date that employer posted the job in ISO 8601 format.
descriptionRequiredTextThe full description of the job in HTML format.
hiringOrganizationRequiredOrganizationThe organization offering the job position.
hiringOrganization.nameRecommendedTextThe name of the hiring company.
hiringOrganization.sameAsRecommendedURLThe company's URL.
hiringOrganization.logoRecommendedURL or ImageObjectThe hiring organization's logo; width/height ratio must be between 0.75 and 2.5.
jobLocationRequiredPlace, can repeatThe physical location(s) of the business where the employee will report to work.
jobLocation.addressRequiredPostalAddressThe PostalAddress of the work location.
titleRequiredTextThe title of the job (not the title of the posting)
applicantLocationRequirementsOne ofAdministrativeArea, Country or State, can repeatThe geographic location(s) in which employees may be located to be eligible for the work from home job.
applicantLocationRequirements.nameRecommendedTextThe name of the country or state.
baseSalaryRecommendedMonetaryAmountThe actual base salary for the job, as provided by the employer.
baseSalary.currencyRecommendedTextThe ISO 4217 currency code.
baseSalary.valueRecommendedQuantitativeValueThe salary amount or range.
directApplyRecommendedBooleanWhether the URL associated with this job posting enables direct application for the job.
employmentTypeRecommendedText, can repeatType of employment: FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, PER_DIEM or OTHER. One of: FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN, VOLUNTEER, and 2 more.
identifierRecommendedPropertyValueThe hiring organization's unique identifier for the job.
identifier.nameRecommendedTextThe hiring organization name.
identifier.valueRecommendedTextThe unique job identifier.
jobLocationTypeRecommendedTextSet to TELECOMMUTE for jobs in which the employee may or must work remotely 100% of the time. One of: TELECOMMUTE.
validThroughRecommendedDateTime or DateThe date when the job posting will expire in ISO 8601 format.
educationRequirementsRecommendedEducationalOccupationalCredential or Text, can repeatThe education required for the position.
educationRequirements.credentialCategoryRecommendedTextOne of high school, associate degree, bachelor degree, professional certificate, postgraduate degree.
experienceRequirementsRecommendedOccupationalExperienceRequirements or TextThe experience required for the position.
experienceRequirements.monthsOfExperienceRecommendedNumberThe minimum number of months of experience required.
experienceInPlaceOfEducationRecommendedBooleanWhether the job posting will accept experience in place of its formal educational qualifications.

Explore Other Schema Framework Pages

Retired Learning Video Use for educational videos where the learning objective and pedagogical structure are explicit. LearningResource Open Local Business Use for physical businesses serving local customers with publicly verifiable contact and location details. LocalBusiness Open Loyalty Program Use to publish a loyalty program and its tiers once at the organization level, so member prices and member shipping can reference them. MemberProgram Open Math Solvers Use for pages that solve mathematical expressions and present step-by-step computational guidance. MathSolver Open Merchant Listing Use on pages where a product can be bought, with price, availability, shipping and return details. Product Open Limited Movie Use for movie detail pages with canonical film information and transparent metadata attribution. Movie Open Organization Use for official organization pages to clarify brand identity and authoritative entity signals. Organization Open Retired Practice Problems Use for educational practice content that presents solvable problems and answer validation structure. Quiz Open Product Snippet Use for product pages where pricing, availability, and product identity are visible to users. Product Open Product Variants Use when one product comes in variants such as size or color, grouped with ProductGroup. ProductGroup Open Profile Page Use for creator or contributor profile pages where identity attributes and ownership are explicit. ProfilePage Open Q&A Use for pages where users submit questions and community or editorial answers are displayed. QAPage Open Recipe Use for recipe pages with complete ingredients, instructions, and preparation details. Recipe Open Return Policy Use to publish a merchant return policy once at the organization level, with offer-level overrides. MerchantReturnPolicy Open Review Snippet Use when a page publishes genuine reviews tied to a clearly identified product, service, or entity. Review Open Shipping Policy Use to publish shipping rates, delivery times and destinations once at the organization level. ShippingService Open Site Name Use on the home page so Google shows the preferred site name in search results. WebSite Open Retired Sitelinks Search Box Use on sites with a functioning internal search endpoint that can accept URL query parameters. WebSite Open Software App (Beta) Use for software product pages that describe platform support and application category clearly. SoftwareApplication Open Limited Speakable Use for news-style pages where specific text segments are appropriate for text-to-speech playback. SpeakableSpecification Open Subscription and Paywalled Content Use for pages with restricted sections so Google can distinguish free previews from protected content. CreativeWork Open Limited Vacation Rental Use for rental property detail pages where booking-relevant amenities and location signals are explicit. VacationRental Open Retired Vehicle Listing Use for inventory pages selling vehicles with standardized listing fields and offer information. Car Open Video Use for pages where a primary video is embedded and key media metadata is visible on-page. VideoObject Open Article Use for editorial pages where the visible content is a complete article with a clear byline and publication timestamps. Article Open Limited Book Actions Use when your page helps users read, preview, or purchase books through clearly described actions. Book Open Breadcrumb Use on pages that belong to a clear hierarchy and display navigational trail links users can follow. BreadcrumbList Open Carousel Use when a page lists multiple related entities in a scannable sequence that can be surfaced as cards. ItemList Open Course Use for educational offerings where curriculum details, provider identity, and delivery context are explicit. Course Open Dataset Use for dataset landing pages so datasets are discoverable in Google Dataset Search with license, creator and download details. Dataset Open Discussion Forum Use for forum and community threads where a primary post and subsequent replies are publicly visible. DiscussionForumPosting Open Limited Education Q&A Use for educational question-answer pages where a single canonical answer and supporting context are visible. Quiz Open Employer Aggregate Rating Use on pages that publish employer rating aggregates from a clear and policy-compliant review source. EmployerAggregateRating Open Retired Estimated Salary Use for compensation insight pages that present transparent salary ranges for specific occupations and locations. Occupation Open Event Use for upcoming events with a real-world or virtual occurrence, schedule, and location context. Event Open Retired Fact Check Use for fact-checking pages that evaluate a specific claim with a transparent rating methodology. ClaimReview Open Retired Home Activities Use for virtual classes and activities where participation details and timing are clearly listed. Event Open Retired How-To Use for instructional content that teaches a process through ordered steps and actionable detail. HowTo Open Image Metadata Use on image pages and image hosts that expose ownership and licensing information to search engines. ImageObject Open

Implement Job Posting Schema Frameworks Correctly

Get implementation support for conditional schema architecture, validation, and deployment governance.