A calendar file that fails to import is a nuisance you notice at once. The file that imports without a complaint and puts the meeting an hour early, or stretches a one-day event over two days, is the one that costs people real appointments. Almost every case of that second kind comes from the same short list of rules in the iCalendar standard, RFC 5545: how a moment in time is written, how an end date is counted, how text is escaped and how long lines are cut. Below, each rule gets its own section with the symptom a broken file produces, and the last part is a test built to go red.
The parts a calendar insists on
An .ics file is plain text made of content lines, and the first and last lines wrap one calendar object between BEGIN:VCALENDAR and END:VCALENDAR. The standard requires the calendar object to carry a version, which for this specification is the value 2.0, and a product identifier that names the program that made the file. Inside it sit one or more events, each opened and closed by BEGIN:VEVENT and END:VEVENT.
Every event needs a unique identifier (UID), a time stamp (DTSTAMP) and, unless the calendar object declares a METHOD, a start (DTSTART). The identifier has to be globally unique and stay the same for the life of the event, because it is how a calendar recognises that a second file describes the same event, so an update replaces the old entry instead of adding a twin. The standard suggests a date-time on the left of an at sign and a domain you control on the right. The time stamp says when the object was made and has to be written in UTC, which means it ends with the letter Z.
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//example//ics-guide//EN
BEGIN:VEVENT
UID:planning-20261109T0900@example.org
DTSTAMP:20261030T081500Z
DTSTART:20261109T090000Z
SUMMARY:Planning meeting
END:VEVENT
END:VCALENDAR
Order does not matter: the standard sets no ordering for properties. A missing required property is the easy failure, since the file is non-conforming and an importer may refuse it. The expensive failures are in the values, starting with time.
Three ways to write a moment
A date-time value comes in three forms, and the spec is explicit that a fixed instant needs either UTC or a local time with a time zone reference. The forms look like this:
| Form | Example | Meaning |
|---|---|---|
| Floating | DTSTART:20261109T150000 |
the same wall-clock reading in whatever zone the viewer is in |
| UTC | DTSTART:20261109T150000Z |
one absolute instant |
| Zone reference | DTSTART;TZID=Europe/Sofia:20261109T170000 |
local time in a named zone, defined inside the file |
The first form is where "3 pm" without a zone goes wrong. The standard says a floating value should be read as fixed to whatever zone the attendee is in at that moment, so two people in different zones receiving the same file can attend at different real times, and adds that a floating value belongs only where this drift is the intent, for example a reminder that always falls at the same hour of the local day (RFC 5545, section 3.3.5). An invitation text that says "3 pm" and names no place has no fixed instant in it. Writing a guessed zone into the file turns an unknown into a confident wrong answer; writing a floating time keeps the ambiguity visible and honest.
An offset on its own, as in a time ending in -0800, is not a valid form at all. The spec lists that notation as invalid for a date-time. If all you know is an offset, convert the value to UTC and add the Z.
A zone reference needs its definition
The TZID form has a condition that is easy to miss. The standard says one VTIMEZONE component must be present for each unique TZID used in the file, and warns that a file without them can be understood differently by each reader (RFC 5545, section 3.2.19). A file with TZID=Europe/Sofia and no matching block may still import, and the program then has to rely on its own knowledge of the zone name. The standard promises nothing about the result, so the time can look right in the file and show up at some other hour.
A VTIMEZONE block holds the zone identifier plus at least one STANDARD or DAYLIGHT part; each part has a start, the offset before the change and the offset after it, and usually a yearly recurrence rule that says when the clocks move. The spec gives no naming scheme for zone identifiers; it only suggests the public time zone database as a model. The block has to agree with the name: a block that gives wrong offsets is worse than none, because it is trusted.
There are two honest ways out. Include the definition, or convert the moment to UTC yourself and write it with the Z. The second is shorter, but it freezes the offset on the day you chose, so it is only right when you are sure what the offset is on that date. Note also that a TZID parameter must not be attached to a UTC value or to a plain date.
All-day events: the end is the next date
An event that has no clock time uses the date value type, written as four digits of year, two of month and two of day, with no separators, and the property carries VALUE=DATE. The end is the part that people get wrong. The standard describes the end of an event as non-inclusive, and its own example for a festival running from 28 June to 8 July 2007 inclusive ends on 9 July (RFC 5545, section 3.6.1). For a one-day event on the tenth, the end is the eleventh:
DTSTART;VALUE=DATE:20261110
DTEND;VALUE=DATE:20261111
Put the last day itself in the end and the event can come out one day short, or empty, depending on the program. Two more rules from the same sections matter here. The end must use the same value type as the start, so a date start with a date-time end is wrong, and the end must be later than the start. If the start is a date and nothing gives an end or a length, the event lasts one day. If the start is a clock time and nothing gives an end, the event has no length: it finishes at its own start instant. So never add a made-up one-hour length when the source text states no end. DTEND and DURATION must not appear together in one event, and a duration attached to a date start has to be counted in days or weeks.
Escaping text and folding long lines
Text values have their own escape rules. The characters backslash, comma and semicolon each get a protecting backslash, a break within the text becomes a backslash and the letter n, and a colon is left alone (RFC 5545, section 3.3.11). Skip the escaping and a description like "Bring slides, notes; coffee" can be read as a list or split into fields, and a raw newline closes the property, so the rest of the sentence becomes a stray line that belongs to nothing.
Lines end with a carriage return and a line feed, and the standard asks writers to keep each line to 75 octets or fewer, not counting that ending. This is line folding: a longer line is split by putting a line ending and then one space or tab where the cut falls, and a reader removes both again when it reads the file. The unit is octets, not characters, so Cyrillic or accented text reaches the limit after fewer characters than plain ASCII does, and a cut must not fall inside one multi-byte character. The default character set of the format is UTF-8 (RFC 5545, section 3.1). A file saved with bare line feeds instead of the carriage-return pair is the quiet variant of this break: some importers accept it, some do not.
Invitations and the METHOD property
Without a METHOD property, a file is just a snapshot of calendar data; the standard says no scheduling transaction may be assumed. Adding METHOD:REQUEST turns the file into an invitation under the scheduling protocol in RFC 5546, and that raises the bar: the event must then name an organizer and at least one attendee, and a PUBLISH object must name an organizer and must not list attendees (RFC 5546, sections 3.2.1 and 3.2.2). A file that says REQUEST and names nobody to invite is internally inconsistent, and clients react differently. When the file travels by mail, the method has to match the one on the text/calendar media type. If all you want is for someone to add an event to their own calendar, leave METHOD out.
Checking a file before sending it
Testing an .ics file proves something only when the test is able to fail. Take one file that imports correctly and break it on purpose, one rule at a time: remove the DTSTAMP, drop the VTIMEZONE block while keeping the TZID, set an all-day end to the last day instead of the day after, leave a comma unescaped in the location, leave an over-long line uncut. Import each broken copy into at least two different calendar programs and confirm that something visibly goes wrong, or that your checker flags it. If a broken copy looks identical to the good one, the test does not measure that rule. Then check the good file the way a person would: open the event and read the start, the end and the zone as the app displays them, not the file.
When this does not apply
Only one event in iCalendar 2.0, as RFC 5545 defines it, is covered here, together with the invitation basics of RFC 5546. Recurrence rules, alarms, to-dos, free-busy data, attachments and the extensions from later documents are outside it, and so is the JSON representation of calendars. How each real application treats a file that breaks these rules differs by program and version; this guide read the standards and did not test Google Calendar, Outlook or Apple Calendar, so the descriptions of what a client does on a break are expectations, not measurements. The standard also leaves some behaviour to implementations, such as how a floating time is shown. A file can follow every rule here and still describe the wrong event if the source text was misread.
What the event-to-ics skill does
Our event-to-ics skill takes the text of an invitation and writes the file. It keeps a named zone together with its definition block, or converts to UTC with the Z, and leaves a clock time that has no zone floating. For a whole-day event it puts the end on the next date, and no end time is invented when the message lacks one. It protects commas, semicolons and backslashes, cuts long lines, and adds a place, organizer or attendee only when the message names one. If you also move data between formats, our note on repairing data with code or a model explains when each is the safer choice.
Read this post as Markdown: /blog/icalendar-ics-time-zones-all-day.md · Atom feed.
