background shape
background shape

Add Calendar in Salesforce Marketing Cloud Email

One innovative way to enhance engagement and provide value is by integrating iCalendar (.ics) events into your email campaigns. Whether you’re hosting webinars, conferences, or any significant event, offering a seamless way for recipients to add these events to their personal calendars can significantly increase attendance rates and overall interest.

In our example, we will focus on implementing an iCalendar event, a format that is widely recognized and supported across all major email providers. This ensures that no matter what email service your audience uses, they can easily add your events to their calendars with minimal effort.

What is iCalendar (.ics)?

The iCalendar format is a widely supported standard (RFC 5545) for calendar data exchange. It’s used by many calendar and email applications like Google Calendar, Apple Calendar (formerly iCal), and Microsoft Outlook. This format allows users to send meeting requests and tasks to other internet users via email or sharing links.

What are the most comon calendar features we can implement?

The iCalendar (ICS) format supports a broad range of features for scheduling and sharing event information. Among these features, several are more commonly used due to their practicality in everyday applications such as personal, business, and educational scheduling. Here’s a list of some of the most frequently used iCalendar features

Basic Event Properties

  • BEGIN:VEVENT and END:VEVENT: Mark the start and end of an event description.
  • SUMMARY: Provides a brief description or title of the event.
  • DTSTART and DTEND: Define the start and end times of an event. For all-day events, DTEND is often set to the day following the event.
  • UID: A unique identifier for the event, crucial for updates and management of recurring events.

Timezone Support

  • BEGIN:VTIMEZONE and END:VTIMEZONE: Define time zone information to ensure correct timing across different regions.
  • TZID: Specifies the Time Zone Identifier for event start and end times.

If your audience spans across different time zones, it is good practice to accommodate them. You can choose from the following options.

How Time Zone Handling Works?

  1. With Specified Time Zone: When you include a VTIMEZONE component and specify a TZID for your events, calendar applications use this information to convert the event’s start and end times to the viewer’s local time zone. For example, if you schedule an event for 9:00 AM Eastern Time and someone in the Pacific Time zone views the event, their calendar application should automatically adjust and show the event starting at 6:00 AM Pacific Time.
  2. Without Specified Time Zone (Floating Time): If you do not specify a time zone for an event (i.e., omit TZID), the event is considered to be “floating” time. This means the event is intended to happen at the specified local time, regardless of the time zone. For example, if you schedule a “floating” event for 9:00 AM, it will appear as 9:00 AM to everyone, regardless of their time zone. This is useful for events that are not tied to a specific hour in a global sense, like daily reminders.
  3. UTC Time: If you specify a time in Coordinated Universal Time (UTC) by appending a ‘Z’ to the time (e.g., 20240315T130000Z), the event’s time is fixed and will be displayed in the exact UTC time specified, converted into the local time of whoever is viewing the calendar.

Location and Description

  • LOCATION: Indicates where the event is taking place.
  • DESCRIPTION: Provides a more detailed description of the event, which can include information not covered in the summary.

Recurring Events

  • RRULE: Defines rules for recurring events, such as weekly meetings or annual reminders.

Alarms/Reminders

  • BEGIN:VALARM and END:VALARM: Specify alarms or reminders for an event, including the type of action (e.g., display a message or send an email) and the trigger time before the event.

Attachments

  • ATTACH: Allows for the inclusion of attachments with an event, such as documents or files relevant to the event’s context.

Something that might not be releveant for our use case.

Attendees

  • ATTENDEE: Specifies participants or attendees of the event, including their RSVP status. This can also include the organizer’s contact information.

We could add key speakers to the attendees list, for example, if we are organizing an event.

Cancellation and Updates

  • Use of METHOD:CANCEL to send cancellations of events.
  • Updating DTSTAMP (the datetime stamp of the event creation or last modification), along with modifications to other properties to reflect changes in event details.

Custom Properties

  • Extensions (X-properties) like X-APPLE-STRUCTURED-LOCATION for Apple devices or other custom properties for specific applications.

Free/Busy Time

  • BEGIN:VFREEBUSY and END:VFREEBUSY: Share availability or busy times without detailing the specific events causing the unavailability.

The full ics calendar example code could look like:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Your Company//Your Product//EN
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:America/New_York
BEGIN:STANDARD
DTSTART:20201101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:20200308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
UID:123456789@example.com
DTSTAMP:20240314T123456Z
DTSTART;TZID=America/New_York:20240315T090000
DTEND;TZID=America/New_York:20240315T100000
SUMMARY:Project Meeting
LOCATION:Conference Room 101
DESCRIPTION:Monthly project meeting to discuss progress and next steps.
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder: Project Meeting starts in 15 minutes
TRIGGER:-PT15M
END:VALARM
END:VEVENT
BEGIN:VFREEBUSY
UID:fb123456789@example.com
DTSTAMP:20240314T123456Z
DTSTART:20240315T000000Z
DTEND:20240316T000000Z
FREEBUSY;FBTYPE=BUSY:20240315T130000Z/20240315T140000Z
END:VFREEBUSY
END:VCALENDAR

How to implement .ics calendar for marketing emails?

Without further ado, after long but necessary introduction (so sorry), let’s dive directly into the implementation. I will not only implement a calendar but also create a “framework” that is easy to use, allowing all calendars to be centrally managed from a data extension.

Apart from that we add functionallty to select calandar to be added as attachment or a link to your marketing emails.

Data extension

We will create non sendable data extension with following fields to work as a source information of our calendar. Also we can add more features like reminder or free busy feature and all other settings that iCalendar offers. for this demo i will keep it short.

NameData TypeLengthPrimary KeyDescription
uidtext50yesthis is unique event id
organizationtext50your organization name
startstart date
endend date
summarytitle
descriptiondescription
locationplace or link to online meeting

Cloud page

To make our calendar work, we would need a middleman to serve our calendar. A cloud page will take encrypted query parameters and, based on the specific mode set, it will either display an HTML page and start to download the calendar, or, if we use the calendar as an attachment, it will simply print our calendar to the browser.

For development, I will add only a reference to a code snippet on the cloud page to avoid caching problems. Once done, the content below should be added to the cloud page content, with some small changes, such as removing the workaround for displaying client-side script in the code snippet.

Cloud page for now will only contain following code

%%=TreatAsContent(ContentBlockByKey("ics-calendar-cta-snippet"))=%%

Now that we set our development environment and all changes will be promoted immediatelly to our cloud page we can start building our code snippet that will server two types of calendar.

  • As link within the email content
  • As attachment
<script runat=server>
Platform.Load("Core","1.1.1");
try{
  // Function to format a JS date object into YYYYMMDD format
  function formatDate(d) {
      var date = new Date(d)
      var year = date.getUTCFullYear();
      var month = ('0' + (date.getUTCMonth() + 1)).slice(-2);
      var day = ('0' + date.getUTCDate()).slice(-2);
      return year + month + day;
  }

  // Function to format a JS date object into HHMMSS format
  function formatTime(d) {
      var date = new Date(d)
      var hours = ('0' + date.getUTCHours()).slice(-2);
      var minutes = ('0' + date.getUTCMinutes()).slice(-2);
      var seconds = ('0' + date.getUTCSeconds()).slice(-2);
      return hours + minutes + seconds;
  }
  // Retrieving query parameters
  var uid = Request.GetQueryStringParameter("uid"),
      country = Request.GetQueryStringParameter("country"),
      mode = Request.GetQueryStringParameter("mode"),
      organization = Request.GetQueryStringParameter("organization"),
      rows,
      row;
      
  
  // Basic validations and default assignments
  if (!id) throw new Error("Error: ID is required.");

  if (!mode) mode = "get";

  // Looking up rows in a Data Extension
  rows = Platform.Function.LookupRows("calendar_info", ["uid"], [uid]);

 if (rows && rows.length == 0)   throw new Error("Error: Calendar not found.");

 row = rows[0]; // Assuming the first row is what we need

var organizer = row["organization"];
if (!organizer) 
    organizer = organization;

if (!organizer) 
    organizer = "MTN";


var prodId = "//-" + organizer + "//" + language.toUpperCase();
var uid = row["uid"];
var start = row["start"];
var end = row["end"];
var summary = row["summary"];
var description = row["description"];
var location = row["location"];
var now = new Date();

// Generating the DTSTAMP, DTSTART, and DTEND strings
var nowStr = formatDate(now) + "T" + formatTime(now) + "Z";
var startStr = formatDate(start) + "T" + formatTime(now).slice(0,6) + "Z";
var endStr =  formatDate(end) + "T" + formatTime(now).slice(0,6) + "Z";



// Constructing the iCalendar content
var ics = "BEGIN:VCALENDAR\n" +
"CALSCALE:GREGORIAN\n" +
"METHOD:PUBLISH\n" +
"VERSION:2.0\n" +
"PRODID:" + prodId + "\n" +
"BEGIN:VEVENT\n" +
"UID:" + uid + "\n" +
"CLASS:PUBLIC\n" +
"DESCRIPTION:" + Platform.Function.TreatAsContent(description) + "\n" +
"DTSTART:" + startStr + "\n" +
"DTSTAMP:" + nowStr + "\n" +
"DTEND:" + endStr + "\n" +
"LOCATION:" + location + "\n" +
"PRIORITY:5\n" +
"TRANSP:TRANSPARENT\n" +
"SEQUENCE:0\n" +
"STATUS:CONFIRMED\n" +
"SUMMARY:" + summary + "\n" +
"BEGIN:VALARM\n" +
"TRIGGER:-PT30M\n" +
"ACTION:DISPLAY\n" +
"DESCRIPTION:" + Platform.Function.TreatAsContent(description) + "\n" +
"END:VALARM\n" +
"END:VEVENT\n" +
"END:VCALENDAR";

 var icsEncoded = encodeURIComponent(ics);

Variable.SetValue("@icsEncoded", icsEncoded);
Variable.SetValue("@ics", ics);
Variable.SetValue("@summary", summary);
if (mode == "get"){
 Write(ics);
}else{</script><html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>%%=v(@summary)=%%</title>
    %%=Concat('<','scr','ipt type="text/javascript"','>')=%%
        window.onload = function() {
            // Create a new link element
            var link = document.createElement('a');
            // Set the link's target to a direct URL to the file you want to download
            link.href = 'data:text/plain;charset=utf-8,%%=v(@icsEncoded)=%%';
            // Set the download attribute to the name you want the file to be saved as
            link.download = 'calendar.ics';
            // Append the link to the body
            document.body.appendChild(link);
            // Simulate a click on the link
            link.click();
            // Remove the link from the body
            document.body.removeChild(link);
        };
      %%=Concat('</','scr','ipt>')=%%
</head>
<body>
    <p>If your download does not start automatically, <a download="calendar.ics" href="data:text/plain;charset=utf-8,%%=v(@icsEncoded)=%%">click here</a>.</p>
  </body>
</html><script runat=server>
  } 
  }catch(e){
   Write(Platform.Function.Stringify(e))
  }
</script>
  1. Date Formatting Functions: Defined two functions, formatDate and formatTime, to format JavaScript Date objects into strings suitable for iCalendar formats (YYYYMMDD and HHMMSS, respectively). I guess this could be done better with native date time functions but nobody’s got time for that.
  2. Retrieving Query Parameters: Extracts query parameters from the request URL, such as uid (a unique identifier for the calendar event it’s our primary key from the data extension), mode, and organization. These parameters are used to fetch the correct event data and to control the behavior of the script.
  3. Validations and Defaults: Performs basic checks to ensure required parameters are present. It also sets default values for country and language if they’re not specified in the request.
  4. Data Extension Lookup: Uses the LookupRows function to find rows in the calendar_info Data Extension that match the given id, language, and country. Throws an error if no matching row is found.
  5. Event Information Extraction: Retrieves information about the event from the found Data Extension row, such as the organization name (defaulting to fromName or “MTN” if not provided), event start and end times, summary, description, and location.
  6. iCalendar Content Generation: Constructs an iCalendar (.ics) file’s content with the event details, including start time, end time, creation timestamp (DTSTAMP), and a VALARM component for an event reminder.
  7. iCalendar String Encoding: Encodes the iCalendar content as a URL-encoded string to facilitate its transmission over HTTP.
  8. Variable Assignment: Stores the encoded iCalendar string, the original iCalendar content, and the event summary in SFMC variables for later use in the response.
  9. Conditional Response Handling: Depending on the mode specified in the query parameters, the script either directly outputs the iCalendar content (for mode == "get") or embeds it within an HTML document that automatically triggers the download of the .ics file when loaded in a browser. This HTML document includes a fallback link for manual download if the automatic process fails.
  10. Error Handling: Catches and displays any errors encountered during the execution of the script, using Stringify to format the error object for output.

As mentioned above, this event will be set in the UTC timezone, as indicated by the addition of ‘Z’ at the end of the timestamp. Also, the timestamp should not contain any separators or spaces; otherwise, it won’t work properly.

How to use the calendar in the email delivery

Now, last but not least, is to add our calendar to the data extension and then reference it in our email template.

As link within the email content

<a href="%%=RedirectTo(CloudPagesURL(6663,'mode','browser','cid',1))=%%">Download calendar</a>

As email attachment

%%[AttachFile('http',RedirectTo(CloudPagesURL(6663,'uid',1)),'calendar.ics')]%%

What is next?

You can take this implementation as a starting template that you can gradually enhance with new features. Some features I would consider implementing include:

  • localization – add language mutations for when you send email to audience spread all around the world.
  • free / busy – to add campaign manager more options for their calendar
  • timezones – add possiblity to add event timezone.
  • attendees – for adding key speakers or key contacts for the event

You could potentially go as far as creating a cloud page that is protected by a login and manages the calendars for you, instead of adding rows through the Contact Builder.

Have fun!

Oh hi there 👋
I have a FREE e-book for you.

Sign up now to get an in-depth analysis of Adobe and Salesforce Marketing Clouds!

We don’t spam! Read our privacy policy for more info.

Share With Others

Leave a Comment

Your email address will not be published. Required fields are marked *

MarTech consultant

Marcel Szimonisz

Marcel Szimonisz

I specialize in solving problems, automating processes, and driving innovation through major marketing automation platforms.

Buy me a coffee