Course and SoftwareApplication Schema Done Right

No Comments

Google retired the dedicated course rich result for individual pages in 2023, but Course structured data still powers the Course List carousel, feeds Google's understanding of educational entities, and remains a ranking-adjacent signal. SoftwareApplication markup is alive and well, driving star ratings and price snippets in the SERP. Both schemas reward precision and punish vagueness, so this is a field-by-field walkthrough of getting each one production-ready.

Course schema: the required spine

A valid Course needs only three properties, but a useful one needs more. Start with the spine:

  • name, the course title, not the page's marketing headline. "Introduction to Python Programming," not "Learn to Code in 30 Days!"
  • description, 60 characters minimum. Describe what the learner does and achieves, not why your platform is great.
  • provider, an Organization with name and ideally url and sameAs. This is the entity Google attaches the course to, so be consistent across every course you publish.

That trio passes the validator. It does not earn carousel eligibility, which requires the hasCourseInstance block below.

hasCourseInstance: where eligibility actually lives

To qualify for the Course List carousel, each Course must contain at least one hasCourseInstance of type CourseInstance. This is the single most-skipped requirement. The instance carries the delivery details:

  • courseMode, use the controlled values "Online", "Onsite", or "Blended". Free text here is ignored.
  • courseWorkload, an ISO 8601 duration like PT22H (22 hours) or P3W (3 weeks). This is required for carousel eligibility alongside courseMode.
  • location, a Place with a PostalAddress for onsite, or a VirtualLocation with a url for online instances.
  • courseSchedule (or legacy startDate/endDate), for scheduled cohorts. Omit for self-paced and rely on courseWorkload instead.
"hasCourseInstance": {
 "@type": "CourseInstance",
 "courseMode": "Online",
 "courseWorkload": "PT22H",
 "location": {
 "@type": "VirtualLocation",
 "url": "https://example.com/python-course"
 }
}

Course offers and ratings: the rich-treatment unlock

Two optional blocks separate a bare-bones course entry from one that earns visual treatment. Add offers to surface price, and aggregateRating to surface stars.

  • offers takes price (a number, no currency symbol inside the string), priceCurrency (ISO 4217, e.g. "USD"), and category. For free courses set "price": "0" and "category": "Free"; for paid, use "Paid" or "Subscription".
  • aggregateRating needs ratingValue, ratingCount (or reviewCount), plus bestRating and worstRating if your scale isn't the default 1, 5.

The hard rule on ratings: they must reflect genuine, user-visible reviews on the same page. Injecting an aggregateRating that doesn't appear in the rendered HTML is a manual-action risk, not a clever shortcut.

"offers": {
 "@type": "Offer",
 "category": "Paid",
 "price": "49.00",
 "priceCurrency": "USD"
},
"aggregateRating": {
 "@type": "AggregateRating",
 "ratingValue": "4.7",
 "ratingCount": "832"
}

SoftwareApplication: the required quartet

SoftwareApplication still produces a live rich result, and Google enforces a stricter required set than most people expect. To be eligible you need name plus one of two combinations: either aggregateRating, or offers. Without at least one of those, the markup is valid schema.org but ineligible for any rich treatment. The fields:

  • name, the application's name.
  • applicationCategory, use a recognized value such as "GameApplication", "BusinessApplication", "DeveloperApplication", or "MobileApplication". This drives how the app is classified.
  • operatingSystem, free text is accepted ("Windows 11, macOS 12+") but keep it specific.
  • offers / aggregateRating, at least one is mandatory for eligibility, same property rules as Course above.

SoftwareApplication offers and pricing nuance

The offers block behaves the same as elsewhere, with one critical detail for free software: Google requires "price": "0" with a valid priceCurrency rather than omitting the offer. Omitting price on a free app is the most common cause of "Either offers, review, or aggregateRating should be specified" warnings.

{
 "@context": "https://schema.org",
 "@type": "SoftwareApplication",
 "name": "TaskFlow",
 "applicationCategory": "BusinessApplication",
 "operatingSystem": "Web, iOS, Android",
 "offers": {
 "@type": "Offer",
 "price": "0",
 "priceCurrency": "USD"
 },
 "aggregateRating": {
 "@type": "AggregateRating",
 "ratingValue": "4.5",
 "ratingCount": "2104"
 }
}

For multi-tier pricing, use an AggregateOffer with lowPrice, highPrice, offerCount, and priceCurrency instead of a single Offer. This is the correct pattern for SaaS with several plans.

Connecting the two: course-as-product

When you sell a course that is software (an interactive coding platform, say), don't merge the types into one node. Publish a Course node and a SoftwareApplication node in the same JSON-LD graph, linked with @id references. Each remains independently valid, and Google reads the relationship instead of you forcing one type to do two jobs.

Common mistakes

  • Course without hasCourseInstance. Validates clean, never reaches the carousel. Always include the instance with courseMode and courseWorkload.
  • Currency symbols inside price. Use "49.00" and a separate priceCurrency, never "$49".
  • Ratings not on the page. Every aggregateRating must correspond to reviews visibly rendered to users.
  • Wrong duration format. courseWorkload must be ISO 8601 (PT22H), not "22 hours" or "3 weeks."
  • Free apps with no offers. Add "price": "0" rather than dropping the offer entirely.
  • Inconsistent provider. Vary the organization name across courses and Google fails to consolidate them into one educational entity.

Validate before you ship

  1. Run every template through the Schema.org validator for syntax, then Google's Rich Results Test for eligibility, they catch different things.
  2. Confirm the Rich Results Test reports the expected feature (Course or Software App) and zero errors; treat warnings as to-do items, not noise.
  3. Render-test with the URL Inspection tool in Search Console, since JavaScript-injected JSON-LD can fail if Google's render budget times out.
  4. Monitor the Enhancements reports in Search Console after deployment for valid-item counts and any new error spikes.

Get the required spine right, add the instance and offer/rating blocks that unlock treatment, keep your entities consistent, and never markup a rating a user can't see. That discipline is the entire difference between schema that validates and schema that earns placement.

Want this handled properly on your site?

It is exactly the kind of work an advanced technical SEO audit covers. See how an advanced SEO audit works →

Claude Vincent is a technical SEO consultant focused on crawlability, rendering, and AI-search visibility. He writes the field guides and case studies at SEO ProCheck, with a bias toward the durable, unglamorous work that decides whether search engines and AI answer engines can actually read and cite a site.

    About SEO ProCheck

    Technical SEO consulting and GEO strategy with 20 years of enterprise experience. Case studies, resources, and tools for search and AI visibility.

    Work With Me

    Technical SEO audits, GEO strategy, site migrations, and international SEO. Hourly consulting for teams who need hands-on support, not just reports.

    Subscribe to our newsletter!

    More from our blog