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
andEND:VEVENT
: Mark the start and end of an event description.SUMMARY
: Provides a brief description or title of the event.DTSTART
andDTEND
: 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
andEND: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?
- With Specified Time Zone: When you include a
VTIMEZONE
component and specify aTZID
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. - 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. - 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
andEND: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
andEND: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.
Name | Data Type | Length | Primary Key | Description |
---|---|---|---|---|
uid | text | 50 | yes | this is unique event id |
organization | text | 50 | your organization name | |
start | start date | |||
end | end date | |||
summary | title | |||
description | description | |||
location | place 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") || "SK", mode = Request.GetQueryStringParameter("mode") || "get", organization = Request.GetQueryStringParameter(“organization”) || "MTN", rows, row; // Basic validations and default assignments if (!uid) throw new Error("Error: ID is required."); // 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 = rows[0][“organization”] || organization, prodId = "//-" + organizer + "//" + language.toUpperCase(), uid = row["uid"], start = row["start"], end = row["end"], summary = row["summary"], description = row["description"], location = row["location"], 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>
- Date Formatting Functions: Defined two functions,
formatDate
andformatTime
, 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. - Retrieving Query Parameters: Extracts query parameters from the request URL, such as u
id
(a unique identifier for the calendar event it’s our primary key from the data extension),mode
, andorganization
. These parameters are used to fetch the correct event data and to control the behavior of the script. - Validations and Defaults: Performs basic checks to ensure required parameters are present. It also sets default values for
country
andlanguage
if they’re not specified in the request. - Data Extension Lookup: Uses the
LookupRows
function to find rows in thecalendar_info
Data Extension that match the givenid
,language
, andcountry
. Throws an error if no matching row is found. - 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. - 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. - iCalendar String Encoding: Encodes the iCalendar content as a URL-encoded string to facilitate its transmission over HTTP.
- Variable Assignment: Stores the encoded iCalendar string, the original iCalendar content, and the event summary in SFMC variables for later use in the response.
- 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. - 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 calendartimezones
– 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!
David Field
says:this is great – but bug on line 31:
— if (!id) throw new Error(“Error: ID is required.”);
+++ if (!uid) throw new Error(“Error: UID is required.”);
Also you can short circuit evaluate your query params to add defaults (which also kills line 33)…
// Retrieving query parameters
var uid = Request.GetQueryStringParameter(“uid”),
country = Request.GetQueryStringParameter(“country”) || “AU”,
mode = Request.GetQueryStringParameter(“mode”) || “get”,
organization = Request.GetQueryStringParameter(“organization”) || “MTN”,
rows;
… and address the looked up rows directly to clean up some things later on:
// Looking up rows in a Data Extension
rows = Platform.Function.LookupRows(“ics_DE”, [“uid”], [uid]);
if (rows.length == 0) throw new Error(“Error: Calendar not found.”);
var organizer = rows[0][“organization”] || organization;