Google Workspace Integration for ChatGPT Apps: Enterprise Automation
Google Workspace powers over 9 million paying businesses worldwide, managing billions of emails, calendar events, and files daily. Integrating Google Workspace APIs with ChatGPT apps unlocks unprecedented automation potential—from intelligent email management and calendar scheduling to document generation and enterprise administration.
This comprehensive guide walks you through building production-ready Google Workspace integrations for ChatGPT apps using the OpenAI Apps SDK. You'll learn OAuth 2.0 authentication, Gmail API integration, Calendar API automation, Drive and Sheets operations, and enterprise administration with the Admin SDK.
Whether you're building an AI executive assistant that manages calendars, an automated email responder, or an enterprise data analyzer, this guide provides the complete technical foundation with 10 production-ready code examples in TypeScript and Python.
By the end, you'll have working implementations of OAuth authentication, email automation, calendar management, file operations, spreadsheet analysis, and user administration—all optimized for ChatGPT app deployment with the Model Context Protocol (MCP).
Google Cloud Project Setup
Before integrating Google Workspace APIs, you need a properly configured Google Cloud project with OAuth credentials and enabled APIs. This foundational setup determines your app's security posture and integration capabilities.
Creating Your Google Cloud Project
Every Google Workspace integration requires a Google Cloud project that acts as the container for API credentials, OAuth consent configuration, and service account keys. This project serves as your identity with Google's APIs and manages quota, billing, and access control.
Start by navigating to the Google Cloud Console and creating a new project. Choose a descriptive name like "ChatGPT Workspace Integration" and note the auto-generated project ID—you'll need this for service account authentication.
Once created, enable the required Google Workspace APIs through the API Library. For comprehensive integration, enable these APIs:
- Gmail API: Send, read, and manage emails programmatically
- Google Calendar API: Create events, check availability, manage calendars
- Google Drive API: Access, create, and manage files and folders
- Google Sheets API: Read and write spreadsheet data with formulas
- Admin SDK API: Manage users, groups, and organization settings (enterprise only)
- People API: Access contact information and user profiles
- Google Docs API: Create and edit documents programmatically
Navigate to "APIs & Services" > "Library" and search for each API, clicking "Enable" for those your ChatGPT app requires. Each API has different quota limits—Gmail allows 250 quota units per user per second, while Sheets permits 300 requests per minute per user.
Configuring OAuth 2.0 Credentials
Google Workspace APIs use OAuth 2.0 for user authorization. The OpenAI Apps SDK requires OAuth 2.1 with PKCE (Proof Key for Code Exchange) for enhanced security. Configure your OAuth consent screen and create credentials:
OAuth Consent Screen Configuration:
- Go to "APIs & Services" > "OAuth consent screen"
- Choose "External" for public apps or "Internal" for Google Workspace organizations
- Fill required fields:
- App name: Descriptive name shown to users during consent
- User support email: Contact email for user questions
- Developer contact: Your email for Google communications
- App logo: 120x120px PNG (optional but recommended)
- Add scopes based on your integration needs (see specific scopes below)
- Add test users during development (required for "External" apps in testing mode—max 100 test users)
Creating OAuth 2.0 Credentials:
- Navigate to "APIs & Services" > "Credentials"
- Click "Create Credentials" > "OAuth client ID"
- Select "Web application"
- Add authorized redirect URIs for ChatGPT:
- Production:
https://chatgpt.com/connector_platform_oauth_redirect - Review/Testing:
https://platform.openai.com/apps-manage/oauth
- Production:
- Save your Client ID and Client Secret securely—never expose these in client-side code
For the ChatGPT Apps SDK, you must publish OAuth metadata at .well-known/oauth-authorization-server:
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://oauth2.googleapis.com/token",
"revocation_endpoint": "https://oauth2.googleapis.com/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": [
"https://www.googleapis.com/auth/gmail.modify",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/spreadsheets"
]
}
Service Account Setup for Server-to-Server Automation
For server-to-server automation (like scheduled reports, batch operations, or background tasks), create a service account that can operate without user interaction:
Creating a Service Account:
- Go to "IAM & Admin" > "Service Accounts"
- Click "Create Service Account"
- Name it descriptively (e.g., "chatgpt-workspace-automation")
- Grant necessary roles:
- Service Account Token Creator: Generate access tokens
- Service Account Key Admin: Manage keys (optional)
- Create and download a JSON key file
- CRITICAL: Store this key in
.vault/directory and NEVER commit to version control
Domain-Wide Delegation (Enterprise Only):
For accessing user data without individual OAuth consent, enable domain-wide delegation:
- In Google Workspace Admin Console, go to Security > API Controls > Domain-wide Delegation
- Add your service account's Client ID
- Authorize scopes:
https://www.googleapis.com/auth/gmail.readonlyhttps://www.googleapis.com/auth/calendarhttps://www.googleapis.com/auth/admin.directory.user.readonly
- Service accounts can now impersonate users:
subject: 'user@yourdomain.com'
Security Best Practices:
- Rotate service account keys every 90 days
- Use separate service accounts for different environments (dev/staging/production)
- Grant minimum necessary scopes (principle of least privilege)
- Monitor service account activity in Cloud Audit Logs
Learn more about building ChatGPT apps with the OpenAI Apps SDK and enterprise authentication patterns.
Gmail API Integration
Gmail API enables your ChatGPT app to send emails, read messages, manage labels, and automate email workflows—all through conversational interfaces. This transforms ChatGPT into an intelligent email assistant capable of drafting responses, categorizing messages, and executing complex email automations.
OAuth Scopes for Gmail
Request only the scopes your app needs. Gmail provides granular scope control for security:
https://www.googleapis.com/auth/gmail.send: Send emails only (read-only for contacts)https://www.googleapis.com/auth/gmail.readonly: Read messages, labels, and settingshttps://www.googleapis.com/auth/gmail.modify: Read, modify, send, and delete (most common for assistants)https://www.googleapis.com/auth/gmail.compose: Create and send draftshttps://mail.google.com/: Full Gmail access (use sparingly—triggers Google review)
For most ChatGPT email assistants, use gmail.modify which balances functionality with security.
Gmail API Client Implementation
Here's a production-ready Gmail API client with comprehensive email operations, thread management, and attachment handling:
// gmail-client.ts
import { google, gmail_v1 } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
export interface GmailMessage {
id: string;
threadId: string;
subject: string;
from: string;
to: string[];
cc?: string[];
date: Date;
snippet: string;
body?: string;
labels: string[];
attachments?: Array<{ filename: string; mimeType: string; size: number; attachmentId: string }>;
}
export interface SendEmailOptions {
to: string | string[];
subject: string;
body: string;
cc?: string | string[];
bcc?: string | string[];
attachments?: Array<{ filename: string; content: Buffer; mimeType: string }>;
inReplyTo?: string;
threadId?: string;
isHtml?: boolean;
}
export class GmailClient {
private gmail: gmail_v1.Gmail;
constructor(private auth: OAuth2Client) {
this.gmail = google.gmail({ version: 'v1', auth });
}
/**
* Send an email with optional attachments and threading
* Supports HTML/plain text, CC/BCC, and inline thread replies
*/
async sendEmail(options: SendEmailOptions): Promise<{ id: string; threadId: string }> {
const {
to, subject, body, cc, bcc, attachments = [],
inReplyTo, threadId, isHtml = true
} = options;
const boundary = '----=_Part_' + Date.now() + Math.random().toString(36);
const recipients = Array.isArray(to) ? to.join(', ') : to;
const ccRecipients = cc ? (Array.isArray(cc) ? cc.join(', ') : cc) : undefined;
const bccRecipients = bcc ? (Array.isArray(bcc) ? bcc.join(', ') : bcc) : undefined;
// Build email headers
let headers = [
`To: ${recipients}`,
...(ccRecipients ? [`Cc: ${ccRecipients}`] : []),
...(bccRecipients ? [`Bcc: ${bccRecipients}`] : []),
`Subject: ${subject}`,
'MIME-Version: 1.0',
...(inReplyTo ? [`In-Reply-To: ${inReplyTo}`, `References: ${inReplyTo}`] : [])
];
let emailBody: string;
if (attachments.length > 0) {
// Multipart email with attachments
headers.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
const contentType = isHtml ? 'text/html; charset=UTF-8' : 'text/plain; charset=UTF-8';
emailBody = [
...headers,
'',
`--${boundary}`,
`Content-Type: ${contentType}`,
'Content-Transfer-Encoding: quoted-printable',
'',
body,
'',
...attachments.flatMap(att => [
`--${boundary}`,
`Content-Type: ${att.mimeType}; name="${att.filename}"`,
`Content-Disposition: attachment; filename="${att.filename}"`,
'Content-Transfer-Encoding: base64',
'',
att.content.toString('base64'),
''
]),
`--${boundary}--`
].join('\r\n');
} else {
// Simple HTML/plain text email
const contentType = isHtml ? 'text/html; charset=UTF-8' : 'text/plain; charset=UTF-8';
headers.push(`Content-Type: ${contentType}`);
emailBody = [...headers, '', body].join('\r\n');
}
// Encode email in base64url format (required by Gmail API)
const encodedMessage = Buffer.from(emailBody)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
const response = await this.gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: encodedMessage,
threadId: threadId
}
});
return {
id: response.data.id!,
threadId: response.data.threadId!
};
}
/**
* Fetch messages with advanced filtering
* Supports Gmail query syntax: "from:john@example.com is:unread"
*/
async getMessages(options: {
maxResults?: number;
query?: string;
labelIds?: string[];
includeBody?: boolean;
}): Promise<GmailMessage[]> {
const { maxResults = 10, query, labelIds, includeBody = false } = options;
const response = await this.gmail.users.messages.list({
userId: 'me',
maxResults,
q: query,
labelIds
});
if (!response.data.messages) {
return [];
}
// Fetch full message details in parallel (batch API calls)
const messages = await Promise.all(
response.data.messages.map(msg => this.getMessage(msg.id!, includeBody))
);
return messages.filter((m): m is GmailMessage => m !== null);
}
/**
* Get a single message by ID with optional body extraction
*/
async getMessage(messageId: string, includeBody = false): Promise<GmailMessage | null> {
try {
const response = await this.gmail.users.messages.get({
userId: 'me',
id: messageId,
format: includeBody ? 'full' : 'metadata',
metadataHeaders: includeBody ? undefined : ['Subject', 'From', 'To', 'Cc', 'Date']
});
const message = response.data;
const headers = message.payload?.headers || [];
const getHeader = (name: string) =>
headers.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value || '';
const subject = getHeader('subject');
const from = getHeader('from');
const to = getHeader('to').split(',').map(t => t.trim()).filter(Boolean);
const cc = getHeader('cc').split(',').map(c => c.trim()).filter(Boolean);
const dateStr = getHeader('date');
let body: string | undefined;
let attachments: GmailMessage['attachments'];
if (includeBody && message.payload) {
// Extract body from multipart message
const extractBody = (parts: gmail_v1.Schema$MessagePart[]): string => {
for (const part of parts) {
if (part.mimeType === 'text/html' && part.body?.data) {
return Buffer.from(part.body.data, 'base64').toString('utf-8');
}
if (part.mimeType === 'text/plain' && part.body?.data && !body) {
return Buffer.from(part.body.data, 'base64').toString('utf-8');
}
if (part.parts) {
const nestedBody = extractBody(part.parts);
if (nestedBody) return nestedBody;
}
}
return '';
};
if (message.payload.body?.data) {
body = Buffer.from(message.payload.body.data, 'base64').toString('utf-8');
} else if (message.payload.parts) {
body = extractBody(message.payload.parts);
}
// Extract attachments metadata
const extractAttachments = (parts: gmail_v1.Schema$MessagePart[]): typeof attachments => {
const atts: NonNullable<typeof attachments> = [];
for (const part of parts) {
if (part.filename && part.body?.attachmentId) {
atts.push({
filename: part.filename,
mimeType: part.mimeType || 'application/octet-stream',
size: part.body.size || 0,
attachmentId: part.body.attachmentId
});
}
if (part.parts) {
atts.push(...extractAttachments(part.parts));
}
}
return atts;
};
if (message.payload.parts) {
attachments = extractAttachments(message.payload.parts);
}
}
return {
id: message.id!,
threadId: message.threadId!,
subject,
from,
to,
cc: cc.length > 0 ? cc : undefined,
date: new Date(dateStr),
snippet: message.snippet || '',
body,
labels: message.labelIds || [],
attachments
};
} catch (error) {
console.error(`Failed to fetch message ${messageId}:`, error);
return null;
}
}
/**
* Download attachment by attachment ID
*/
async getAttachment(messageId: string, attachmentId: string): Promise<Buffer> {
const response = await this.gmail.users.messages.attachments.get({
userId: 'me',
messageId,
id: attachmentId
});
const data = response.data.data!;
return Buffer.from(data, 'base64');
}
/**
* Manage labels (add, remove, create)
*/
async modifyLabels(messageId: string, addLabels: string[], removeLabels: string[]) {
await this.gmail.users.messages.modify({
userId: 'me',
id: messageId,
requestBody: {
addLabelIds: addLabels,
removeLabelIds: removeLabels
}
});
}
async createLabel(name: string, color?: { backgroundColor: string; textColor: string }) {
const response = await this.gmail.users.labels.create({
userId: 'me',
requestBody: {
name,
labelListVisibility: 'labelShow',
messageListVisibility: 'show',
color
}
});
return response.data.id!;
}
async listLabels(): Promise<Array<{ id: string; name: string; type: string }>> {
const response = await this.gmail.users.labels.list({ userId: 'me' });
return (response.data.labels || []).map(label => ({
id: label.id!,
name: label.name!,
type: label.type!
}));
}
/**
* Search emails with Gmail query syntax
* Examples: "from:john@example.com", "has:attachment", "is:unread after:2026/01/01"
*/
async searchEmails(query: string, maxResults = 20): Promise<GmailMessage[]> {
return this.getMessages({ query, maxResults, includeBody: true });
}
/**
* Mark messages as read/unread
*/
async markAsRead(messageIds: string[]): Promise<void> {
await Promise.all(
messageIds.map(id => this.modifyLabels(id, [], ['UNREAD']))
);
}
async markAsUnread(messageIds: string[]): Promise<void> {
await Promise.all(
messageIds.map(id => this.modifyLabels(id, ['UNREAD'], []))
);
}
/**
* Delete messages (move to trash)
*/
async deleteMessages(messageIds: string[]): Promise<void> {
await Promise.all(
messageIds.map(id => this.gmail.users.messages.trash({ userId: 'me', id }))
);
}
}
Email Automation MCP Tool Examples
Use the Gmail client in your ChatGPT app MCP server:
// Example: AI email assistant tool
import { GmailClient } from './gmail-client';
server.tool({
name: 'send_email',
description: 'Send an email via Gmail with optional attachments and threading',
parameters: {
type: 'object',
properties: {
to: { type: 'string', description: 'Recipient email address or comma-separated list' },
subject: { type: 'string', description: 'Email subject line' },
body: { type: 'string', description: 'Email body (supports HTML)' },
cc: { type: 'string', description: 'CC recipients (optional)' },
inReplyTo: { type: 'string', description: 'Message ID to reply to (for threading)' },
attachDriveFiles: {
type: 'array',
items: { type: 'string' },
description: 'Drive file IDs to attach (optional)'
}
},
required: ['to', 'subject', 'body']
}
}, async ({ to, subject, body, cc, inReplyTo, attachDriveFiles = [] }, { token }) => {
const oauth2Client = createOAuthClient(token);
const gmailClient = new GmailClient(oauth2Client);
// If attachments requested, fetch from Drive
const attachments = await Promise.all(
attachDriveFiles.map(async fileId => {
const driveClient = new DriveClient(oauth2Client);
return await driveClient.downloadFile(fileId);
})
);
const result = await gmailClient.sendEmail({
to,
subject,
body: `<html><body>${body}</body></html>`,
cc,
inReplyTo,
attachments,
isHtml: true
});
return {
content: `✅ Email sent successfully!\n\nMessage ID: ${result.id}\nThread ID: ${result.threadId}`,
structuredContent: {
type: 'email_sent',
status: 'success',
messageId: result.id,
threadId: result.threadId,
timestamp: new Date().toISOString()
},
_meta: {
mimeType: 'text/html+skybridge',
displayMode: 'inline'
}
};
});
server.tool({
name: 'search_emails',
description: 'Search Gmail using advanced query syntax',
parameters: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Gmail search query (e.g., "from:john@example.com is:unread after:2026/01/01")'
},
maxResults: { type: 'number', description: 'Maximum results (default: 20)', default: 20 }
},
required: ['query']
}
}, async ({ query, maxResults }, { token }) => {
const oauth2Client = createOAuthClient(token);
const gmailClient = new GmailClient(oauth2Client);
const emails = await gmailClient.searchEmails(query, maxResults);
return {
content: `Found ${emails.length} emails matching "${query}"`,
structuredContent: {
type: 'email_list',
query,
count: emails.length,
emails: emails.map(e => ({
id: e.id,
subject: e.subject,
from: e.from,
date: e.date.toISOString(),
snippet: e.snippet
}))
}
};
});
Explore more email automation patterns for ChatGPT apps and Gmail API best practices.
Google Calendar API Integration
Google Calendar API enables intelligent scheduling, availability checking, event creation, and calendar management through conversational interfaces. This transforms ChatGPT into an AI executive assistant that can coordinate meetings across time zones, find optimal meeting slots, and manage complex scheduling workflows.
OAuth Scopes for Calendar
Common Calendar API scopes:
https://www.googleapis.com/auth/calendar: Full calendar access (read/write all calendars)https://www.googleapis.com/auth/calendar.readonly: Read-only access to all calendarshttps://www.googleapis.com/auth/calendar.events: Manage events only (most common for assistants)https://www.googleapis.com/auth/calendar.events.readonly: Read events only
For AI scheduling assistants, use calendar scope for full event management including Google Meet creation.
Calendar Event Manager Implementation
Production-ready Calendar API client with scheduling intelligence and conflict detection:
// calendar-client.ts
import { google, calendar_v3 } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
export interface CalendarEvent {
id: string;
summary: string;
description?: string;
start: Date;
end: Date;
attendees?: Array<{ email: string; responseStatus?: string }>;
location?: string;
conferenceData?: {
type: 'hangoutsMeet' | 'zoom';
url: string;
entryPoints?: Array<{ uri: string; label?: string }>;
};
recurrence?: string[];
reminders?: Array<{ method: 'email' | 'popup'; minutes: number }>;
status?: 'confirmed' | 'tentative' | 'cancelled';
}
export class CalendarClient {
private calendar: calendar_v3.Calendar;
constructor(private auth: OAuth2Client) {
this.calendar = google.calendar({ version: 'v3', auth });
}
/**
* Create a calendar event with optional Google Meet
* Supports recurring events, custom reminders, and attendee management
*/
async createEvent(options: {
summary: string;
description?: string;
start: Date;
end: Date;
attendees?: string[];
location?: string;
addMeet?: boolean;
recurrence?: string[];
reminders?: Array<{ method: 'email' | 'popup'; minutes: number }>;
calendarId?: string;
sendUpdates?: boolean;
}): Promise<CalendarEvent> {
const {
summary,
description,
start,
end,
attendees = [],
location,
addMeet = false,
recurrence,
reminders,
calendarId = 'primary',
sendUpdates = true
} = options;
const eventRequest: calendar_v3.Schema$Event = {
summary,
description,
start: { dateTime: start.toISOString(), timeZone: 'UTC' },
end: { dateTime: end.toISOString(), timeZone: 'UTC' },
attendees: attendees.map(email => ({ email })),
location,
recurrence,
reminders: reminders
? {
useDefault: false,
overrides: reminders
}
: { useDefault: true }
};
// Add Google Meet if requested
if (addMeet) {
eventRequest.conferenceData = {
createRequest: {
requestId: `meet-${Date.now()}`,
conferenceSolutionKey: { type: 'hangoutsMeet' }
}
};
}
const response = await this.calendar.events.insert({
calendarId,
requestBody: eventRequest,
conferenceDataVersion: addMeet ? 1 : undefined,
sendUpdates: sendUpdates ? 'all' : 'none' // Send email invitations to attendees
});
return this.mapEventToCalendarEvent(response.data);
}
/**
* Find available time slots across multiple calendars
* Implements intelligent scheduling with working hours and time zone support
*/
async findAvailableSlots(options: {
emails: string[];
duration: number; // minutes
timeMin: Date;
timeMax: Date;
workingHoursOnly?: boolean;
workingHours?: { start: number; end: number }; // 24-hour format (default: 9-17)
timeZone?: string;
maxResults?: number;
}): Promise<Array<{ start: Date; end: Date; conflicts: number }>> {
const {
emails,
duration,
timeMin,
timeMax,
workingHoursOnly = true,
workingHours = { start: 9, end: 17 },
timeZone = 'UTC',
maxResults = 10
} = options;
const response = await this.calendar.freebusy.query({
requestBody: {
timeMin: timeMin.toISOString(),
timeMax: timeMax.toISOString(),
timeZone,
items: emails.map(email => ({ id: email }))
}
});
const busySlots: Array<{ start: Date; end: Date }> = [];
// Aggregate all busy periods
for (const calendar of Object.values(response.data.calendars || {})) {
if (calendar.busy) {
busySlots.push(
...calendar.busy.map(slot => ({
start: new Date(slot.start!),
end: new Date(slot.end!)
}))
);
}
}
// Sort busy slots by start time
busySlots.sort((a, b) => a.start.getTime() - b.start.getTime());
// Find gaps between busy slots
const availableSlots: Array<{ start: Date; end: Date; conflicts: number }> = [];
let currentTime = new Date(timeMin);
const durationMs = duration * 60 * 1000;
while (currentTime < timeMax && availableSlots.length < maxResults) {
// Check if current time is within working hours
if (workingHoursOnly) {
const hour = currentTime.getUTCHours();
if (hour < workingHours.start || hour >= workingHours.end) {
// Skip to next working hour
const nextDay = new Date(currentTime);
nextDay.setUTCHours(workingHours.start, 0, 0, 0);
if (nextDay <= currentTime) {
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
}
currentTime = nextDay;
continue;
}
}
const slotEnd = new Date(currentTime.getTime() + durationMs);
// Check for conflicts
const conflicts = busySlots.filter(busy =>
(currentTime >= busy.start && currentTime < busy.end) ||
(slotEnd > busy.start && slotEnd <= busy.end) ||
(currentTime <= busy.start && slotEnd >= busy.end)
).length;
if (conflicts === 0) {
availableSlots.push({
start: new Date(currentTime),
end: slotEnd,
conflicts: 0
});
// Jump to end of this slot to find next available
currentTime = slotEnd;
} else {
// Find next potential start time (after current conflicts)
const nextBusyEnd = busySlots
.filter(b => b.end > currentTime)
.sort((a, b) => a.end.getTime() - b.end.getTime())[0];
if (nextBusyEnd) {
currentTime = nextBusyEnd.end;
} else {
currentTime = new Date(currentTime.getTime() + 30 * 60 * 1000); // 30-minute increment
}
}
}
return availableSlots.slice(0, maxResults);
}
/**
* Get upcoming events with filtering
*/
async getUpcomingEvents(options: {
maxResults?: number;
calendarId?: string;
timeMin?: Date;
timeMax?: Date;
query?: string;
}): Promise<CalendarEvent[]> {
const {
maxResults = 10,
calendarId = 'primary',
timeMin = new Date(),
timeMax,
query
} = options;
const response = await this.calendar.events.list({
calendarId,
timeMin: timeMin.toISOString(),
timeMax: timeMax?.toISOString(),
maxResults,
singleEvents: true,
orderBy: 'startTime',
q: query
});
return (response.data.items || []).map(event => this.mapEventToCalendarEvent(event));
}
/**
* Update an existing event
*/
async updateEvent(
eventId: string,
updates: Partial<CalendarEvent>,
calendarId = 'primary',
sendUpdates = true
): Promise<CalendarEvent> {
const eventRequest: calendar_v3.Schema$Event = {};
if (updates.summary) eventRequest.summary = updates.summary;
if (updates.description) eventRequest.description = updates.description;
if (updates.start) eventRequest.start = { dateTime: updates.start.toISOString() };
if (updates.end) eventRequest.end = { dateTime: updates.end.toISOString() };
if (updates.attendees) {
eventRequest.attendees = updates.attendees.map(a => ({ email: a.email }));
}
if (updates.location) eventRequest.location = updates.location;
const response = await this.calendar.events.patch({
calendarId,
eventId,
requestBody: eventRequest,
sendUpdates: sendUpdates ? 'all' : 'none'
});
return this.mapEventToCalendarEvent(response.data);
}
/**
* Delete an event
*/
async deleteEvent(eventId: string, calendarId = 'primary', sendUpdates = true): Promise<void> {
await this.calendar.events.delete({
calendarId,
eventId,
sendUpdates: sendUpdates ? 'all' : 'none'
});
}
/**
* List all accessible calendars
*/
async listCalendars(): Promise<Array<{ id: string; summary: string; primary: boolean }>> {
const response = await this.calendar.calendarList.list();
return (response.data.items || []).map(cal => ({
id: cal.id!,
summary: cal.summary!,
primary: cal.primary || false
}));
}
private mapEventToCalendarEvent(event: calendar_v3.Schema$Event): CalendarEvent {
const conferenceData = event.conferenceData?.entryPoints?.find(ep => ep.entryPointType === 'video');
return {
id: event.id!,
summary: event.summary || 'Untitled Event',
description: event.description,
start: new Date(event.start?.dateTime || event.start?.date || ''),
end: new Date(event.end?.dateTime || event.end?.date || ''),
attendees: event.attendees?.map(a => ({
email: a.email!,
responseStatus: a.responseStatus
})),
location: event.location,
conferenceData: conferenceData
? {
type: 'hangoutsMeet',
url: conferenceData.uri!,
entryPoints: event.conferenceData?.entryPoints?.map(ep => ({
uri: ep.uri!,
label: ep.label
}))
}
: undefined,
recurrence: event.recurrence,
reminders: event.reminders?.overrides?.map(r => ({
method: r.method as 'email' | 'popup',
minutes: r.minutes!
})),
status: event.status as 'confirmed' | 'tentative' | 'cancelled'
};
}
}
Intelligent Scheduling MCP Tool
Implement a ChatGPT scheduling assistant with automatic conflict detection:
server.tool({
name: 'schedule_meeting',
description: 'Schedule a meeting with automatic availability checking and Google Meet creation',
parameters: {
type: 'object',
properties: {
attendees: {
type: 'array',
items: { type: 'string' },
description: 'Email addresses of attendees'
},
duration: { type: 'number', description: 'Meeting duration in minutes' },
preferredDate: {
type: 'string',
description: 'Preferred date (YYYY-MM-DD) or leave empty for next 7 days'
},
title: { type: 'string', description: 'Meeting title/subject' },
description: { type: 'string', description: 'Meeting description/agenda' },
addGoogleMeet: { type: 'boolean', description: 'Add Google Meet link', default: true }
},
required: ['attendees', 'duration', 'title']
}
}, async ({ attendees, duration, preferredDate, title, description, addGoogleMeet }, { token }) => {
const oauth2Client = createOAuthClient(token);
const calendarClient = new CalendarClient(oauth2Client);
// Find available slots
const timeMin = preferredDate ? new Date(preferredDate) : new Date();
const timeMax = new Date(timeMin.getTime() + 7 * 24 * 60 * 60 * 1000); // Next 7 days
const availableSlots = await calendarClient.findAvailableSlots({
emails: attendees,
duration,
timeMin,
timeMax,
workingHoursOnly: true,
maxResults: 5
});
if (availableSlots.length === 0) {
return {
content: '❌ No available slots found in the next 7 days for all attendees.',
structuredContent: {
type: 'scheduling_error',
message: 'No availability',
attendees,
searchRange: { start: timeMin.toISOString(), end: timeMax.toISOString() }
}
};
}
// Create event in first available slot
const slot = availableSlots[0];
const event = await calendarClient.createEvent({
summary: title,
description,
start: slot.start,
end: slot.end,
attendees,
addMeet: addGoogleMeet,
sendUpdates: true
});
const alternativeSlots = availableSlots.slice(1, 4).map(s => ({
start: s.start.toISOString(),
end: s.end.toISOString()
}));
return {
content: `✅ Meeting scheduled!\n\n**${event.summary}**\n📅 ${event.start.toLocaleString()}\n👥 ${attendees.length} attendees\n${event.conferenceData ? `🔗 Google Meet: ${event.conferenceData.url}` : ''}`,
structuredContent: {
type: 'meeting_scheduled',
event: {
id: event.id,
title: event.summary,
start: event.start.toISOString(),
end: event.end.toISOString(),
attendees: event.attendees?.map(a => a.email),
meetLink: event.conferenceData?.url,
alternativeSlots
}
},
_meta: {
mimeType: 'text/html+skybridge',
displayMode: 'inline'
}
};
});
Learn more about AI scheduling assistants and calendar automation best practices.
Google Drive & Sheets APIs
Drive and Sheets APIs enable file management, document generation, and spreadsheet automation—essential for data-driven ChatGPT apps that analyze business metrics, generate reports, and manage knowledge bases.
Drive API Client Implementation
Production-ready Drive client with advanced file operations:
// drive-client.ts
import { google, drive_v3 } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import { Readable } from 'stream';
export interface DriveFile {
id: string;
name: string;
mimeType: string;
size?: number;
createdTime: Date;
modifiedTime: Date;
webViewLink?: string;
thumbnailLink?: string;
parents?: string[];
}
export class DriveClient {
private drive: drive_v3.Drive;
constructor(private auth: OAuth2Client) {
this.drive = google.drive({ version: 'v3', auth });
}
/**
* Upload a file to Google Drive with metadata
*/
async uploadFile(options: {
name: string;
content: Buffer | Readable;
mimeType: string;
folderId?: string;
description?: string;
}): Promise<DriveFile> {
const { name, content, mimeType, folderId, description } = options;
const response = await this.drive.files.create({
requestBody: {
name,
description,
parents: folderId ? [folderId] : undefined,
mimeType
},
media: {
mimeType,
body: content instanceof Buffer ? Readable.from([content]) : content
},
fields: 'id, name, mimeType, size, createdTime, modifiedTime, webViewLink, thumbnailLink'
});
return this.mapDriveFile(response.data);
}
/**
* Download a file from Drive
*/
async downloadFile(fileId: string): Promise<{
filename: string;
content: Buffer;
mimeType: string
}> {
const metaResponse = await this.drive.files.get({
fileId,
fields: 'name, mimeType'
});
const response = await this.drive.files.get(
{ fileId, alt: 'media' },
{ responseType: 'arraybuffer' }
);
return {
filename: metaResponse.data.name!,
content: Buffer.from(response.data as ArrayBuffer),
mimeType: metaResponse.data.mimeType!
};
}
/**
* List files with advanced filtering
*/
async listFiles(options: {
query?: string;
maxResults?: number;
folderId?: string;
orderBy?: string;
}): Promise<DriveFile[]> {
const { query, maxResults = 20, folderId, orderBy = 'modifiedTime desc' } = options;
let q = query || '';
if (folderId) {
q += (q ? ' and ' : '') + `'${folderId}' in parents`;
}
q += (q ? ' and ' : '') + 'trashed = false';
const response = await this.drive.files.list({
q: q || undefined,
pageSize: maxResults,
orderBy,
fields: 'files(id, name, mimeType, size, createdTime, modifiedTime, webViewLink, thumbnailLink, parents)'
});
return (response.data.files || []).map(file => this.mapDriveFile(file));
}
/**
* Search files by name or content
*/
async searchFiles(searchTerm: string, maxResults = 20): Promise<DriveFile[]> {
const query = `fullText contains '${searchTerm}' or name contains '${searchTerm}'`;
return this.listFiles({ query, maxResults });
}
/**
* Share a file with permissions
*/
async shareFile(
fileId: string,
email: string,
role: 'reader' | 'writer' | 'commenter' = 'reader',
sendNotification = true
): Promise<void> {
await this.drive.permissions.create({
fileId,
requestBody: {
type: 'user',
role,
emailAddress: email
},
sendNotificationEmail: sendNotification
});
}
/**
* Create a folder
*/
async createFolder(name: string, parentId?: string): Promise<string> {
const response = await this.drive.files.create({
requestBody: {
name,
mimeType: 'application/vnd.google-apps.folder',
parents: parentId ? [parentId] : undefined
},
fields: 'id'
});
return response.data.id!;
}
/**
* Move file to folder
*/
async moveFile(fileId: string, newParentId: string): Promise<void> {
const file = await this.drive.files.get({
fileId,
fields: 'parents'
});
const previousParents = file.data.parents?.join(',');
await this.drive.files.update({
fileId,
addParents: newParentId,
removeParents: previousParents,
fields: 'id, parents'
});
}
/**
* Delete file (move to trash)
*/
async deleteFile(fileId: string): Promise<void> {
await this.drive.files.delete({ fileId });
}
private mapDriveFile(file: drive_v3.Schema$File): DriveFile {
return {
id: file.id!,
name: file.name!,
mimeType: file.mimeType!,
size: file.size ? parseInt(file.size) : undefined,
createdTime: new Date(file.createdTime!),
modifiedTime: new Date(file.modifiedTime!),
webViewLink: file.webViewLink,
thumbnailLink: file.thumbnailLink,
parents: file.parents
};
}
}
Sheets API Client Implementation
Production-ready Sheets client with batch operations and formula support:
// sheets-client.ts
import { google, sheets_v4 } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
export class SheetsClient {
private sheets: sheets_v4.Sheets;
constructor(private auth: OAuth2Client) {
this.sheets = google.sheets({ version: 'v4', auth });
}
/**
* Read data from a spreadsheet range
*/
async readRange(spreadsheetId: string, range: string): Promise<any[][]> {
const response = await this.sheets.spreadsheets.values.get({
spreadsheetId,
range,
valueRenderOption: 'FORMATTED_VALUE'
});
return response.data.values || [];
}
/**
* Write data to a spreadsheet range
*/
async writeRange(
spreadsheetId: string,
range: string,
values: any[][],
inputOption: 'RAW' | 'USER_ENTERED' = 'USER_ENTERED'
): Promise<{ updatedCells: number; updatedRows: number }> {
const response = await this.sheets.spreadsheets.values.update({
spreadsheetId,
range,
valueInputOption: inputOption,
requestBody: { values }
});
return {
updatedCells: response.data.updatedCells || 0,
updatedRows: response.data.updatedRows || 0
};
}
/**
* Append data to the end of a sheet
*/
async appendData(
spreadsheetId: string,
range: string,
values: any[][]
): Promise<{ updatedRange: string; updatedRows: number }> {
const response = await this.sheets.spreadsheets.values.append({
spreadsheetId,
range,
valueInputOption: 'USER_ENTERED',
insertDataOption: 'INSERT_ROWS',
requestBody: { values }
});
return {
updatedRange: response.data.updates?.updatedRange || '',
updatedRows: response.data.updates?.updatedRows || 0
};
}
/**
* Create a new spreadsheet
*/
async createSpreadsheet(
title: string,
sheets: Array<{ title: string; rows?: number; cols?: number }>
): Promise<{ id: string; url: string }> {
const response = await this.sheets.spreadsheets.create({
requestBody: {
properties: { title },
sheets: sheets.map(sheet => ({
properties: {
title: sheet.title,
gridProperties: {
rowCount: sheet.rows || 1000,
columnCount: sheet.cols || 26
}
}
}))
}
});
return {
id: response.data.spreadsheetId!,
url: response.data.spreadsheetUrl!
};
}
/**
* Batch read multiple ranges efficiently
*/
async batchRead(spreadsheetId: string, ranges: string[]): Promise<Record<string, any[][]>> {
const response = await this.sheets.spreadsheets.values.batchGet({
spreadsheetId,
ranges
});
const result: Record<string, any[][]> = {};
response.data.valueRanges?.forEach((vr, index) => {
result[ranges[index]] = vr.values || [];
});
return result;
}
/**
* Batch write multiple ranges efficiently
*/
async batchWrite(
spreadsheetId: string,
data: Array<{ range: string; values: any[][] }>
): Promise<{ totalUpdatedCells: number }> {
const response = await this.sheets.spreadsheets.values.batchUpdate({
spreadsheetId,
requestBody: {
valueInputOption: 'USER_ENTERED',
data: data.map(d => ({ range: d.range, values: d.values }))
}
});
return {
totalUpdatedCells: response.data.totalUpdatedCells || 0
};
}
/**
* Format cells (bold headers, colors, number formats)
*/
async formatCells(
spreadsheetId: string,
requests: sheets_v4.Schema$Request[]
): Promise<void> {
await this.sheets.spreadsheets.batchUpdate({
spreadsheetId,
requestBody: { requests }
});
}
/**
* Clear range
*/
async clearRange(spreadsheetId: string, range: string): Promise<void> {
await this.sheets.spreadsheets.values.clear({
spreadsheetId,
range
});
}
}
Discover more about file automation with ChatGPT apps and spreadsheet AI assistants.
Enterprise Administration with Admin SDK
Google Workspace Admin SDK enables user management, group administration, and organizational control—essential for enterprise ChatGPT apps that automate onboarding, access control, and compliance reporting.
Admin SDK User Manager
Production-ready Admin SDK client for enterprise user management:
// admin-client.ts
import { google, admin_directory_v1 } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
export interface WorkspaceUser {
id: string;
email: string;
name: { fullName: string; givenName: string; familyName: string };
suspended: boolean;
orgUnitPath: string;
creationTime: Date;
lastLoginTime?: Date;
isAdmin: boolean;
}
export class AdminClient {
private admin: admin_directory_v1.Admin;
constructor(private auth: OAuth2Client) {
this.admin = google.admin({ version: 'directory_v1', auth });
}
/**
* List users in the organization with filtering
*/
async listUsers(options: {
domain: string;
maxResults?: number;
query?: string;
orderBy?: 'email' | 'givenName' | 'familyName';
}): Promise<WorkspaceUser[]> {
const { domain, maxResults = 100, query, orderBy = 'email' } = options;
const response = await this.admin.users.list({
domain,
maxResults,
query,
orderBy,
projection: 'full'
});
return (response.data.users || []).map(user => ({
id: user.id!,
email: user.primaryEmail!,
name: {
fullName: user.name?.fullName!,
givenName: user.name?.givenName!,
familyName: user.name?.familyName!
},
suspended: user.suspended || false,
orgUnitPath: user.orgUnitPath!,
creationTime: new Date(user.creationTime!),
lastLoginTime: user.lastLoginTime ? new Date(user.lastLoginTime) : undefined,
isAdmin: user.isAdmin || false
}));
}
/**
* Create a new user
*/
async createUser(options: {
email: string;
firstName: string;
lastName: string;
password: string;
orgUnitPath?: string;
changePasswordAtNextLogin?: boolean;
}): Promise<string> {
const {
email, firstName, lastName, password,
orgUnitPath = '/',
changePasswordAtNextLogin = true
} = options;
const response = await this.admin.users.insert({
requestBody: {
primaryEmail: email,
name: {
givenName: firstName,
familyName: lastName
},
password,
orgUnitPath,
changePasswordAtNextLogin
}
});
return response.data.id!;
}
/**
* Update user properties
*/
async updateUser(
userId: string,
updates: {
suspended?: boolean;
orgUnitPath?: string;
password?: string;
}
): Promise<void> {
await this.admin.users.update({
userKey: userId,
requestBody: updates
});
}
/**
* Suspend/unsuspend user
*/
async suspendUser(userId: string, suspend = true): Promise<void> {
await this.updateUser(userId, { suspended: suspend });
}
/**
* Delete user
*/
async deleteUser(userId: string): Promise<void> {
await this.admin.users.delete({ userKey: userId });
}
/**
* List groups
*/
async listGroups(domain: string): Promise<Array<{
id: string;
email: string;
name: string;
memberCount: number;
}>> {
const response = await this.admin.groups.list({ domain });
return (response.data.groups || []).map(group => ({
id: group.id!,
email: group.email!,
name: group.name!,
memberCount: parseInt(group.directMembersCount || '0')
}));
}
/**
* Add user to group
*/
async addUserToGroup(
groupId: string,
userEmail: string,
role: 'MEMBER' | 'MANAGER' | 'OWNER' = 'MEMBER'
): Promise<void> {
await this.admin.members.insert({
groupKey: groupId,
requestBody: {
email: userEmail,
role
}
});
}
/**
* Remove user from group
*/
async removeUserFromGroup(groupId: string, userEmail: string): Promise<void> {
await this.admin.members.delete({
groupKey: groupId,
memberKey: userEmail
});
}
/**
* List group members
*/
async listGroupMembers(groupId: string): Promise<Array<{
email: string;
role: string;
status: string;
}>> {
const response = await this.admin.members.list({ groupKey: groupId });
return (response.data.members || []).map(member => ({
email: member.email!,
role: member.role!,
status: member.status!
}));
}
}
Service Account Authentication for Automation
For server-side automation without user OAuth (requires domain-wide delegation):
// service-account-auth.ts
import { JWT } from 'google-auth-library';
export function createServiceAccountClient(
keyFile: string,
scopes: string[],
subject?: string // Email of user to impersonate (domain-wide delegation)
): JWT {
const auth = new JWT({
keyFile,
scopes,
subject // Required for Admin SDK and domain-wide delegation
});
return auth;
}
// Example: Automated daily user audit report
async function generateUserAuditReport() {
const auth = createServiceAccountClient(
'./.vault/service-account-key.json',
[
'https://www.googleapis.com/auth/admin.directory.user.readonly',
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/gmail.send'
],
'admin@yourdomain.com' // Impersonate admin
);
const adminClient = new AdminClient(auth as any);
const sheetsClient = new SheetsClient(auth as any);
const gmailClient = new GmailClient(auth as any);
// Fetch all users
const users = await adminClient.listUsers({
domain: 'yourdomain.com',
maxResults: 500
});
// Create spreadsheet with user data
const spreadsheet = await sheetsClient.createSpreadsheet('User Audit Report', [
{ title: 'Active Users' },
{ title: 'Suspended Users' }
]);
const activeUsers = users.filter(u => !u.suspended);
const suspendedUsers = users.filter(u => u.suspended);
await sheetsClient.writeRange(
spreadsheet.id,
'Active Users!A1:E1',
[['Email', 'Name', 'Org Unit', 'Created', 'Last Login']]
);
await sheetsClient.appendData(
spreadsheet.id,
'Active Users!A2:E',
activeUsers.map(u => [
u.email,
u.name.fullName,
u.orgUnitPath,
u.creationTime.toLocaleDateString(),
u.lastLoginTime?.toLocaleDateString() || 'Never'
])
);
// Send report via email
await gmailClient.sendEmail({
to: 'it-team@yourdomain.com',
subject: `Daily User Audit Report - ${new Date().toLocaleDateString()}`,
body: `<h2>User Audit Report</h2><p>Total Users: ${users.length}</p><p>Active: ${activeUsers.length}</p><p>Suspended: ${suspendedUsers.length}</p><p><a href="${spreadsheet.url}">View Full Report</a></p>`,
isHtml: true
});
}
Learn more about enterprise ChatGPT app deployment and Google Workspace security best practices.
Conclusion: Build Enterprise-Grade Google Workspace ChatGPT Apps
Google Workspace integration transforms ChatGPT apps into powerful enterprise automation tools capable of replacing multiple SaaS products. With OAuth 2.0 authentication, Gmail API email management, Calendar API scheduling intelligence, Drive and Sheets file operations, and Admin SDK user administration, you can build conversational interfaces that rival traditional enterprise software.
The 10 production-ready code examples in this guide provide a complete foundation for:
- Email Automation: Send, read, manage Gmail messages with attachments, threading, and labels
- Intelligent Scheduling: Find availability, create events, detect conflicts, generate Google Meet links
- File Management: Upload, download, share, organize Drive files and folders with permissions
- Spreadsheet Operations: Read, write, analyze Google Sheets data with batch operations and formulas
- Enterprise Administration: Manage users, groups, organizational units, and audit logs
Implementation Checklist:
- ✅ Create Google Cloud project with OAuth credentials
- ✅ Enable required APIs (Gmail, Calendar, Drive, Sheets, Admin SDK)
- ✅ Configure OAuth consent screen with minimum necessary scopes
- ✅ Implement OAuth 2.1 with PKCE for ChatGPT Apps SDK compliance
- ✅ Build MCP server with Google Workspace API clients
- ✅ Test with MCP Inspector:
npx @modelcontextprotocol/inspector@latest http://localhost:3000/mcp - ✅ Deploy to production HTTPS endpoint
- ✅ Submit to OpenAI for ChatGPT App Store approval
Performance Best Practices:
- Use batch requests for multiple operations (10x faster than sequential calls)
- Implement exponential backoff for rate limit errors (429 responses)
- Cache OAuth tokens and refresh automatically (tokens expire after 1 hour)
- Request minimum necessary scopes (reduces friction during user consent)
- Monitor quota usage in Google Cloud Console (avoid hitting daily limits)
Security Considerations:
- Never expose OAuth credentials in client-side code or version control
- Store service account keys in
.vault/directory with.gitignore - Rotate service account keys every 90 days
- Use domain-wide delegation only when necessary (audit regularly)
- Implement proper error handling to prevent token leakage in logs
Ready to build enterprise-grade ChatGPT apps with Google Workspace integration? Start building with MakeAIHQ.com—the only no-code platform specifically designed for ChatGPT App Store deployment with built-in Google Workspace templates, OAuth handling, enterprise security, and MCP server generation.
Additional Resources
Official Documentation:
- Google Workspace APIs Documentation
- OAuth 2.0 for Web Server Applications
- Admin SDK Directory API Reference
Related MakeAIHQ Guides:
- OpenAI Apps SDK ChatGPT Builder Guide
- Enterprise Authentication Patterns
- Email Automation for ChatGPT Apps
- AI Scheduling Assistant Implementation
- ChatGPT App Security Best Practices
- MCP Server Development Guide
- File Automation with ChatGPT Apps
Templates:
- AI Email Assistant Template
- Document Automation Template
- Data Analysis Chatbot Template
- Scheduling Assistant Template
Published December 25, 2026 | Updated: December 25, 2026 | Reading Time: 18 minutes
About MakeAIHQ: We're the no-code platform for building ChatGPT apps—from zero to ChatGPT App Store in 48 hours. Start building today with our AI-powered app builder and reach 800 million ChatGPT users worldwide.