# Event Schema: The Structured Data Most Local Sites Skip

**Author:** John Morabito (Founder, /winston)
**Published:** September 20, 2026
**Reading time:** 11 minutes
**Canonical:** https://www.winstondigitalmarketing.com/playbooks/event-schema-for-ai-answers/

Businesses that run tastings, classes, open houses and popups announce them where the announcement expires in a day, and publish nothing structured on the site at all. Event is the schema type that makes a name, a date, a place and a price readable, and it is the one type most local sites never ship.

## The short answer

A question like "what is happening in Ridgewood this Saturday" gets answered from things that have a name, a date and a place attached in a form something can sort. Your Saturday tasting has all three. What it does not have is a URL where those three facts sit in machine-readable form, because the tasting was announced in a story that expired, a post that scrolled away, and a line on a chalkboard by the door.

Event is the schema type for anything bound to a date and a place: a class, a tasting, an open house, a workshop, a popup, a launch, a webinar. It is also the type with the widest gap between the businesses that would benefit and the businesses that ship it. `LocalBusiness` comes with the theme. `Article` comes with the blog. `Event` has to be created per event, by a person, on purpose, which is why it does not happen.

The payoff is narrower than most technical SEO work and unusually clean. There is no keyword here to outrank anybody for. You are supplying a fact nobody else has published, about an event only you know the details of, at a moment when the question being asked is specific and local and has a small number of correct answers. Almost every other page on your site is one of many plausible answers to a general question. An event page is often the only answer to a precise one.

This is the per-page half of a job whose other half is the business itself. If your `LocalBusiness` block is missing or thin, fix that first, because an event with no credible organizer behind it is a floating date. The field-by-field version of that is in the LocalBusiness schema guide: https://www.winstondigitalmarketing.com/playbooks/local-business-schema-guide/, and the wider question of which types connect to which is in schema markup for AI engines: https://www.winstondigitalmarketing.com/playbooks/schema-markup-for-ai-engines-2026/.

## The six properties that carry the answer

Schema.org allows dozens of properties on `Event`. Six of them decide whether the record can answer a question, and two of those six are the ones almost every implementation leaves out.

| Property | What it has to contain | The version that fails |
| --- | --- | --- |
| `name` | The event as a person would say it out loud, including which one it is: "Beginner Sourdough, Saturday October 4" | "Class" or "Workshop", or the series name with no instance in it |
| `startDate` and `endDate` | ISO 8601 with a time and a UTC offset: `2026-10-04T18:30:00-04:00` | A date with no time, or a time with no offset, which makes the event ambiguous by up to a day |
| `location` | A nested `Place` with a full `PostalAddress`, or a `VirtualLocation` with the join URL | The venue name as a bare string, which is a label, not a place |
| `offers` | An `Offer` with `url` pointing at the page where you can actually book, plus `price`, `priceCurrency`, `availability` and `validFrom` | Omitted because the event is free, or a price written as "$25" instead of a number with a currency alongside it |
| `eventStatus` | One of scheduled, cancelled, postponed, rescheduled or moved online | Left off, so a cancelled event goes on reading as scheduled until somebody deletes the page |
| `eventAttendanceMode` | Offline, online, or mixed | Left off, so a Zoom workshop looks like an address you can drive to |

Here is a complete one. The details are invented so that nothing in it reads as a claim about a real business.

```json
{
  "@context": "https://schema.org",
  "@type": "Event",
  "@id": "https://harlowbakery.example/events/beginner-sourdough-2026-10-04/#event",
  "name": "Beginner Sourdough, Saturday October 4",
  "description": "A three-hour hands-on class covering starter maintenance, bulk fermentation and shaping. Each student leaves with a shaped loaf and a jar of starter.",
  "startDate": "2026-10-04T10:00:00-04:00",
  "endDate": "2026-10-04T13:00:00-04:00",
  "eventStatus": "https://schema.org/EventScheduled",
  "eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
  "location": {
    "@type": "Place",
    "name": "Harlow Bakery",
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "418 Onderdonk Avenue",
      "addressLocality": "Ridgewood",
      "addressRegion": "NY",
      "postalCode": "11385",
      "addressCountry": "US"
    }
  },
  "organizer": { "@id": "https://harlowbakery.example/#org" },
  "performer": {
    "@type": "Person",
    "name": "Dana Harlow"
  },
  "image": "https://harlowbakery.example/assets/sourdough-class.jpg",
  "offers": {
    "@type": "Offer",
    "url": "https://harlowbakery.example/events/beginner-sourdough-2026-10-04/#book",
    "price": "85",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "validFrom": "2026-09-01T09:00:00-04:00"
  }
}
```

Four of those fields are worth a note. `organizer` points at the same `@id` your `Organization` node uses elsewhere on the site rather than describing the bakery again, which is what turns a loose event into part of your entity graph instead of an orphan. `availability` is the field that distinguishes a class you can still get into from one that sold out in March, and it is worth updating because "can I still get in" is half of why anybody asks about an event at all. `price` is a number with the currency carried separately, and a free event takes `"price": "0"` rather than no offers block, because an absent price means unknown, not free. And if you postpone or reschedule, `previousStartDate` is what reconciles the announcement people already saw against the date you moved to.

> **The one rule that voids the rest:** Every fact in the markup has to be visible on the page in text. If the schema says the class costs 85 dollars and the page shows the price only inside an image, or the schema carries an address the page never states, you have not marked up the page, you have written a second page for parsers that nothing corroborates. Mismatched markup is a worse position than no markup, because it is the kind of error that gets treated as an attempt to mislead rather than an oversight.

## A weekly class is not one page

This is where most implementations go wrong, and the mistake feels like good housekeeping at the time. You run a class every Thursday. You have one page called "Thursday Pottery Class" with one `Event` block on it, and you update the copy at the top when the details change. It looks tidy. It cannot answer the only question anybody asks about a recurring class, which is when the next one is.

The `startDate` on that page is one of two things. It is the date of the first session ever held, in which case the record says your class happened once, in 2023. Or somebody rewrites it every week, in which case the page has a different identity every Thursday and no stable record of any specific session exists. Neither gives you something that can be sorted into a list of what is on this weekend.

The fix is dated instances. Each upcoming session gets its own URL, its own `startDate`, its own `offers` block with its own `availability`, and its own `eventStatus` so the week you close for a holiday can say so. The series page stays, and its job changes: it describes what the class is, holds everything that does not vary, and links the upcoming instances. In schema terms the series is an `EventSeries` and each session is an `Event` that points back to it via `superEvent`.

There is a property called `eventSchedule` that takes a `Schedule` object and expresses a repeating pattern compactly, and it is genuinely useful for a class that runs on the same terms indefinitely. Use it as a supplement. A repetition rule describes the pattern; it does not carry the fact that the October 9 session is sold out and the October 16 one moved to the annex. Those per-session facts are the ones people are actually asking about.

Generating a page per date sounds like a chore until you notice it is a template with a date in it. If your site has any build step at all, this is a loop over a list of dates. If it does not, twelve weeks of a weekly class is twelve copies of one page with two fields changed, which is an afternoon, once a quarter.

## The index and the event page do different jobs

Two pages are in play and they get cited for different questions, which is why the answer to "which one should I build" is both, with each one built for its own job.

| Page | The question it answers | What it needs |
| --- | --- | --- |
| The events index | "What is happening near me this weekend" | Every upcoming event listed in the HTML as text with its date, place and link, marked up as an `ItemList` whose items are `Event` nodes, in date order, with past events removed |
| The individual event page | "What is this one, and can I still get in" | A complete `Event` block, the booking link on the page rather than a click away, the address as text, and enough description to answer a follow-up question without another request |

The index is the page that gets found for the browsing question, so it has to be readable on its own. An index of card titles, with the dates rendered inside the card images and the times a click away, answers neither question: not the browsing one, because there are no dates in the text, and not the specific one, because there are no details. Put the date and the place in the markup and in the visible text of each row.

The individual page is what earns the citation for the specific question, and it earns it by being complete. The most common gap is the booking step sitting behind a button that opens a vendor modal, so the page never states a price and never links to anything with a price on it. If someone reading only your HTML cannot tell what it costs and where to pay, the record is missing the half that converts.

One more piece of plumbing: link both ways. The index links to each event page, each event page links back to the index and to the series page it belongs to, and the pages for a recurring class link to the next instance. That is ordinary internal linking, and it is how a crawler finds the eleven events you published last Tuesday. Events are the easiest pages on a site to orphan, because they get created in a rush and nothing older links to them.

## What happens to an event after it happens

Every events section eventually accumulates a pile of dates in the past, and the instinct is either to delete them or to leave them exactly where they are. Both are wrong in their own way, and the second is worse, because a page that still presents itself as upcoming three months after the fact is actively misleading anything that reads it.

What a past event page should do is keep being a record and stop being an announcement. In practice:

1. Take it out of the index and out of any "upcoming" list. That list is the thing an assistant reads to answer what is on, and a past date in it makes the whole list untrustworthy.
2. Leave the `startDate` alone. Rolling the date forward on an old page to recycle its links destroys the record and means your archive now contains a lie about when something happened.
3. Keep the page up, with the visible copy in the past tense and, if you have them, photos, the recipe, the slides, the recording. This is the version worth citing later, and it is often the only durable evidence that the series exists and has run for years.
4. Use `eventStatus` honestly. If it was cancelled, `EventCancelled` is the correct value and it stays that way permanently. If it moved online, say so. If it ran as planned, leave it as scheduled and let the date carry the tense.
5. When the archive gets genuinely large, keep a representative set and let the thinnest ones go. What you should not do is delete a page and redirect it to the homepage, which converts a dated record into a dead end for no gain.

The reason to keep any of this is that the archive is evidence. A business with four years of dated, photographed, specific events on its own domain has published something checkable about what it does and how long it has done it. That is the same material the rest of your entity work depends on, and it is a good deal more convincing than a sentence claiming years of experience.

## The calendar nobody can read

All of the above can be done correctly inside a third-party calendar tool and still never reach the page, because these tools render in one of the two ways that hide content.

The first is client-side rendering. The HTML your server sends contains an empty container and a script tag. The events appear afterwards, in the browser, once the script runs. Google will often render this and see the events eventually; many other crawlers will not, and rendering is the slow, optional, expensive path even where it exists. What arrives in the initial HTML is what reliably gets read. The general version of this problem, including how to tell which of your pages depend on it, is in JavaScript SEO: https://www.winstondigitalmarketing.com/playbooks/javascript-seo-making-dynamic-sites-crawlable/.

The second is the iframe, and it is worse. An embedded calendar pointing at `calendar-vendor.example/yourbusiness` puts your content at a URL on somebody else's domain. Whatever it earns accrues there. Your page, from the outside, is a heading and a box.

Two checks, both under five minutes. Fetch your own events page the way a simple crawler would, without executing JavaScript, and read what comes back: if your next three events are not in there as text with their dates, the markup settings inside the widget are irrelevant. Then look at the page source for an `iframe` tag around the calendar, and check whether the URL in its `src` is on your domain.

If either check fails, the fix in order of preference is: have the tool output server-rendered HTML on your domain if it supports that; pull the events through its API at build time and publish real pages; or, if neither is possible, publish the next handful of events as plain pages yourself and keep the widget as a convenience for humans. A hand-maintained list of six real event pages beats a beautiful calendar that only exists in a browser.

> **The ticketing platform question:** Selling through Eventbrite or a similar platform is fine and often necessary. The mistake is letting the platform listing be the only place the event exists. Publish the event on your own site with complete markup and let the offer link point at the platform for checkout. You keep a record you control with your organization as the organizer, the platform keeps the part it is good at, and the two corroborate each other rather than competing.

## What to do first

If you run events and have none of this, the useful order is smallest first. Most of the value arrives in the first two steps.

1. Make one page for your next single event, with the six properties filled in and every fact from the markup visible as text on the page. One page proves the shape.
2. Run that page through the Rich Results Test and the schema.org validator, and fix what they flag. This takes minutes and catches the date format errors, which are the most common failure by a distance.
3. Fetch your existing events page without JavaScript. If the events are not in the HTML, that is now the highest-value thing on this list, ahead of any further markup.
4. Turn your recurring class into dated instances for the next quarter, with a series page above them.
5. Mark up the index as an `ItemList` of your upcoming events, and make sure each row carries its date and place as text.
6. Go through past events once: out of the index, dates left alone, status corrected, copy in the past tense. Then put a recurring reminder somewhere so it happens monthly instead of annually.

## Hiring someone for this

Event markup rarely gets sold on its own; it turns up inside a local SEO or technical engagement, which is fine as long as whoever is doing it has thought about it past the copy-paste. Three questions sort that out fast. Ask what they would do with a class that runs every Thursday, and listen for dated instances rather than one page with a repeating rule bolted on. Ask what happens to an event page after the event, and listen for whether the record survives. Ask them to check whether your current calendar is in the HTML, and see whether they know how to look without being told.

On our side the pricing is published rather than quoted after a call, and every engagement is a written scope with the number attached before you pay anything. It is on the pricing page: https://www.winstondigitalmarketing.com/pricing/. The done-for-you version of this sits inside our SEO service: https://www.winstondigitalmarketing.com/services/seo/, and if you want to see how your events pages read to a machine before you talk to anybody, put one through the free audit: https://www.winstondigitalmarketing.com/audit/.

## Frequently asked questions

### Do I need Event schema if my events are already on Facebook or Eventbrite?

Those platforms are how people find out an event exists, and they are also the reason nothing on your own site says so. A Facebook event is a record on Facebook. An Eventbrite listing is a record on Eventbrite. Both can be surfaced, both point at the platform rather than at you, and both disappear from your control the moment the platform changes what it shows. Event markup on your own page is the version you own, the one that carries your organization as the organizer, and the one an assistant can reach without an account. Keep the platform listings. They sell tickets and they corroborate the dates. Just stop treating them as the place the facts live.

### What are the required properties for Event schema?

Only name, startDate and location are structurally required, and a page with just those three is worth shipping. In practice an event that answers a real question carries six: name, startDate with an end time, location as a nested Place or VirtualLocation, offers with a price and a booking URL, eventStatus, and eventAttendanceMode. The last two are the ones almost everybody omits, and they are the two that change the meaning of the record rather than decorating it. Without eventStatus a cancelled event reads as scheduled. Without eventAttendanceMode an online workshop reads as somewhere you can drive to.

### Should a weekly class be one page or a separate page per date?

Separate dated instances, and the exception is narrower than people hope. The question a recurring class has to answer is when the next one is, and one evergreen page with a startDate of the first session ever held cannot answer it. Generate a dated instance for each upcoming session, each with its own startDate, its own offers block and its own availability. Keep the series page as the human-readable hub that describes what the class is, links the upcoming instances, and holds the things that do not change. If you genuinely cannot generate per-date pages, publish the instances as a list of Event nodes on the series page rather than collapsing them into one.

### What should I do with events after they are over?

Leave the page up, stop it from competing, and let the schema tell the truth about what happened. A past event page is still a legitimate record and often the only durable evidence that the series exists and has run for years. What it must not do is keep presenting itself as upcoming. Remove it from the index of what is on, keep the startDate as it was rather than rolling it forward, and set eventStatus to EventCancelled if it never happened. If a series has run long enough that the archive is large, keep a representative set and let the rest go, but do not delete a page and redirect it to the homepage, which throws away a dated record for nothing.

### Does my events calendar work if it is a third-party widget?

Usually not, and this is the failure that undoes all of the markup work. The widget renders its content in JavaScript after the page loads, or inside an iframe pointing at the vendor's domain. In the first case a crawler that does not execute scripts sees an empty container. In the second case the content belongs to a URL on a domain you do not control, so whatever citation it earns is not yours. The test takes a minute: fetch your own events page with JavaScript disabled and read what comes back. If your next three events are not in that HTML as text, nothing you added to the widget's schema settings matters.

### Which page gets cited, the events index or the individual event page?

They get cited for different questions, so build both and give each one a job. The index answers the browsing question, what is happening near me this weekend, and it wins that by listing each event as a real item with a date, a place and a link in the HTML, marked up as an ItemList of Events. The individual page answers the specific question, what is this one and can I still get in, and it wins that by being a complete record with the booking link on it. The common failure is an index of card titles with the dates rendered as images and the details a click away, which answers neither question.

### Will Event schema get me a rich result in Google?

Sometimes, and it is the wrong reason to do the work. Event markup is eligible for event experiences in Google Search, eligibility is not a promise, and the display treatment changes without notice. The reason to ship it is the part that does not depend on a rich result: a date, a place, a price and a status stated in a form that can be read and sorted, on a URL you control, for events that otherwise exist only in an expired story. That is useful to a search engine deciding what to show and to an assistant assembling an answer about this weekend. If the rich result appears, it is a bonus on top of a record that was worth publishing anyway.
