Technical SEO

Event Schema: JSON-LD for Webinars, Courses and Live Sessions

· · 18 min read

Event schema is schema.org Event structured data: JSON-LD that tells search engines and AI systems what a session is called, when it starts, where it happens and how to book it. For education teams it covers in-person workshops, webinars, hybrid classes and cohort start dates, but since June 2025 Google’s event experience only accepts the ones held at a physical venue.

That last point changes how you should mark up a webinar. This article covers what Google still shows for events in 2026, which schema.org type fits each kind of live learning session, copy-paste JSON-LD for six real scenarios, the date and status rules that break most implementations, and how event markup pairs with course markup.

Key takeaways

  • Google’s event documentation now says that “virtual experiences that have no real-world component aren’t supported” and that events must take place in a physical location. An online-only webinar cannot earn the event rich result.
  • Google removed the online event properties from that documentation on 5 June 2025. VirtualLocation and eventAttendanceMode are still valid schema.org vocabulary; Google simply no longer documents them for its feature.
  • Use EducationEvent for workshops, masterclasses and live lessons, CourseInstance (a schema.org subtype of Event) for a cohort of a course, and VideoObject with BroadcastEvent for a livestream you want Google to badge as LIVE.
  • Most broken event markup is a date problem: a missing UTC offset, a fake midnight start, or a past event still marked as scheduled.
  • Mark up one event per URL and generate the JSON-LD from the same record that renders the page, so the schedule, the price and the markup cannot drift apart.

What event schema is

In schema.org’s vocabulary an Event is “an event happening at a certain time and location, such as a concert, lecture, or festival”, and repeated events “may be structured as separate Event objects”. schema.org defines more specific types beneath it. For learning businesses, three matter:

TypeWhat it describesWhere it fits
EducationEventA subtype of Event for education events. It adds teaches, assesses and educationalLevel.Workshops, masterclasses, open days, live lessons, webinars
CourseInstance”An instance of a Course” offered at a different time, place, mode or to a different group of students. It is a subclass of Event.A cohort of a course with its own start date
BroadcastEventA live broadcast of a video, nested in VideoObject as publication.A livestream you want marked LIVE in Google

EducationEvent is the type most teams should reach for. Its own properties are small: teaches (“the competency or learning outcome”), assesses and educationalLevel (for example “beginner”, “intermediate” or “advanced”). Everything else, dates, location, offers, organiser, comes from Event.

Because EducationEvent is a subtype of Event, it still qualifies for Google’s feature. Google’s technical guideline is that the page “must contain structured data items from event types on schema.org”, and a subtype is an event type.

What Google still shows for events in 2026

Event is still a supported feature. Google’s structured data gallery lists it as “an interactive rich result that shows a list of organized events, such as concerts or art festivals, that people may attend at a particular time and place.”

Online-only events are out

On 5 June 2025 Google updated its guidance for event structured data. The Search Central changelog records that it added more examples of eligible and ineligible events, “also removed the online event properties”, and gives the reason: “To be eligible for the event experience on Google, events must be bookable by the general public and held at a physical location.”

Stat card: on 5 June 2025 Google removed the online event properties from its Event structured data documentation

The current event documentation (last updated 8 September 2026 when I read it) makes the rule explicit in its content guidelines: “Virtual experiences that have no real-world component aren’t supported. Events must take place in a physical location.” Its required location property now expects a Place with name and a full address, and the page no longer mentions VirtualLocation or eventAttendanceMode at all.

This reverses the 17 March 2020 change that added eventAttendanceMode, VirtualLocation and location.url to the same page. Webinar markup shipped on the strength of that guidance no longer earns anything in Google’s event experience.

The other eligibility rules education teams trip over

The content guidelines on the event documentation page rule out several learning formats:

  • Closed sessions. “Events must be bookable to the general public. Events that require a membership, or invitation prior to purchasing the ticket or attending the event are ineligible.” Alumni-only masterclasses and enrolled-student workshops do not qualify.
  • Promotions dressed as events. Google says not to mark up “short-term discounts or purchase opportunities” or business hours as events.
  • Listing pages. “Each event MUST have a unique URL (a leaf page) and markup on that URL”, and the experience “only supports pages that focus on a single event.”

The general structured data guidelines add a relevance example that reads as if it were written for webinars: “A sports live streaming site labeling broadcasts as local events” is given as irrelevant data.

The experience is also regional. Google lists nine regions: Australia, Brazil, Canada, Germany, India, Latin America, Spain, the United Kingdom and the United States. A valid workshop in Paris will not show in it.

What that means for each format

FormatGoogle event experienceStill worth marking up?
In-person workshop, public ticketsEligibleYes, this is the case the feature is built for
Hybrid class with a venueNot excluded, because it has a venueYes, describe both locations
Online-only webinarNot eligibleYes, for other consumers, but expect no event rich result
Livestream on your own watch pageNot via Event; LIVE badge via BroadcastEventYes, as video markup
Cohort start dateNot via EventYes, as CourseInstance inside Course
Members-only or invite-only sessionNot eligibleOnly if the page is public and the markup is accurate

“Still worth marking up” is my judgement, not a Google promise: other search engines, calendar tools and AI systems can read the same markup. What you should not do is give an online webinar your office address to force eligibility. That is the misleading markup the guidelines warn against.

Google’s event documentation has three required properties and a longer recommended list:

PropertyStatusNotes
nameRequiredThe full title. No prices, URLs or “book now” text in it
startDateRequiredISO 8601 date and time, with a UTC offset where you know it
locationRequiredA Place with name and address (PostalAddress)
endDateRecommendedSame format as startDate
eventStatusRecommendedDefaults to EventScheduled if omitted
descriptionRecommendedThe event itself, not your site’s features
imageRecommended1920px wide recommended, 720px minimum; 16x9, 4x3 and 1x1
offersRecommendedOne Offer per ticket type: price, priceCurrency, availability, url, validFrom
organizerRecommendedOrganization or Person with name and url
performerRecommendedPerson or PerformingGroup. For education, your speaker or tutor
previousStartDateRecommendedOnly with eventStatus set to EventRescheduled

Two details are easy to miss. If a session is free, Google says to set price to 0. And offers.url must go to a page that “clearly and predominantly provides the opportunity to buy a ticket” for that specific event, and must not be blocked by robots.txt.

JSON-LD examples for six learning formats

Each block below goes inside a <script type="application/ld+json"> tag in the server-rendered HTML of the event’s own page. All six parse as valid JSON. Replace every example.com value with your own.

1. In-person workshop (eligible for Google’s event experience)

{
  "@context": "https://schema.org",
  "@type": "EducationEvent",
  "name": "Structured Data Workshop for Course Teams",
  "description": "A hands-on half-day workshop on marking up course and event pages, with laptops and real templates.",
  "startDate": "2026-11-18T09:30:00+00:00",
  "endDate": "2026-11-18T13:00:00+00:00",
  "eventStatus": "https://schema.org/EventScheduled",
  "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
  "location": {
    "@type": "Place",
    "name": "Example Learning Centre",
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "1 Example Street",
      "addressLocality": "London",
      "postalCode": "EC1A 1AA",
      "addressCountry": "GB"
    }
  },
  "image": [
    "https://example.com/img/workshop-1x1.jpg",
    "https://example.com/img/workshop-4x3.jpg",
    "https://example.com/img/workshop-16x9.jpg"
  ],
  "teaches": "Writing and validating JSON-LD for course and event pages",
  "educationalLevel": "intermediate",
  "offers": {
    "@type": "Offer",
    "url": "https://example.com/events/structured-data-workshop/tickets/",
    "price": 95,
    "priceCurrency": "GBP",
    "availability": "https://schema.org/InStock",
    "validFrom": "2026-09-01T09:00:00+01:00"
  },
  "performer": {
    "@type": "Person",
    "name": "Jane Example"
  },
  "organizer": {
    "@type": "Organization",
    "name": "Example Academy",
    "url": "https://example.com/"
  }
}

Google no longer documents eventAttendanceMode. I keep it because it makes the online and hybrid variants a one-line change.

2. Online-only webinar

The markup below is valid and accurate, but under Google’s current guidelines it will not appear in the event experience.

{
  "@context": "https://schema.org",
  "@type": "EducationEvent",
  "name": "Webinar: Choosing an LMS for a Multilingual Course Catalogue",
  "description": "A 45-minute live webinar with a 15-minute Q&A on LMS selection for course catalogues in several languages.",
  "startDate": "2027-02-10T17:00:00+00:00",
  "endDate": "2027-02-10T18:00:00+00:00",
  "eventStatus": "https://schema.org/EventScheduled",
  "eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
  "location": {
    "@type": "VirtualLocation",
    "url": "https://example.com/webinars/lms-multilingual/join/"
  },
  "image": "https://example.com/img/lms-webinar-16x9.jpg",
  "isAccessibleForFree": true,
  "offers": {
    "@type": "Offer",
    "url": "https://example.com/webinars/lms-multilingual/",
    "price": 0,
    "priceCurrency": "GBP",
    "availability": "https://schema.org/InStock"
  },
  "performer": {
    "@type": "Person",
    "name": "Jane Example"
  },
  "organizer": {
    "@type": "Organization",
    "name": "Example Academy",
    "url": "https://example.com/"
  }
}

The pattern follows schema.org’s own VirtualLocation example, which pairs OnlineEventAttendanceMode with a VirtualLocation that carries only a url. Point that url at the registration page, not a private meeting link, and always give the UTC offset: with no address, nothing else says which time zone “17:00” is in.

3. Hybrid session

Put both locations in an array and set the mode to mixed, as in schema.org’s mixed attendance example:

{
  "@context": "https://schema.org",
  "@type": "EducationEvent",
  "name": "Open Evening: Data Analytics Bootcamp",
  "description": "Meet the tutors, see a live lesson and ask about funding. Join in person or online.",
  "startDate": "2026-12-02T18:00:00+00:00",
  "endDate": "2026-12-02T19:30:00+00:00",
  "eventStatus": "https://schema.org/EventScheduled",
  "eventAttendanceMode": "https://schema.org/MixedEventAttendanceMode",
  "location": [
    {
      "@type": "Place",
      "name": "Example Learning Centre",
      "address": {
        "@type": "PostalAddress",
        "streetAddress": "1 Example Street",
        "addressLocality": "Manchester",
        "postalCode": "M1 1AA",
        "addressCountry": "GB"
      }
    },
    {
      "@type": "VirtualLocation",
      "url": "https://example.com/open-evening/online/"
    }
  ],
  "maximumPhysicalAttendeeCapacity": 40,
  "offers": {
    "@type": "Offer",
    "url": "https://example.com/open-evening/",
    "price": 0,
    "priceCurrency": "GBP",
    "availability": "https://schema.org/InStock"
  },
  "organizer": {
    "@type": "Organization",
    "name": "Example Academy",
    "url": "https://example.com/"
  }
}

A hybrid session has a real-world component, so Google’s virtual-only exclusion does not apply. Google’s documentation says nothing about the virtual half, so make sure the Place alone would pass.

4. Cohort start date as a CourseInstance

A cohort is not a standalone event. It is an offering of a course, and schema.org models that as a CourseInstance nested in the Course through hasCourseInstance. Because CourseInstance is a subclass of Event, it takes startDate, endDate and location, plus course-specific properties: courseMode (“online”, “onsite” or “blended”), courseSchedule, courseWorkload and instructor.

{
  "@context": "https://schema.org",
  "@type": "Course",
  "name": "Data Analytics Bootcamp",
  "description": "A 12-week part-time programme covering SQL, Python and dashboarding.",
  "provider": {
    "@type": "Organization",
    "name": "Example Academy",
    "sameAs": "https://example.com/"
  },
  "hasCourseInstance": [
    {
      "@type": "CourseInstance",
      "name": "January 2027 cohort",
      "courseMode": "online",
      "startDate": "2027-01-12",
      "endDate": "2027-04-06",
      "courseWorkload": "8 hours per week: two live evening sessions and independent project work",
      "courseSchedule": {
        "@type": "Schedule",
        "repeatFrequency": "P1W",
        "byDay": ["https://schema.org/Tuesday", "https://schema.org/Thursday"],
        "startTime": "18:30:00",
        "endTime": "20:30:00",
        "scheduleTimezone": "Europe/London"
      },
      "instructor": {
        "@type": "Person",
        "name": "Jane Example"
      }
    },
    {
      "@type": "CourseInstance",
      "name": "April 2027 cohort",
      "courseMode": "blended",
      "startDate": "2027-04-20",
      "endDate": "2027-07-13",
      "location": {
        "@type": "Place",
        "name": "Example Learning Centre",
        "address": {
          "@type": "PostalAddress",
          "streetAddress": "1 Example Street",
          "addressLocality": "Manchester",
          "postalCode": "M1 1AA",
          "addressCountry": "GB"
        }
      }
    }
  ]
}

The cohort dates are plain dates; the weekly rhythm lives in courseSchedule, a schema.org Schedule with an IANA time zone.

Google’s course features work at the Course level, not the instance level. Its course list documentation requires name and description, recommends provider, and needs at least three courses, with carousel markup on a summary or all-in-one page. The course schema guide is the first part of this two-part series and covers that side in full.

5. A recurring live session

Weekly office hours do not need a new Event per week. schema.org’s eventSchedule property associates an event with a Schedule, and the Schedule documentation says an event using it “should not have startDate or endDate properties”, because those live in the schedule.

{
  "@context": "https://schema.org",
  "@type": "EducationEvent",
  "name": "Weekly Portfolio Review Office Hours",
  "description": "Drop-in portfolio reviews with a senior tutor, every Wednesday evening during term.",
  "url": "https://example.com/office-hours/",
  "duration": "PT60M",
  "eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
  "location": {
    "@type": "VirtualLocation",
    "url": "https://example.com/office-hours/"
  },
  "eventSchedule": {
    "@type": "Schedule",
    "startDate": "2027-01-13",
    "endDate": "2027-03-31",
    "repeatFrequency": "P1W",
    "byDay": "https://schema.org/Wednesday",
    "startTime": "19:00:00",
    "endTime": "20:00:00",
    "scheduleTimezone": "Europe/London"
  },
  "organizer": {
    "@type": "Organization",
    "name": "Example Academy",
    "url": "https://example.com/"
  }
}

Google’s feature requires startDate, so a schedule-only event is description, not a rich result candidate. If you need an in-person recurring class to appear in the event experience, Google’s guidance is the opposite: where there are several performances “each with individual tickets, add a separate Event element for each performance.”

6. A livestream with a LIVE badge

For a session streamed on your own watch page, aim for Google’s video feature instead. Google’s video structured data documentation says a LIVE badge “can be applied to any public video that is live-streamed for any length of time” when you nest a BroadcastEvent in the VideoObject:

{
  "@context": "https://schema.org",
  "@type": "VideoObject",
  "name": "Live lesson: Joins in SQL for Beginners",
  "description": "A live-streamed lesson on SQL joins, with questions answered on air.",
  "thumbnailUrl": "https://example.com/img/sql-joins-live.jpg",
  "uploadDate": "2027-02-17T18:00:00+00:00",
  "embedUrl": "https://example.com/live/sql-joins/embed/",
  "publication": {
    "@type": "BroadcastEvent",
    "isLiveBroadcast": true,
    "startDate": "2027-02-17T18:00:00+00:00",
    "endDate": "2027-02-17T19:00:00+00:00"
  }
}

Google requires publication, isLiveBroadcast, startDate and endDate for the badge, and says endDate must be provided once the stream has finished. It also asks you to call the Indexing API when the video goes live, when it stops and when the markup changes. Livestreams hosted on YouTube are a separate question, covered in YouTube SEO for AI citations.

Dates and time zones: where most event markup breaks

Google’s date guidance on the event documentation page is specific, and every rule in it maps to a real bug:

  • Give the UTC offset. “If no timezone is provided, Google uses the timezone of the event’s location.” For an in-person event that is a fallback; for anything online there is no location to fall back on.
  • Do not invent midnight. If the start hour is not known, give the date alone (2027-01-12). Google lists 2019-07-20T00:00:00 as “likely wrong unless the event was meant to start at midnight.”
  • Do not use T23:59:59+00:00 for “end of day”. Google shows how that value shifts to a different day once converted to the event’s time zone.
  • Multi-day events need both dates. Give startDate and endDate, and leave the time out if you do not know it.

For UK teams the trap is British Summer Time. A 6pm London workshop is 18:00:00+00:00 in December and 18:00:00+01:00 in June, so a template that appends a hard-coded +00:00 is an hour out for summer events. Store the IANA zone (Europe/London) with the event and compute the offset per date.

Cancelled, postponed, rescheduled and past events

eventStatus takes four values in Google’s documentation:

StatusWhen to use itWhat else changes
EventScheduledDefault. Going ahead as plannedNothing
EventCancelledNot happeningDates stay as they were, as in Google’s example
EventPostponedMoving, new date unknownKeep the original startDate until you know
EventRescheduledMoving to a known new dateNew startDate/endDate, old one in previousStartDate

Past events need a decision too. Google’s general guidelines say: “We won’t show a rich result for time-sensitive content that is no longer relevant.” Once a workshop has run, change the markup to match what the page now is, for example VideoObject for the replay, rather than leaving a stale Event on it.

Implementing it in a template

Hand-written JSON-LD on event pages rots within a term. The fix is the same one I use for breadcrumb schema: generate the markup from the record that renders the page.

This helper takes one event record and picks the attendance mode and location shape itself, so the workshop, webinar and hybrid cases come from one function:

// Build Event JSON-LD from one event record.
export function eventJsonLd(ev) {
  const venue = ev.venue && {
    '@type': 'Place',
    name: ev.venue.name,
    address: { '@type': 'PostalAddress', ...ev.venue.address },
  };
  const online = ev.joinUrl && { '@type': 'VirtualLocation', url: ev.joinUrl };
  const location = [venue, online].filter(Boolean);
  const mode = venue && online ? 'Mixed' : online ? 'Online' : 'Offline';

  return {
    '@context': 'https://schema.org',
    '@type': ev.type ?? 'EducationEvent',
    '@id': `${ev.url}#event`,
    url: ev.url,
    name: ev.name,
    description: ev.description,
    startDate: ev.start, // ISO 8601 with offset, e.g. 2026-11-18T09:30:00+00:00
    endDate: ev.end,
    eventStatus: `https://schema.org/Event${ev.status ?? 'Scheduled'}`,
    eventAttendanceMode: `https://schema.org/${mode}EventAttendanceMode`,
    location: location.length === 1 ? location[0] : location,
    ...(ev.previousStart && { previousStartDate: ev.previousStart }),
    image: ev.images,
    organizer: { '@type': 'Organization', name: ev.orgName, url: ev.orgUrl },
    ...(ev.speakers && {
      performer: ev.speakers.map((name) => ({ '@type': 'Person', name })),
    }),
    offers: {
      '@type': 'Offer',
      url: ev.ticketUrl,
      price: ev.price ?? 0,
      priceCurrency: ev.currency ?? 'GBP',
      availability: `https://schema.org/${ev.soldOut ? 'SoldOut' : 'InStock'}`,
    },
  };
}

Run against a hybrid workshop record in Node, it returned MixedEventAttendanceMode and a two-item location array, and the output parsed cleanly with python3 -m json.tool. In Astro, Next.js or Nuxt, render JSON.stringify(eventJsonLd(event)) into a <script type="application/ld+json"> tag during the server render. It has to be in the initial HTML: crawlers that do not execute JavaScript never see markup injected after hydration, which is the same rendering problem described in JavaScript SEO.

Three template rules keep it honest:

  1. One event, one URL. A “what’s on” page lists events without event markup; each event’s own page carries it.
  2. Visible content matches the markup. Google’s guidelines say “if the JSON-LD markup describes a performer, the HTML body must describe that same performer.” The same goes for price, date and venue.
  3. Status comes from the booking system. If “sold out” is a field in your ticketing tool, availability should read from it, not from a person remembering to edit the page.

On a real site, event markup sits alongside the rest of the graph: Organization schema for the organiser, BreadcrumbList for the page, and @id references to tie them together. The schema markup examples article has the other types in the same copy-paste format.

How to validate event schema

Validation depends on which case you are in:

  1. In-person or hybrid events: paste the markup into Google’s Rich Results Test, then test the live URL once deployed. Google’s troubleshooting steps for a missing or wrong location use the test’s preview to confirm the eventLocation it reads.
  2. Online-only events, cohorts and schedules: there is no Google event feature to test against, so check the vocabulary with the Schema Markup Validator.
  3. Everything: check the raw server response, not the rendered DOM. curl -s https://example.com/events/your-event/ | grep -c 'application/ld+json' should return at least 1.

After launch, watch Search Console’s rich result report after every template release. A fall in valid items with no rise in invalid ones usually means a template stopped outputting the markup.

Common event schema mistakes

  • Putting the event name in location.name. Google names this as a common cause of the “event location is missing or incorrect” issue.
  • Promotional names. Prices and “LIMITED TIME SALE” do not belong in name. Google recommends a descriptive title with a distinctive detail, such as “feat. Q&A with the artist”.
  • One Event for a term of separately ticketed sessions. Each needs its own Event.
  • offers.url pointing at a generic pricing page. Google wants the booking page for that specific event.

Event schema and course schema together

For a course business, event markup is the second half of a pair. Course describes the programme; events and course instances describe when people can take part.

The division I use on education sites: the course page carries Course with hasCourseInstance for each upcoming cohort. Each open day, taster session or public workshop gets its own page with EducationEvent and links back to the course. The livestreamed lesson gets VideoObject with BroadcastEvent. None of it overlaps, and none of it asks Google for a feature it no longer offers.

The edtech marketing strategy piece covers where course buyers search now; this markup makes each of those touchpoints a page a machine can read without guessing. If you want this built into your templates rather than bolted on, it is part of the GEO and technical SEO work I do for education and SaaS teams.

Frequently asked questions

Does Google still show event rich results in 2026?

Yes, for eligible events. Event is still listed in Google’s structured data gallery, and the event experience runs in nine regions including the UK and the US. Eligibility is narrower than it was: since June 2025 the event must be bookable by the general public and held at a physical location.

Can I use event schema for a webinar?

You can, and the markup is valid schema.org: an EducationEvent with OnlineEventAttendanceMode and a VirtualLocation. It will not appear in Google’s event experience, because Google’s guidelines state that virtual experiences with no real-world component aren’t supported.

What is the difference between Event and EducationEvent?

EducationEvent is a schema.org subtype of Event for education events. It inherits every Event property and adds teaches, assesses and educationalLevel. Google accepts event subtypes, so an in-person EducationEvent is eligible for the event experience in the same way as a plain Event.

How do I mark up a course cohort’s start date?

Use a CourseInstance nested in your Course with hasCourseInstance. CourseInstance is a subclass of Event, so it takes startDate, endDate and location, plus courseMode, courseSchedule, courseWorkload and instructor.

What are the required properties for event schema?

Google requires name, startDate and location, where location is a Place with a name and a full PostalAddress. It recommends endDate, eventStatus, description, image, offers, organizer, performer and, for rescheduled events, previousStartDate.

What should happen to event markup after the event?

Update it. Google says it won’t show a rich result for time-sensitive content that is no longer relevant, so a past event gains nothing from staying marked as scheduled. Set eventStatus correctly if it was cancelled or moved, and once the page becomes a replay or a write-up, mark up what it now is, such as a VideoObject.