The iCalendar Format, Explained

After reading this you will know how an .ics file stores a calendar event byte by byte, why the same event can show up at 9:00 in one place and 2:00 in another, and how to write files that strict parsers accept.

What an .ics file actually is

An iCalendar file is plain text. Open one in a text editor and you see readable lines, each with a property name, a colon, and a value. The format is defined by RFC 5545. A single event looks like this:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//ABC Tools//iCal Generator//EN
BEGIN:VEVENT
UID:1a2b3c@abctools
DTSTAMP:20240115T090000Z
DTSTART:20240620T100000
DTEND:20240620T110000
SUMMARY:Team standup
END:VEVENT
END:VCALENDAR

The whole thing is wrapped in a VCALENDAR container. Inside it sits one VEVENT block. Google Calendar, Outlook and Apple Calendar all read this same structure, which is why one file imports everywhere.

The character set is UTF-8, and line endings are CRLF (carriage return plus line feed, bytes 0D 0A). That CRLF requirement is real: some strict parsers reject files that use bare 0A line feeds.

When to use it, and when not to

Reach for an .ics file when you want to hand someone an event they can add to their own calendar in one click. Conference schedules, appointment confirmations, and "add to calendar" buttons on a web page all use this. The file is self-contained, so it works offline and needs no account.

Do not use .ics as a general database. It has no query language, no indexes, and it is verbose: a single event with a description of 200 characters can produce a 500-byte file once you add the required headers. For storing contacts rather than events, use the vCard Generator & Parser instead, which speaks the .vcf format built for that job.

A VCALENDAR can hold many VEVENT blocks in one file. The generator here writes one event per file, which is the common case for a shareable invite. Full calendar exports from Outlook or Google routinely contain hundreds of events in a single .ics.

How dates and times are encoded

Every timestamp uses the same compact layout: four-digit year, two-digit month, two-digit day, the letter T, then hours, minutes and seconds. So 20240620T100000 means 20 June 2024 at 10:00:00. There are exactly three forms, and the difference decides where the event lands.

Floating local time
Written with no suffix: DTSTART:20240620T100000. It means "10:00 wherever the viewer is." A meeting at this time shows at 10:00 in London and 10:00 in Tokyo. Good for a wake-up alarm, bad for a global call.
UTC time
Written with a trailing Z: DTSTART:20240620T100000Z. The Z stands for Zulu, meaning UTC. This is an absolute instant. It shifts on the viewer's clock: 10:00 UTC shows as 11:00 in London during summer and 19:00 in Tokyo.
Zoned time
Written with a TZID parameter, like DTSTART;TZID=Europe/London:20240620T100000, and paired with a VTIMEZONE block that spells out the offset rules. This is the most precise but the most verbose.

The offset from UTC to a viewer's wall clock is a simple addition:

t_{\text{local}} = t_{\text{UTC}} + \Delta_{\text{tz}}

Here t_{\text{UTC}} is the instant stored in the file, \Delta_{\text{tz}} is the viewer's offset from UTC (for example +9 hours in Tokyo), and t_{\text{local}} is what appears on screen. For a floating time, no offset is applied at all, so the displayed hour never changes.

A UTC event at 10:00Z shows as 10:00 in Reykjavik (offset 0), 11:00 in London (offset +1 in summer), 12:00 in Berlin (+2), and 19:00 in Tokyo (+9). The same event written as floating local time shows as 10:00 in all four cities, because no offset is applied.

Line folding and escaping

RFC 5545 limits a line to 75 octets. Longer values must be folded: split the value and begin the continuation line with a single space or tab. A parser rejoins folded lines by deleting the CRLF and the one leading whitespace character. So a 120-character description becomes two physical lines but one logical value.

Certain characters inside a value carry meaning and must be escaped with a backslash:

Escape sequences inside iCalendar text values
Literal characterWritten asReason
comma ,\,separates list values
semicolon ;\;separates structured parts
backslash \\\the escape character itself
newline\na line break inside the value

A description of "Bring laptop, charger; notes attached" must be stored as Bring laptop\, charger\; notes attached. Skip the escaping and a strict parser either truncates the value at the comma or throws an error.

A worked example you can reproduce

Building the demo event line by line

The demo button fills the form with its defaults, then produces this file. Follow each step to see how the value becomes bytes.

  1. Open the wrapper: BEGIN:VCALENDAR, then VERSION:2.0 and a PRODID naming the software.
  2. Open the event with BEGIN:VEVENT.
  3. Write a UID. It must be globally unique so a calendar can tell an update from a new event. A random token plus a domain, like e7c1a@abctools, is enough.
  4. Write DTSTAMP:20240620T090000Z, the moment the file was created, always in UTC.
  5. Write DTSTART:20240620T100000 and DTEND:20240620T110000 for a one-hour event in floating local time.
  6. Write SUMMARY:Team standup, LOCATION:Room 4 and the escaped description.
  7. Add a reminder as a nested VALARM: TRIGGER:-PT15M fires 15 minutes before the start.
  8. Close both blocks: END:VEVENT then END:VCALENDAR.

The TRIGGER value -PT15M is an ISO 8601 duration read as "minus 15 minutes." PT1H would mean one hour, P1D one day. The minus sign places the alarm before the start rather than after.

Set DTSTAMP and UID even when a quick test seems to work without them. They are required fields. A lenient parser will guess a value, but two events sharing one UID can silently overwrite each other on import.

Reading an existing file

When you paste an .ics file into the parser, it unfolds the lines, un-escapes the text values, and lists each VEVENT with its resolved fields. The parser also tells you which time form each event uses, so you can predict how it will display.

Count the events per day in a large export and you get a quick sense of a schedule's shape. The chart below shows a week where meetings cluster mid-week.

26 events across a week, peaking at 7 on Wednesday. Saturday has 1, Sunday has none.

If a parser shows an event an hour off from what you expect, check the suffix. A missing Z on what should be a UTC time is the usual cause, and it moves the event by exactly the viewer's offset.

Common mistakes

The single most frequent error is mixing time forms in one event. If DTSTART is floating (20240620T100000) but DTEND is UTC (20240620T110000Z), the computed duration is wrong by the viewer's offset, and some parsers reject the event outright.

Other pitfalls appear often enough to name:

  • Using LF line endings instead of CRLF. The file may open fine in one app and fail to import in another.
  • Reusing a UID across events, which makes a calendar treat the second as an edit of the first.
  • Forgetting to escape a comma or semicolon inside SUMMARY or DESCRIPTION, which truncates the text.
  • Writing an all-day event with a time. All-day events use DTSTART;VALUE=DATE:20240620 with a date and no T.

Related tools

To fill fields with realistic placeholder titles and locations while testing an import pipeline, generate them with the Test Data Generator. To turn a per-weekday event count like the one above into a shareable image, paste the numbers into the Chart Maker.

Frequently asked questions

Why does my event show up at the wrong time after importing?

Almost always a time-form issue. A UTC time (with Z) shifts by the viewer's offset, so a 10:00Z event shows as 19:00 in Tokyo. If you want the same wall-clock hour everywhere, use floating local time with no suffix.

Can one .ics file hold more than one event?

Yes. Place several VEVENT blocks between the single BEGIN:VCALENDAR and END:VCALENDAR. Full calendar exports do exactly this.

What does the -PT15M reminder value mean?

It is an ISO 8601 duration. PT15M is 15 minutes, and the leading minus makes the alarm fire before the start. -PT1H would be one hour before, -P1D one day before.

Does the file work in both Google Calendar and Outlook?

Yes, as long as it follows RFC 5545: CRLF line endings, UTF-8 text, and the required UID and DTSTAMP fields. The output here includes all three.

Is my data uploaded anywhere?

No. The tool runs entirely in your browser, so your files and event text never leave your device.