Salesforce-Google Calendar Integration: Architecture and Best Practices
Introduction
Google Calendar is widely used to schedule meetings, events, and tasks — and in many Salesforce-driven processes, those events need to be created automatically, not manually. Often, requests for such meetings come from external systems, requiring the configuration of their integration with Google Workspace.
For these purposes, Salesforce offers a convenient Google API integration mechanism that can create the desired event with the specified time, description, and attendees. This eliminates the need to ask users to manually enter events in the calendar.
This article further describes the architecture of this implementation and how to use it most effectively to ensure data synchronization with minimal manual intervention. This approach can be useful not only for Google Calendar, but also for other external applications that integrate with Salesforce.
The Architecture That Actually Works
High-Level Flow
In general, the process is performed according to the following schema:
Record Update → Trigger → Queueable Job → Named Credential → Google Calendar API → Update Record
The key insight: never make callouts directly from triggers. Triggers run synchronously and have strict governor limits. Therefore, it is better to start asynchronous work with a separate call in order to make an external API call, get the result and handle errors. This option is necessary when the user needs not only to send a request to the external environment, but also to wait for a response.
Components Overview
Setting Up Authentication
Step 1: Google Cloud Console Setup
1. Open Google Cloud Console → APIs & Services → Credentials
2. Create an OAuth client ID with application type "Web application"
3. Add the Authorized redirect URI for your Salesforce org:
https://YOUR_ORG.my.salesforce.com/services/authcallback/YOUR_AUTH_PROVIDER_NAME4. Copy the Client ID and Client Secret to use them later in Salesforce
5. Enable the Google Calendar API in the same project (open APIs & Services → Library → "Google Calendar API" → Enable)
Step 2: Salesforce Auth Provider
Open Setup → Auth. Providers, create a new provider with these settings:
Important: The query parameters in the Authorize Endpoint URL trigger Google to issue a refresh token. Without them, tokens expire and the integration breaks.
Step 3: External Credential and Named Credential
Need to create an External Credential that links to your Auth Provider:
• Authentication Protocol: OAuth 2.0
• Authentication Flow Type: Browser Flow
• Identity Provider: Your Auth Provider
After that create a Named Credential:
• URL: https://www.googleapis.com
• External Credential: Your External Credential
Finally, create a Principal in the External Credential, assign it to a Permission Set, and click Authenticate to complete the OAuth flow with your Google account.
The Apex Implementation
Trigger Pattern: Detect and Queue
The trigger handler detects when a record meets the criteria for event creation and queues an async job. The key is to check both the status change AND whether an event already exists:
private void processRecordsForCalendarEvents(Map<Id, SObject> oldMap, Map<Id, SObject> newMap) {
for (Id recordId : newMap.keySet()) {
SObject oldRecord = oldMap.get(recordId);
SObject newRecord = newMap.get(recordId);
String oldStatus = (String) oldRecord.get('Status__c');
String newStatus = (String) newRecord.get('Status__c');
String eventUrl = (String) newRecord.get('Calendar_Event_URL__c');
// Only create event if status changed to target AND no event exists yet
if (oldStatus != 'Scheduled' && newStatus == 'Scheduled' && String.isBlank(eventUrl)) {
System.enqueueJob(new CalendarEventQueueable(recordId));
}
}
}The duplicate prevention check (String.isBlank(eventUrl)) is essential. Without it, any subsequent update to the record would create another calendar event. The URL field serves as a flag indicating an event already exists.
Queueable: The Async Worker
The Queueable class performs the actual API call. It must implement Database.AllowsCallouts to make external requests:
public class CalendarEventQueueable implements Queueable, Database.AllowsCallouts {
private Id recordId;
private static final Integer DEFAULT_DURATION_MINUTES = 60;
public CalendarEventQueueable(Id recordId) {
this.recordId = recordId;
}
public void execute(QueueableContext context) {
try {
// 1. Query the record with all needed fields
SObject record = queryRecord(recordId);
// 2. Build the event request body
Map<String, Object> eventBody = buildEventBody(record);
// 3. Call Google Calendar API
String eventUrl = GoogleCalendarService.createEvent(eventBody);
// 4. Update the record with the event URL
record.put('Calendar_Event_URL__c', eventUrl);
update record;
} catch (Exception ex) {
// Log error but don't throw - prevents job failure loops
System.debug(LoggingLevel.ERROR, 'Calendar event creation failed: ' + ex.getMessage());
System.debug(LoggingLevel.ERROR, 'Stack trace: ' + ex.getStackTraceString());
}
}
}Building the Event Body
Google Calendar API expects a specific JSON structure. Here's how to build it from your Salesforce record:
private Map<String, Object> buildEventBody(SObject record) {
Map<String, Object> body = new Map<String, Object>();
// Event title
body.put('summary', (String) record.get('Name'));
// Start time
DateTime startTime = (DateTime) record.get('Event_DateTime__c');
body.put('start', new Map<String, Object>{
'dateTime' => startTime.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss'),
'timeZone' => 'Etc/UTC'
});
// End time (start + duration)
Integer duration = getDurationMinutes(record);
DateTime endTime = startTime.addMinutes(duration);
body.put('end', new Map<String, Object>{
'dateTime' => endTime.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss'),
'timeZone' => 'Etc/UTC'
});
// Attendees
body.put('attendees', buildAttendeeList(record));
// Optional: description, location, conferencing
if (record.get('Description__c') != null) {
body.put('description', record.get('Description__c'));
}
return body;
}Handling Attendees: Preventing Duplicates
When building the attendee list, the same person might appear in multiple roles on your record. Google Calendar doesn't handle duplicate emails well - it either errors or sends multiple invitations to the same person. Use a Set to track already-added emails:
private List<Map<String, String>> buildAttendeeList(SObject record) {
List<Map<String, String>> attendees = new List<Map<String, String>>();
Set<String> addedEmails = new Set<String>();
// Add each potential attendee, checking for duplicates
addAttendeeIfValid(attendees, addedEmails, getRelatedEmail(record, 'Organizer__r'));
addAttendeeIfValid(attendees, addedEmails, getRelatedEmail(record, 'Participant_1__r'));
addAttendeeIfValid(attendees, addedEmails, getRelatedEmail(record, 'Participant_2__r'));
// ... add other participants as needed
return attendees;
}
private void addAttendeeIfValid(List<Map<String, String>> attendees, Set<String> added, String email) {
if (String.isNotBlank(email) && !added.contains(email.toLowerCase())) {
attendees.add(new Map<String, String>{ 'email' => email });
added.add(email.toLowerCase());
}
}The API Service Class
Encapsulate all Google API logic in a dedicated service class. This keeps your business logic clean and makes the integration testable:
public class GoogleCalendarService {
private static final String CALENDAR_API_PATH = '/calendar/v3';
// Get Named Credential name from Custom Setting
private static String getNamedCredential() {
Google_Calendar_Settings__c settings = Google_Calendar_Settings__c.getOrgDefaults();
if (settings == null || String.isBlank(settings.Named_Credential__c)) {
throw new ConfigurationException('Google Calendar Named Credential not configured');
}
return settings.Named_Credential__c;
}
public static String createEvent(Map<String, Object> eventBody) {
return createEvent(eventBody, 'primary'); // Use authenticated user's primary calendar
}
public static String createEvent(Map<String, Object> eventBody, String calendarId) {
String endpoint = '/calendars/' + calendarId + '/events?sendUpdates=all';
HttpResponse response = makeCallout('POST', endpoint, JSON.serialize(eventBody));
if (response.getStatusCode() == 200) {
Map<String, Object> result = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
return (String) result.get('htmlLink');
}
throw new CalloutException('Google Calendar API error: ' + response.getStatusCode() + ' - ' + response.getBody());
}
private static HttpResponse makeCallout(String method, String endpoint, String body) {
HttpRequest request = new HttpRequest();
request.setMethod(method);
request.setEndpoint('callout:' + getNamedCredential() + CALENDAR_API_PATH + endpoint);
request.setHeader('Content-Type', 'application/json');
if (String.isNotBlank(body)) {
request.setBody(body);
}
return new Http().send(request);
}
public class ConfigurationException extends Exception {}
}The 'callout:' prefix tells Salesforce to use the Named Credential for authentication. The Named Credential automatically injects the OAuth token into the request header. You never handle tokens directly in your code.
Error Handling
Common Errors and Solutions
Defensive Error Handling
In Queueable jobs, never let exceptions bubble up unhandled. Unhandled exceptions mark the job as failed and can cause issues with monitoring. Catch everything and log appropriately:
public void execute(QueueableContext context) {
try {
// Main logic here
} catch (CalloutException ce) {
// API communication error
logError('Callout failed', ce);
} catch (JSONException je) {
// Response parsing error
logError('Invalid API response', je);
} catch (Exception ex) {
// Unexpected error
logError('Unexpected error', ex);
}
}
private void logError(String context, Exception ex) {
System.debug(LoggingLevel.ERROR, context + ': ' + ex.getMessage());
System.debug(LoggingLevel.ERROR, 'Stack: ' + ex.getStackTraceString());
// Optional: create a log record, send notification, etc.
}Preventing Duplicate Events
Duplicate events are a common problem in integrations. Implement multiple layers of protection:
1. Trigger-level check: Only queue a job if the event URL field is blank. If there's already a URL, an event exists.
2. Queueable-level check: Before making the API call, re-query the record and verify the URL is still blank. Another process might have created an event between the trigger firing and the job executing.
3. Store the Google Event ID: Save the Google event ID (not just the URL) in a separate field. This enables future operations like updating or deleting the event, and provides a more reliable duplicate check.
// Double-check before API call
SObject freshRecord = [SELECT Id, Calendar_Event_URL__c FROM MyObject__c WHERE Id = :recordId];
if (String.isNotBlank(freshRecord.Calendar_Event_URL__c)) {
System.debug('Event already exists, skipping creation');
return;
}Configuration Best Practices
Store integration configuration in Custom Settings rather than hardcoding values. This makes the integration deployable across environments and allows administrators to modify settings without code changes:
Recommended Custom Setting fields:
• Named_Credential__c - Name of the Named Credential to use
• Default_Duration_Minutes__c - Default event duration if not specified
• Send_Notifications__c - Whether to send email invites to attendees ('all', 'externalOnly', 'none')
• Create_Video_Meeting__c - Whether to automatically add Google Meet to events
• Guests_Can_Modify__c - Whether attendees can edit the event
Testing the Integration
Before deploying to production, verify these scenarios:
1. Basic flow: Change a record to the trigger status and verify an event appears in Google Calendar with correct details.
2. Token refresh: Wait at least one hour after initial authentication, then trigger an event creation. If it fails with 401, your refresh token setup is incorrect.
3. Duplicate prevention: Update a record that already has an event URL and verify no new event is created.
4. Error handling: Temporarily break the Named Credential and verify the error is logged without crashing the job.
5. Attendee deduplication: Create a record where the same person appears in multiple participant fields and verify they receive only one invitation.
Key Takeaways
Building a robust Google Calendar integration requires attention to several architectural concerns beyond just making API calls:
• Use Named Credentials for authentication - never handle tokens directly in code
• Queue async jobs from triggers - never make callouts synchronously
• Configure the Auth Provider correctly - include access_type=offline and prompt=consent for refresh tokens
• Store configuration in Custom Settings - keep the integration deployable and configurable
• Prevent duplicates at multiple levels - trigger checks, job checks, and database constraints
• Handle errors gracefully - log them, don't let jobs crash
These patterns apply not just to Google Calendar, but to any external API integration with Salesforce. Master them once, and you'll have a solid foundation for building reliable integrations with any service.

