Skip to main content

Automating Google Calendar Events from Salesforce: Architecture Guide

About Us
Published by yuliya.dzemidchuk
29 January 2026

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

Component

 

Purpose

 

Auth Provider

 

Handles OAuth 2.0 flow with  Google

 

External Credential

 

Stores tokens and manages  authentication principals

 

Named Credential

 

Endpoint configuration with  automatic token injection

 

Custom Settings

 

Configuration storage  (credential names, event defaults)

 

Trigger Handler

 

Detects qualifying record  changes, queues async jobs

 

Queueable Class

 

Async job that constructs and  sends the API request

 

API Wrapper Class

Encapsulates Google Calendar API  logic and response parsing

 

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_NAME

4. 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:

Field

 

Value

 

Provider Type

 

Google

 

Consumer Key

 

Your Google Client ID

 

Consumer Secret

 

Your Google Client Secret

 

Authorize Endpoint URL

 

https://accounts.google.com/o/oauth2/auth?access_type=offline&prompt=consent

 

Default Scopes

 

openid email profile  https://www.googleapis.com/auth/calendar  https://www.googleapis.com/auth/calendar.events

 

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

Error

 

Cause

 

Solution

 

401 Unauthorized

 

Token expired, no refresh token

 

Re-authenticate with correct  Auth Provider settings (access_type=offline)

 

403 Forbidden

 

Missing scope or API not enabled

 

Enable Calendar API in Google  Cloud Console, verify scopes include calendar permissions

 

400 Bad Request

 

Invalid request body format

 

Check date format (ISO 8601),  required fields, valid email addresses for attendees

 

Missing id_token

 

Missing 'openid' scope

 

Add 'openid email profile' to  Default Scopes in Auth Provider

 

404 Not Found

 

Invalid calendar ID

 

Use 'primary' for the  authenticated user's main calendar, or verify the calendar ID  exists

 

 

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.


Alexander Zherebilo
Certified Salesforce Developer
image
Expertise
Question to the expert
image

We have available resources to start working on your project within 5 business days

1 UX Designer

image

1 Admin

image

2 QA engineers

image

1 Consultant

image
Related Articles
All articles
image
Is Salesforce Winning the Public Sector Race?
An analysis of Salesforce's rapid expansion into the U.S. public sector, tracing its path from cautious early government licensing deals in the 2010s through the launch of Government Cloud in 2012, its pivotal role in COVID-19 vaccine rollouts, and its 2025–2026 push into military and intelligence work via Agentforce and Missionforce. The piece covers major 2026 contracts — including a $5.6 billion Army deal, a $1.6 billion VA agreement, and Pentagon Impact Level 5 authorization — alongside real-world case studies like California's REAL ID processing and the UK's NHS back-office operations. It also examines the structural obstacles still facing Salesforce and other vendors in government tech: legacy IT systems decades old, outdated federal procurement rules, budget constraints, and organizational caution around AI adoption, plus the competitive pressure from Palantir, Microsoft, and Oracle in the race for public sector AI spending.
28 August 2026
image
Why Your Salesforce Flows Are Agentforce's Biggest Problem
This article argues that the most underestimated risk in Agentforce deployments isn't data quality — it's the automation layer: years of overlapping Flows, Process Builder processes, Apex triggers, and managed package logic that no one has reviewed end-to-end. It explains why AI agents inherit automation complexity without the tribal knowledge human admins carry, why technical debt only becomes visible after an agent hits it in production, and why a clean demo is no indicator of production readiness. The article closes with a concrete, tool-by-tool inventory approach using Flow Trigger Explorer, Salesforce Optimizer, Setup Audit Trail, Apex Debug Logs, Agent Builder, and Health Check — scoped to the specific processes the agent will actually use rather than the whole org.
23 July 2026
image
How to Wire Multiple Salesforce Projects in One Org Without Breaking Everything
This article maps the real integration patterns that emerge when multiple Salesforce projects — both managed packages and unpackaged code — share a single org. It covers four concrete patterns: attaching custom triggers to package-owned objects, calling global members exposed by managed packages, writing directly into another project's objects, and runtime-guarded reads of package data. It then addresses access control for authenticated and guest users, including the Master-Detail wall and the without sharing elevation pattern. The piece closes with eight concrete risks (compile-time dependencies that block uninstall, upgrade coupling, silent cascade failures, access invisible to admins) and six actionable recommendations for keeping cross-project coupling manageable.
08 July 2026