.rcal Specificationv0.2
Strand Builder →
The fastest way to create a .rcal file: copy the full spec into any AI assistant (ChatGPT, Claude, etc.), then describe your schedule in plain language. It will output a block of code — paste that directly into EventStrand. No need to save it as a file.
No code required. The format is plain JSON — readable by humans, writable by AI.
Already use ChatGPT or Claude? Copy the spec, describe your schedule, paste the output here.

.rcal
Format Specification

v0.2 Open — No License Required March 2026

.rcal is an open file format for encoding recurring real-world events. It is designed to be simple to publish, simple to parse, and expressive enough to describe how venues, people, and communities actually operate — including weekly rhythms, monthly patterns, seasonal windows, exceptions, and one-off events.

The format is maintained by EventStrand but is not owned by it. Anyone can read, write, parse, or build tools around .rcal files without permission, license, or fee.

💡
Design philosophy. .rcal prioritizes human readability and minimal publisher friction over machine precision. A venue owner should be able to look at their strand file and understand it. A developer should be able to parse it in an afternoon.

What .rcal is not

.rcal is not a replacement for iCal/iCalendar (.ics). iCal is designed for scheduling commitments — things you are doing. .rcal is designed for encoding possibility — things you could be doing, structured around the recurring rhythms of real-world life.

.rcal is not a calendar sync protocol. It does not implement CALDAV and does not attempt to interoperate with existing calendar infrastructure. It is a simpler, purpose-built format for a different use case.

File Format #

.rcal files are valid JSON documents with an .rcal file extension. They must be UTF-8 encoded. No other encoding is permitted.

MIME type

The registered MIME type for .rcal files is application/vnd.rcal+json. When serving .rcal files over HTTP, use this content type. Parsers should also accept application/json as a fallback.

File extension

Files must use the .rcal extension. This extension is used by applications to identify the format and associate the correct parser.

Hosted strands

A strand may be hosted at a permanent URL. Hosted strand URLs must return the .rcal file with an appropriate content type. EventStrand hosts published strands at eventstrand.com/s/{handle}/{strand-id} but any URL is valid.

⚠️
Once you go live, the URL is a promise. An .rcal file with meta.live: true is telling subscriber apps that it is permanently hosted at a specific URL — and they should check back for updates. Anyone who has scanned your QR code or installed your strand is silently pointing to that exact address. Move it or change it, and they lose the connection with no way to recover it.

Top-Level Structure #

Every .rcal file represents one strand — a named, themed collection of events from a single publisher. The file has two required top-level keys: rcal (the spec version) and events (an array of event objects). The meta object is optional but strongly recommended.

structure.rcalJSON
{
  "rcal": "0.1",
  "meta": { },
  "events": [ ]
}
FieldTypeRequiredDescription
rcal string Yes Spec version. Must be a valid semver string. Current value: "0.1". Parsers should warn on unknown versions rather than fail.
meta object No Publisher metadata. Contains title, type, location, timezone, and display preferences. See meta object.
events array Yes Array of one or more event objects. A file with an empty array is valid but has no content. See events.

meta object #

The meta object describes the publisher — the venue, person, or organization that created the strand file. Fields here apply to the strand as a whole and can be inherited by individual events.

FieldTypeRequiredDescription
title string No Display name of the publisher. Shown as the strand header in apps. E.g. "The Rusty Nail".
type string No Publisher category. One of: venue, artist, community, personal, retail, event. Used for display and filtering. Defaults to venue if omitted.
description string No Short description of the publisher. One to two sentences. Plain text only.
location object No Publisher's physical location. Inherited by all events unless overridden at the event level. See location object.
timezone string No IANA timezone identifier. E.g. "America/Chicago", "Europe/London". All times in the file are interpreted in this timezone unless a strand overrides it. Strongly recommended.
live boolean No If true, subscriber apps should periodically check the source URL for updates. Requires the file to be hosted at a stable URL. Defaults to false.
source_url string No The canonical hosted URL for this file. Required when live is true. Used by subscriber apps to poll for updates.
color string No Hex color code used as the accent color in apps. E.g. "#C0392B". Should be a saturated color with sufficient contrast against dark backgrounds.
website string No URL of the publisher's website. Must include protocol. E.g. "https://therustynaill.com".
published string No ISO 8601 date the file was first published. E.g. "2026-03-17". Informational only.
updated string No ISO 8601 datetime of the last modification. E.g. "2026-03-17T14:22:00Z". Used by subscriber apps to detect changes without re-parsing the full file.

location object #

The location object appears in both meta and individual strand objects. When present on a strand, it overrides the meta-level location for that strand only.

FieldTypeRequiredDescription
address string No Street address. E.g. "1234 N Milwaukee Ave".
city string No City name.
state string No State or province abbreviation. E.g. "IL", "ON".
country string No ISO 3166-1 alpha-2 country code. E.g. "US", "GB", "DE".
lat number No Latitude in decimal degrees. E.g. 41.9214. Used for proximity filtering in apps.
lng number No Longitude in decimal degrees. E.g. -87.7078. Used for proximity filtering in apps.
name string No Human-readable name for the location when different from the publisher name. Useful for events that occur at a secondary location. E.g. "Wicker Park Field House".
notes string No Free-text location notes. E.g. "Enter through the alley door on Fridays".
📍
Location inheritance. A strand without its own location field inherits meta.location. A strand with a location field replaces the meta location entirely — there is no partial merge. If a strand happens at a different venue but you still want to display the publisher's address, repeat it on the strand.

events[] array #

The events array contains one or more event objects. Each event represents a single recurring event, one-off, or date-listed series published under this strand. A strand file (.rcal) is the strand — a thematic, named presence belonging to one publisher. Events are the things that happen within it.

FieldTypeRequiredDescription
id string Yes Unique identifier for the strand within this file. Lowercase, hyphen-separated. E.g. "friday-jazz". Used by subscriber apps to track updates across file versions. Must be stable — do not change once published.
name string Yes Display name of the event. E.g. "Friday Jazz Night".
description string No Short description of the event. Plain text. One to three sentences.
location object No Location override for this strand. Replaces meta.location entirely. See location object.
timezone string No Timezone override for this strand. Inherits meta.timezone if omitted.
category string No Event category. See categories reference.
vibe_tags array No Array of vibe tag strings. Used for mood-based filtering. See vibe tags reference. Max 5 tags per strand recommended.
price string No Price indicator. See price values. Defaults to "unknown" if omitted.
price_note string No Human-readable price note. E.g. "$15 at the door, $10 in advance".
ticket_url string No URL to purchase tickets or RSVP. Must include protocol.
recurrence array No Array of recurrence rule objects. Multiple rules are merged — the strand occurs on all dates generated by any rule. Use for events with a predictable repeating pattern. See recurrence rules.
dates array No Explicit list of dates, maintained by the publisher over time. Use for events with no predictable pattern — a pop-up, a DJ who books when they book, a market with irregular scheduling. Each entry is either an ISO 8601 date string ("2026-05-03") or an object with date, time_start, and/or time_end to override times for that occurrence. Plain strings inherit the strand-level time_start and time_end. Publisher updates this list as new dates are confirmed. See dates array.
date string No ISO 8601 date for one-off events. E.g. "2026-06-14". Used instead of recurrence for singular events. See one-off events.
time_start string No Start time for one-off events in 24h format. E.g. "19:30". For recurring events, time is set per recurrence rule.
time_end string No End time for one-off events in 24h format. May cross midnight — "02:00" on a Saturday night event means 2am Sunday.
exceptions array No Array of exception objects describing dates the recurring pattern is skipped, cancelled, or modified. See exceptions.
lead_time_days number No How many days in advance the app should surface this event. 0 means day-of only. 7 means it appears in the portal up to a week ahead. Defaults to 0.
notes string No Free-text notes for attendees. E.g. "No reservations needed. Bar opens at 7pm, music starts at 9pm."

recurrence[] rules #

Each strand may have one or more recurrence rules. Rules are additive — the strand occurs on all dates produced by any rule. This allows complex patterns like "every weekday plus extended Saturday hours" to be expressed as two simple rules rather than one complicated one.

FieldTypeRequiredDescription
pattern string Yes The recurrence pattern type. See recurrence patterns for valid values.
every number No Interval multiplier. 1 means every occurrence, 2 means every other, etc. Defaults to 1. Used with weekly and daily patterns.
days array No Array of day strings. E.g. ["friday", "saturday"]. Accepts full day names or shorthand values. Required for weekly pattern.
month_week string No Which week of the month. One of: first, second, third, fourth, last. Required for monthly_week pattern.
month_date number No Day of month as integer 1–31. Required for monthly_date pattern. If the date doesn't exist in a given month (e.g. 31 in April), the occurrence is skipped.
time_start string No Start time in 24h format. E.g. "21:00". Interpreted in the strand's timezone.
time_end string No End time in 24h format. May be earlier than time_start to indicate the event runs past midnight.
season_start string No Month this rule becomes active. Three-letter abbreviation: "jan" through "dec". If omitted, the rule is active year-round.
season_end string No Month this rule stops being active (inclusive). Same format as season_start.
season_start_day number No Day of month (1–31) within season_start that the rule becomes active. Pairs with season_start to give day-level precision. E.g. season_start: "jun", season_start_day: 21 for the summer solstice. If omitted, the rule activates on the first day of season_start.
season_end_day number No Day of month (1–31) within season_end that the rule stops being active (inclusive). If omitted, the rule is active through the last day of season_end.
fuzzy_start boolean No If true, the start of this rule's active window is approximate — the exact day may shift year to year (e.g. astronomical solstices and equinoxes). Apps should render a ~ indicator at the start edge rather than a hard boundary. Defaults to false.
fuzzy_end boolean No If true, the end of this rule's active window is approximate. Same rendering guidance as fuzzy_start. Defaults to false.
start_date string No ISO 8601 date after which this rule becomes active. Useful for rules that begin in the future.
end_date string No ISO 8601 date after which this rule stops generating occurrences. Useful for limited-run events.

exceptions[] array #

Exceptions modify or suppress specific occurrences of a recurring strand. They do not affect the underlying recurrence rules — they are applied as a post-processing layer on the generated occurrence list.

FieldTypeRequiredDescription
type string Yes Exception type. One of: skip, cancelled_range, modified. See below.
date string Yes ISO 8601 date of the exception. For cancelled_range, this is the start date of the range.
date_end string No End date for cancelled_range type. Inclusive. All occurrences between date and date_end are suppressed.
note string No Human-readable explanation. E.g. "Closed for Thanksgiving", "Private event". Displayed to users in supporting apps.
time_start string No Modified start time for modified type. Overrides the recurrence rule's time for this specific occurrence.
time_end string No Modified end time for modified type.
location object No Modified location for modified type. Overrides the strand's location for this specific occurrence only.

Exception types

skip

Suppresses a single occurrence. Use when a regularly-scheduled event simply doesn't happen on a specific date.

cancelled_range

Suppresses all occurrences between date and date_end inclusive. Use for closures, holidays, or temporary suspension.

modified

Keeps the occurrence but changes one or more of its properties — time, location, or adds a note. The occurrence still appears in the portal; only specified fields are overridden.

dates[] array #

The dates field is used instead of recurrence or date for events that have a persistent strand identity but no predictable schedule. The publisher maintains this list over time, adding new dates as they are confirmed and removing past ones on each update.

This is the right choice for: pop-up events, artists who announce dates as they book them, markets with irregular timing, or any recurring presence that doesn't fit a weekly or monthly pattern.

💡
Why a separate field from recurrence? Recurrence rules are computed — you define the pattern and parsers generate the dates. dates is curated — the publisher decides exactly which dates exist. Both produce a list of occurrences, but the authorship and update model are different. Live strands with dates are how publishers push specific confirmed bookings to all their subscribers at once.

Entry formats

Each entry in the dates array is either a plain ISO 8601 date string or an object. Plain strings inherit the strand-level time_start and time_end. Objects can override any time field for that specific occurrence.

dates-example.rcal snippetJSON
{
  "id": "irregular-nights",
  "name": "Thursday Residency",
  "category": "music",
  "vibe_tags": ["social", "curious"],
  "time_start": "21:00",
  "time_end": "23:30",
  "dates": [
    "2026-04-09",
    "2026-04-23",
    {
      "date": "2026-05-14",
      "time_start": "22:00",
      "time_end": "02:00",
      "note": "Extended set with guest DJ"
    },
    "2026-06-04",
    "2026-06-18"
  ]
}

Date entry object fields

FieldTypeRequiredDescription
date string Yes ISO 8601 date. E.g. "2026-05-14".
time_start string No Start time override in 24h format. Overrides the strand-level time_start for this occurrence only.
time_end string No End time override in 24h format.
note string No Per-occurrence note. E.g. "Guest DJ", "Moved to upstairs room".
location object No Per-occurrence location override. Replaces the strand-level location for this date only.

Validation rules for dates

date (singular one-off) is mutually exclusive with recurrence and dates. recurrence and dates may coexist on the same event — a single opening date plus an ongoing weekly pattern, for example. A strand must have at least one schedule field.

Past dates in a dates array should be treated as informational by parser apps — they may be displayed in history views but should not appear in "what's coming up" queries. Publishers are encouraged (but not required) to remove past dates on each update to keep file size manageable.

An empty dates array ([]) is valid. It means the strand exists but has no confirmed upcoming dates. Apps may surface it as "coming soon" if the strand has a name and description.

Vibe Tags #

Vibe tags are emotional descriptors applied to strands. Apps use them to filter the portal by how the user is feeling rather than by event category. Every tag below is a valid vibe_tags value. Tags are case-insensitive.

🌿mellow
Calm, low-key, unhurried. No loud rooms.
🎉social
Oriented around people and interaction.
restless
Need to get out, move, do something.
🔮curious
Discovery, learning, exploring something new.
💥spontaneous
Happens right now, no planning needed.
🌙solo
Comfortable to attend alone.
🥂celebratory
Marking a moment, festive, special occasion.
🛋cozy
Warm, enclosed, comfortable atmosphere.
🏃active
Physical activity, movement, sport.
🌅outdoor
Takes place outside.
🏠indoor
Takes place indoors.
🆓free
No cost to attend.
⚠️
Custom vibe tags. Parsers must not reject files with unrecognised vibe tags — apps should treat unknown tags as informational and not filter on them. The canonical tag list may be extended in future spec versions.

Categories #

The category field classifies an event by type. Unlike vibe tags (which describe mood), categories describe the nature of the activity.

ValueDescription
musicLive music, DJ sets, concerts, residencies.
food_drinkHappy hours, tastings, pop-up dinners, market stalls.
fitnessClasses, open gym, sports, pools, training sessions.
artsVisual art, film screenings, gallery openings, craft.
educationWorkshops, lectures, classes, talks.
communityMeetups, group activities, neighborhood events, clubs.
marketFarmers markets, flea markets, vendor fairs.
sportSports viewing events, game days, leagues.
comedyStand-up, improv, sketch, open mic (comedy).
theaterPlays, performances, spoken word, poetry.
spiritualReligious services, meditation, spiritual gatherings.
maintenanceHome maintenance tasks, service intervals, reminders.
personalBirthdays, anniversaries, milestones, personal reminders.
generalDefault. Use when no other category fits.

Price Values #

ValueDescription
freeCompletely free. No cost at any point.
free_entryFree to enter; costs may apply inside (e.g. drinks, food).
ticketedRequires a ticket or paid admission.
donationSuggested or voluntary donation.
members_onlyRestricted to members; membership may have a fee.
variesPrice varies by date or instance. Use price_note for detail.
unknownDefault when price is omitted.

Recurrence Patterns #

daily
Every day, or every N days via every.
E.g. gym opening hours, daily standup.
weekly
Every week on specified days. Use every: 2 for bi-weekly.
E.g. Friday jazz night, Saturday farmers market.
monthly_week
A specific weekday in a specific week of the month. Requires month_week and days.
E.g. second Thursday, last Saturday.
monthly_date
A fixed date each month. Requires month_date.
E.g. rent due on the 1st, meeting on the 15th.
annual
Once per year. Requires date on the strand (MM-DD format) or a full ISO date.
E.g. birthday, anniversary, annual festival.
weekdays
Shorthand for weekly on Monday through Friday. Equivalent to days: ["weekday"].
E.g. office hours, weekday happy hour.

Day Shorthands #

The days field accepts full day names or the following shorthands:

ValueExpands to
weekdaymonday, tuesday, wednesday, thursday, friday
weekendsaturday, sunday
monday – sundayFull day names, case-insensitive.
mon, tue, wed, thu, fri, sat, sunThree-letter abbreviations, case-insensitive.

Publisher Profile #

A publisher is any venue, artist, organiser, or person who creates and maintains strands on EventStrand. Each publisher has a profile — a permanent page that lists all their published strands and can be linked, shared, or embedded.

Publisher profiles are managed through the EventStrand portal. They are not encoded in the .rcal format itself — the format describes individual strands, not collections of them. Profile management and multi-strand aggregation are platform features built on top of the format.

ConceptURL patternDescription
Publisher profile eventstrand.com/p/{handle} All strands belonging to this publisher. Links to individual strands, shows combined upcoming events, and provides the master QR code.
Individual strand eventstrand.com/s/{handle}/{strand-id} A single strand. Serves the .rcal file and a human-readable preview page. This is the URL the strand's own QR code points to.
Master QR / link eventstrand.com/p/{handle} A single code that loads all of the publisher's strands at once. Recommended for front-door placement where a visitor might not know which strand they want.
💡
One QR code to rule them all. The master QR at /p/{handle} is what most venues should put on their front door. When scanned, it installs all the publisher's strands simultaneously. Individual strand QR codes are useful for targeted contexts — a flyer for one specific night, a table card for a weekly residency, a poster for a seasonal series.

Multiple Strands per Publisher #

A publisher can maintain as many strands as makes sense for their operation. Each strand is a separate .rcal file with its own identity, QR code, URL, and subscriber list. Subscribers can install one strand, several, or all of them.

Strands are the right unit of organisation when a venue runs thematically distinct programmes that different audiences care about differently. A bar that hosts jazz on Fridays and stand-up comedy on Thursdays might keep these as separate strands — a jazz regular doesn't necessarily need the comedy schedule surfaced in their portal, and vice versa.

When to split into multiple strands

  • Different audiences who wouldn't install each other's strand
  • Different vibes or categories that would confuse the mood filter
  • Different locations (two venues, one publisher)
  • A seasonal series that should be installable independently

When to keep as one strand

  • The audience overlaps significantly and would want everything
  • The events share the same location and general vibe
  • The publisher wants one QR code to cover their whole programme

Embed widgets

Each strand can be embedded on a publisher's own website as a widget — a compact, styled card showing upcoming events with an install button. Publishers can embed a single strand widget, multiple strand widgets side by side, or a combined widget that aggregates all their strands. Embed code is generated automatically in the EventStrand publisher portal.

The strand is the source of truth. Whether a subscriber installs it via QR scan, a direct link, a master profile scan, or an embedded widget — the same live .rcal file powers all of them. Update the strand once, every surface stays current.

Live vs Static Strands #

A strand is static by default. It is downloaded once and stored locally by the subscriber app. Updates require the user to re-download the file.

A strand is live when meta.live is true and meta.source_url is set. Subscriber apps should poll the source URL periodically and merge any changes. The recommended polling interval is once every 24 hours. Apps should use the meta.updated field to detect changes without parsing the full file on every poll.

Recommended approach. Publish a live strand. The publisher updates their file once when their schedule changes. Every subscriber gets the update automatically — no action required on their end.

Merging updates

When a subscriber app receives an updated version of a live strand, it should apply the following merge strategy: match events by id. Updated events replace their previous version entirely. New events (new id) are added. Events whose id no longer appears in the updated file are considered removed and should be flagged to the user before deletion.

One-Off Events #

An event with only a date field (no recurrence, no dates) is a one-off — it happens once and never repeats. One-off events coexist alongside recurring events in the same strand file. Note that date can also be combined with recurrence — for example, an opening-night date plus a weekly pattern that follows — but it cannot be combined with dates.

one-off.rcal snippetJSON
{
  "id": "summer-block-party",
  "name": "Summer Block Party",
  "category": "community",
  "date": "2026-07-18",
  "time_start": "14:00",
  "time_end": "22:00",
  "vibe_tags": ["social", "outdoor", "free"],
  "price": "free",
  "notes": "Rain date July 25."
}

The date field is for a singular event that will never repeat. For an event without a predictable pattern that will have many dates over time, use dates instead — see dates array. One-off events should be surfaced in apps up to lead_time_days before the event date, and removed from the portal after the event passes. For live strands, publishers should remove past one-offs from the file on their next update to keep the file clean.

Validation Rules #

A valid .rcal file must satisfy all of the following:

Hard requirements (reject if violated)

  • The file is valid JSON.
  • The top-level rcal key is present and a non-empty string.
  • The top-level events key is present and an array (may be empty).
  • Every strand has a non-empty id string.
  • Every strand has a non-empty name string.
  • Strand id values are unique within the file.
  • Every recurrence rule has a pattern field.
  • One-off events (no recurrence, no dates) have a date field.
  • date is mutually exclusive with recurrence and dates. A strand with date may not also have either of the others.
  • recurrence and dates may coexist on the same event — occurrences from both are merged.
  • A strand must have at least one of recurrence, date, or dates.

Soft requirements (warn, do not reject)

  • Unrecognised fields at any level should be preserved and ignored, not rejected.
  • Unrecognised pattern, category, or vibe_tags values should be treated as unknown rather than causing failure.
  • A live: true strand without source_url should warn but not fail.
  • A dates array where all entries are in the past should warn but not fail.
  • An entry in dates that is not a valid ISO 8601 date should be skipped with a warning, not cause full parse failure.
  • A monthly_week rule without month_week should be treated as first.
  • Times outside 00:00–23:59 should be rejected per-rule with a warning.
💡
Be liberal in what you accept. The format will evolve. Parsers that hard-fail on unknown fields will break as the spec grows. Parsers that warn and continue will stay compatible across versions.

Minimal valid file #

The smallest possible .rcal file — one recurring event, no metadata.

minimal.rcalJSON
{
  "rcal": "0.1",
  "events": [
    {
      "id": "open-swim",
      "name": "Open Swim",
      "recurrence": [
        {
          "pattern": "weekly",
          "days": ["monday", "wednesday", "friday"],
          "time_start": "06:00",
          "time_end": "08:00"
        }
      ]
    }
  ]
}

Full venue strand #

A bar with two recurring events, a seasonal rule, an exception, and live hosting.

rusty-nail.rcalJSON
{
  "rcal": "0.1",
  "meta": {
    "title": "The Rusty Nail",
    "type": "venue",
    "description": "Jazz bar in Logan Square, Chicago.",
    "color": "#C0392B",
    "website": "https://therustynail.com",
    "timezone": "America/Chicago",
    "live": true,
    "source_url": "https://eventstrand.com/s/rusty-nail",
    "published": "2026-03-17",
    "location": {
      "address": "1234 N Milwaukee Ave",
      "city": "Chicago",
      "state": "IL",
      "country": "US",
      "lat": 41.9214,
      "lng": -87.7078
    }
  },
  "events": [
    {
      "id": "friday-jazz",
      "name": "Friday Jazz Residency",
      "category": "music",
      "vibe_tags": ["mellow", "social"],
      "price": "free_entry",
      "notes": "Bar opens at 7pm. Music 9pm–midnight.",
      "lead_time_days": 1,
      "recurrence": [
        {
          "pattern": "weekly",
          "days": ["friday"],
          "time_start": "21:00",
          "time_end": "00:00"
        }
      ],
      "exceptions": [
        {
          "type": "skip",
          "date": "2026-12-25",
          "note": "Closed Christmas"
        }
      ]
    },
    {
      "id": "happy-hour",
      "name": "Happy Hour",
      "category": "food_drink",
      "vibe_tags": ["spontaneous", "social"],
      "price": "free_entry",
      "lead_time_days": 0,
      "recurrence": [
        {
          "pattern": "weekly",
          "days": ["weekday"],
          "time_start": "16:00",
          "time_end": "19:00"
        },
        {
          "pattern": "weekly",
          "days": ["saturday"],
          "time_start": "14:00",
          "time_end": "18:00",
          "season_start": "may",
          "season_end": "sep"
        }
      ]
    }
  ]
}

Personal strand #

A personal file mixing life milestones, maintenance reminders, and a monthly recurring event.

my-life.rcalJSON
{
  "rcal": "0.1",
  "meta": {
    "title": "My World",
    "type": "personal",
    "timezone": "America/New_York",
    "live": false
  },
  "events": [
    {
      "id": "sisters-birthday",
      "name": "Sarah's Birthday",
      "category": "personal",
      "vibe_tags": ["celebratory"],
      "lead_time_days": 14,
      "recurrence": [
        {
          "pattern": "annual",
          "month_date": 22,
          "season_start": "aug",
          "season_end": "aug"
        }
      ]
    },
    {
      "id": "hvac-filter",
      "name": "Replace HVAC Filter",
      "category": "maintenance",
      "vibe_tags": [],
      "lead_time_days": 3,
      "recurrence": [
        {
          "pattern": "monthly_date",
          "month_date": 1,
          "every": 3,
          "time_start": "09:00"
        }
      ]
    },
    {
      "id": "book-club",
      "name": "Book Club",
      "category": "community",
      "vibe_tags": ["curious", "social", "cozy"],
      "recurrence": [
        {
          "pattern": "monthly_week",
          "month_week": "second",
          "days": ["thursday"],
          "time_start": "19:00",
          "time_end": "21:30"
        }
      ]
    }
  ]
}

Recurring strand with one-off event #

A venue's strand file that includes both a recurring weekly night and a special one-off show next month.

with-oneoff.rcalJSON
{
  "rcal": "0.1",
  "meta": {
    "title": "The Hideout",
    "type": "venue",
    "timezone": "America/Chicago",
    "live": true,
    "source_url": "https://eventstrand.com/s/hideout-chicago"
  },
  "events": [
    {
      "id": "open-mic",
      "name": "Open Mic Night",
      "category": "music",
      "vibe_tags": ["curious", "social"],
      "price": "free_entry",
      "recurrence": [
        {
          "pattern": "monthly_week",
          "month_week": "last",
          "days": ["thursday"],
          "time_start": "20:00",
          "time_end": "23:00"
        }
      ]
    },
    {
      "id": "anniversary-show-2026",
      "name": "30th Anniversary Show",
      "category": "music",
      "vibe_tags": ["celebratory", "social"],
      "price": "ticketed",
      "price_note": "$20 advance, $25 door",
      "ticket_url": "https://dice.fm/hideout-anniversary",
      "date": "2026-05-09",
      "time_start": "19:00",
      "time_end": "02:00",
      "lead_time_days": 21
    }
  ]
}

Seasonal span #

A span-style event describing astronomical summer in the northern hemisphere. No specific days or times — the event is the season itself. Apps that render span-style strands (such as ManaCal) display these as solid blocks from start to end date rather than discrete occurrences.

💡
Span detection. An event is treated as a span (rather than a schedule of occurrences) when it uses annual pattern with season_start + season_end but no days or time_start. Span-aware apps render these as a continuous block; occurrence-based apps may render only the start date.
summer.rcalJSON
{
  "rcal": "0.2",
  "meta": {
    "title": "Summer",
    "type": "personal",
    "timezone": "America/New_York"
  },
  "events": [
    {
      "id": "summer",
      "name": "Summer",
      "category": "general",
      "vibe_tags": ["outdoor", "active", "social"],
      "recurrence": [
        {
          "pattern": "annual",
          "season_start": "jun",
          "season_start_day": 21,
          "season_end": "sep",
          "season_end_day": 22,
          "fuzzy_start": true,
          "fuzzy_end": true
        }
      ]
    }
  ]
}

Both boundaries are marked fuzzy because the exact solstice and equinox dates shift by 1–2 days each year. Span-aware apps should recalculate the precise dates for the viewed year using the Meeus algorithm or equivalent, and render a ~ symbol at each fuzzy edge.

Changelog #

v0.2 2026-06-16
Adds day-level precision to seasonal windows and fuzzy boundary signaling for recurrence rules.
  • New recurrence rule fields: season_start_day and season_end_day (integer, 1–31) — pair with season_start/season_end to specify an exact day within the active month rather than snapping to first/last day of month.
  • New recurrence rule fields: fuzzy_start and fuzzy_end (boolean) — signal that the boundary date shifts year to year (e.g. astronomical seasons). Apps should render a ~ indicator at fuzzy edges and may recalculate the exact date per year using known algorithms.
  • Motivating use case: seasonal span events (summer, winter, tax season, sports seasons) for consumers such as ManaCal-style dashboards that render date ranges as continuous blocks rather than discrete occurrences.
v0.1 2026-03-17
Initial draft specification. Defines top-level structure, meta, location, events, recurrence, and exceptions objects. Renames strands[] to events[] to clarify that the file is the strand and the array contains events. Establishes canonical vibe tag list, category list, price values, recurrence patterns, and day shorthands. Introduces live vs static strand model, one-off event support (date), and manually-curated date list support (dates). Defines schedule authoring modes: recurrence (pattern-based), date (singular one-off, mutually exclusive with dates), dates (curated list). recurrence and dates may coexist on one event; date and recurrence may also coexist.

License #

The .rcal specification is released into the public domain under CC0 1.0. No rights reserved.

You may use, implement, extend, fork, or build commercial products based on this specification without attribution, permission, or fee. We ask (but do not require) that compatible implementations use the .rcal file extension and the application/vnd.rcal+json MIME type to aid ecosystem interoperability.

No license required. The format is a public good. EventStrand maintains the reference implementation and this spec document, but does not own the format.