ICS file structure
A field reference for the iCalendar format: line grammar, folding at 75 octets, escaping, required properties, recurrence rules and a complete file.
An iCalendar file is a flat sequence of lines wrapped in nested BEGIN/END blocks. No
indentation, no schema, no punctuation holding the tree together — the block markers carry
all of it, and everything else is one property per line.
This is the reference for writing or parsing one. For the format explained from the outside, what an .ics file is takes a single appointment apart for a general reader. The rules below come from RFC 5545, published in 2009, which replaced RFC 2445 from 1998.
The content line
One grammar covers every line in the file:
NAME;PARAM=VALUE;PARAM=VALUE:VALUE
- Names are case-insensitive.
DTSTART,dtstartandDtStartare one property, as are parameter names and the standard’s own enumerated values. Text you supply is not. - Parameters sit between the name and the value, each introduced by a semicolon.
- The value starts at the first colon that is not inside quotes and runs to the end of the line. Every later colon is ordinary text.
- A parameter value containing
:,;or,has to be wrapped in double quotes.
Both lines are legal; the second shows why that last rule exists:
DTSTART;TZID=America/New_York:20260812T140000
ATTENDEE;CN="Rivera, Ada";RSVP=TRUE:mailto:ada@example.com
The value on the second line begins after RSVP=TRUE, so the colon inside the address is
past the split point and needs no protection. The comma in the display name does: without
the quotes, a parser sees two parameters where there is one.
Folding is measured in octets, not characters
A line should not exceed 75 octets, excluding its line break. Longer ones are folded:
insert a CRLF, then begin the next line with a single space or a horizontal tab. That
leading whitespace is a marker rather than content — unfolding strips it and joins what
remains with nothing between.
A 97-octet property, and the same property folded:
DESCRIPTION:Doors open at 18:30\, the keynote starts at 19:00 sharp\, and t
he bar closes at 23:00
The break falls inside the word the, which looks wrong and is not: fold positions carry no
meaning. What a break must not land inside is a multi-octet UTF-8 sequence, and that is what
a writer counting characters eventually produces — ö is two octets and an emoji four, so
a character limit both overshoots 75 octets and splits a letter down the middle. The
standard warns about exactly that.
Every line ends with CRLF
Carriage return then line feed, on every line including the last. Files with a bare \n are
common, most calendars tolerate them, and strict parsers reject the lot — an annoying bug to
chase, since nothing looks wrong in an editor.
Text pasted into a <textarea> has its breaks rewritten to \n before any script can look
— the HTML specification requires it — so line endings can only be reported from the file
itself. That is why the ICS viewer treats a paste and a drop differently.
Escaping inside text values
Four sequences, and only four, apply to SUMMARY, DESCRIPTION, LOCATION, COMMENT and
the other text-typed properties:
| Written | Means |
|---|---|
\\ |
a literal backslash |
\; |
a literal semicolon |
\, |
a literal comma |
\n or \N |
a line break within the value |
A colon is not escaped in a text value. It cannot be and need not be: the value already started at the first unquoted colon, so the rest are just characters.
Encoding order is not a style choice. Replace backslashes first. Do it last and you
escape the backslashes you just introduced, turning a;b into a\\;b, which reads back as a
literal backslash plus a structural semicolon. Two characters wrong, invisible until it
reaches somebody’s calendar.
The components
| Component | What it holds |
|---|---|
VCALENDAR |
The container. Exactly one, wrapping everything else |
VEVENT |
An appointment: something occupying time on a calendar |
VTODO |
A task, with an optional due date and a completion state |
VJOURNAL |
A dated note, attached to a day rather than a span of time |
VFREEBUSY |
A report of busy and free intervals, with no detail about why |
VTIMEZONE |
The offsets and daylight-saving rules for one named zone |
VALARM |
A reminder, nested inside a VEVENT or a VTODO |
One nesting detail catches quickly written parsers. A VALARM has its own DESCRIPTION
— required for a display alarm — and that text belongs to the reminder. A loop assigning
every DESCRIPTION it meets to the current event either overwrites the real one or invents
one that was never there.
The properties you cannot leave out
| Component | Required | Notes |
|---|---|---|
VCALENDAR |
PRODID, VERSION |
VERSION is 2.0, and has been since 1998 |
VEVENT |
UID, DTSTAMP |
Plus DTSTART, unless the calendar sets a METHOD |
PRODID identifies the program that produced the file: never displayed, and the quickest
way to work out where something came from. DTSTAMP is when the file was written, which has
nothing to do with when the meeting is. SUMMARY is not required — a valid event can
carry no title at all, and calendars draw it as an untitled block.
Dates and times
Three forms exist, they mean different things, and picking the wrong one is the commonest defect in generated files.
DTSTART:20260812T140000 floating
DTSTART:20260812T120000Z UTC
DTSTART;TZID=Europe/Stockholm:20260812T140000 zoned
Floating is a wall clock with nothing anchoring it: two o’clock wherever it is read. Right for a birthday, wrong for a webinar, and the author never sees the failure, since their own machine sits in the zone they had in mind.
UTC is an absolute instant, which every calendar renders in its viewer’s local time.
Zoned is a wall clock plus the name of the clock. It requires a matching VTIMEZONE
block in the same file, one per distinct TZID, carrying that zone’s standard and daylight
offsets; without it, strict parsers refuse the file. DTEND may also carry a different
TZID from DTSTART — that is how a flight is written — so a reader assuming one zone for
both reports the wrong arrival.
For whole days, switch the value type:
DTSTART;VALUE=DATE:20260812
DTEND;VALUE=DATE:20260813
DTEND is exclusive: it names the first moment outside the event, not the last one
inside it, so a single day on the 12th closes on the 13th. Repeat one date in both fields and
the span collapses to nothing, and what a calendar does with that is its own business.
DURATION is the alternative to DTEND, and the two must never both appear on one event:
DTSTART:20260812T120000Z
DURATION:PT1H30M
P opens the duration and T introduces its time part, so PT1H30M is ninety minutes and
P3D is three days. When DTSTART is a DATE, the duration has to be given in days or
weeks.
Recurrence
A single RRULE on the event describes the whole series. FREQ is required; everything else
has a default.
RRULE:FREQ=WEEKLY;BYDAY=TU,TH;COUNT=8
RRULE:FREQ=MONTHLY;BYDAY=-1FR
RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=TH;BYSETPOS=4
FREQ—SECONDLY,MINUTELY,HOURLY,DAILY,WEEKLY,MONTHLYorYEARLY.INTERVAL— how many of those units between occurrences. Defaults to 1.COUNTandUNTILbound the series and must not both appear.COUNTincludes the occurrence atDTSTART.UNTILis inclusive, and its form has to agree withDTSTART: where the start is UTC or zoned,UNTILhas to be written in UTC.BYDAYtakes two-letter codes with an optional signed ordinal.-1FRis the last Friday,2MOthe second Monday; the ordinal only means anything withMONTHLYorYEARLY.BYMONTHDAYtakes 1 to 31, or −1 to −31 counting back from the end of the month.BYSETPOSpicks positions out of the set the other parts generate for each interval. The third rule above says “the last weekday of the month”; the fourth is US Thanksgiving.WKSTnames the day the week begins on,MOby default. It changes which occurrences aWEEKLYrule with anINTERVALabove 1 produces.
Exceptions live outside the rule: EXDATE removes named occurrences, RDATE adds dates the
rule would not generate. A single occurrence that moved is written as a second VEVENT
sharing the same UID, plus a RECURRENCE-ID naming the slot it replaces — which is why
counting VEVENT blocks does not tell you how many appointments a file describes.
UID and SEQUENCE decide update against duplicate
UID is the event’s permanent identity, conventionally an opaque token, an @, and a domain
you control. Re-import a corrected file and the calendar matches on it: same value, the
existing entry is replaced; different value, a second entry lands beside the first.
SEQUENCE is the revision number, starting at 0 and raised by whoever owns the event each
time something meaningful changes, so a client holding two versions of one UID knows which
is newer.
That is why a generator randomizing UID per download makes duplicates unavoidable. A
download page has no memory of the previous visitor, so the identifier must be derived
rather than invented: hash the fields that define the event — title, start, location — and
the same event yields the same UID every time, with nothing stored anywhere. That is how
the ICS file generator does it.
METHOD, and what makes an invitation an invitation
METHOD on the VCALENDAR promotes a description of an event into a scheduling message, in
the vocabulary defined by iTIP (RFC 5546).
PUBLISH— a bulletin. Here is an event; no reply is expected.REQUEST— an invitation. Recipients are being asked to answer, and mail clients draw Accept and Decline buttons for it.
CANCEL, REPLY and a few others complete the set. A METHOD is also what makes DTSTART
optional, since a cancellation need only identify what it cancels.
Participants are listed with ORGANIZER and ATTENDEE, whose values are mailto: URIs:
ORGANIZER;CN=Ada Rivera:mailto:ada@example.com
ATTENDEE;CN=Sam Ohlin;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:sam@example.com
PARTSTAT holds that person’s answer so far — NEEDS-ACTION, ACCEPTED, DECLINED,
TENTATIVE or DELEGATED. RSVP=TRUE asks for one; it defaults to FALSE, and forgetting
it produces an invitation nobody is prompted to answer.
X- properties
Anything beginning with X- is non-standard by design, and parsers are obliged to tolerate
what they do not recognize. The one worth knowing is X-WR-CALNAME:
X-WR-CALNAME:Autumn program
It appears in no RFC — it is a vendor extension that spread until everyone implemented it —
and it is what gives an imported calendar a name instead of a filename. RFC 7986, from 2016,
standardized a NAME property for the same job, but support is patchy, so the X- version
remains the one that works.
A complete file
Save this with an .ics extension and it opens. It is the output of our own generator, so
every rule above has been applied to it:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//creatorvalet.com//ICS Generator//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Spring program
BEGIN:VEVENT
UID:8fbhdd1qoxgo5@creatorvalet.com
DTSTAMP:20260806T091500Z
DTSTART:20260812T120000Z
DTEND:20260812T133000Z
SUMMARY:Spring product briefing
LOCATION:Slagthuset\, Malmo
DESCRIPTION:Doors open at 13:30\, badges at the desk
STATUS:CONFIRMED
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Spring product briefing
TRIGGER:-PT30M
END:VALARM
END:VEVENT
END:VCALENDAR
CALSCALE:GREGORIAN is the default and could be omitted. TRIGGER:-PT30M fires the alarm
thirty minutes ahead of the start; a positive duration would fire it afterwards.
Where these files go wrong
Roughly in order of how often they turn up:
- Times with no zone and no
Z. Floating where absolute was meant, so everyone outside the author’s timezone gets a different hour. - A
TZIDwith noVTIMEZONE— or one in Microsoft’s vocabulary,W. Europe Standard Timerather thanEurope/Berlin, which browsers have no rules for. - All-day entries off by one, from reading the closing boundary as inclusive.
- Unescaped commas and semicolons in
SUMMARY,LOCATIONorDESCRIPTION, truncating the value at the first one. - Backslashes escaped last, doubling the escapes just added.
- Bare
\nline endings whereCRLFis required. - Lines over 75 octets, or folded by character count so a split lands inside a UTF-8 sequence.
- A fresh random
UIDper download, which guarantees duplicates on the second import. - Both
DTENDandDURATIONon one event, orCOUNTandUNTILon one rule. - A missing
DTSTAMPorPRODID. Both mandatory, both routinely skipped by hand-rolled writers.
To check a file against that list, the ICS viewer reads it in your browser and reports what it found rather than what it assumed. For sortable rows, ICS to CSV flattens the events into a table. The rest of the calendar tools and guides are on the calendar page.