google-workspace

SKILL.md

Google Workspace

Comprehensive AI agent skill for all Google Workspace document operations — Docs, Sheets, Slides, Drive, Gmail, Calendar, Chat, Forms, Admin SDK, and Apps Script — via official REST APIs.

When to use this skill

  • Creating or editing Google Docs, Sheets, Slides
  • Uploading, downloading, organizing Google Drive files and folders
  • Sending/reading Gmail, managing labels and drafts
  • Creating calendar events, inviting attendees, checking availability
  • Posting Google Chat messages, managing spaces
  • Building and reading Google Forms/surveys
  • Provisioning/managing Google Workspace users (Admin SDK)
  • Running automated workflows via Apps Script

Quick Setup

Step 1: Enable APIs in Google Cloud Console

# Install gcloud CLI (if not available)
brew install --cask google-cloud-sdk   # macOS
# Or: curl https://sdk.cloud.google.com | bash

# Enable all Workspace APIs
gcloud services enable docs.googleapis.com \
  sheets.googleapis.com slides.googleapis.com \
  drive.googleapis.com gmail.googleapis.com \
  calendar-json.googleapis.com chat.googleapis.com \
  forms.googleapis.com admin.googleapis.com \
  script.googleapis.com

Step 2: Install Python client library

pip install --upgrade \
  google-api-python-client \
  google-auth-httplib2 \
  google-auth-oauthlib

Step 3: Authenticate

# OAuth2 — interactive user auth (for accessing user's own data)
bash scripts/auth-setup.sh --oauth2 credentials.json

# Service Account — server-to-server (for automation/backend)
bash scripts/auth-setup.sh --service-account service-account-key.json

API Reference by Product

Google Docs

Endpoint: https://docs.googleapis.com/v1 Scope: https://www.googleapis.com/auth/documents

from googleapiclient.discovery import build

docs = build('docs', 'v1', credentials=creds)

# Create document
doc = docs.documents().create(body={'title': 'My Document'}).execute()
doc_id = doc['documentId']

# Read document
doc = docs.documents().get(documentId=doc_id).execute()
content = doc.get('body', {}).get('content', [])

# Edit: replace all text matching a pattern
requests = [{
    'replaceAllText': {
        'containsText': {'text': '{{name}}', 'matchCase': False},
        'replaceText': 'Alice'
    }
}]
docs.documents().batchUpdate(documentId=doc_id, body={'requests': requests}).execute()

# Insert text at position
requests = [{'insertText': {'location': {'index': 1}, 'text': 'Hello!\n'}}]
docs.documents().batchUpdate(documentId=doc_id, body={'requests': requests}).execute()

Key batchUpdate operations: insertText, deleteContentRange, replaceAllText, updateTextStyle, updateParagraphStyle, insertTable, insertInlineImage, createHeader, createFooter, createNamedRange


Google Sheets

Endpoint: https://sheets.googleapis.com/v4 Scope: https://www.googleapis.com/auth/spreadsheets

sheets = build('sheets', 'v4', credentials=creds)
ss = sheets.spreadsheets()

# Create spreadsheet
spreadsheet = ss.create(body={
    'properties': {'title': 'My Sheet'},
    'sheets': [{'properties': {'title': 'Data'}}]
}).execute()
sheet_id = spreadsheet['spreadsheetId']

# Write data
ss.values().update(
    spreadsheetId=sheet_id,
    range='Sheet1!A1',
    valueInputOption='USER_ENTERED',
    body={'values': [['Name', 'Score'], ['Alice', 95], ['Bob', 87]]}
).execute()

# Read data
result = ss.values().get(spreadsheetId=sheet_id, range='Sheet1!A:B').execute()
rows = result.get('values', [])

# Append rows
ss.values().append(
    spreadsheetId=sheet_id,
    range='Sheet1!A1',
    valueInputOption='USER_ENTERED',
    body={'values': [['Charlie', 91]]}
).execute()

# Batch update (format: freeze row 1, bold header)
ss.batchUpdate(spreadsheetId=sheet_id, body={'requests': [
    {'updateSheetProperties': {
        'properties': {'sheetId': 0, 'gridProperties': {'frozenRowCount': 1}},
        'fields': 'gridProperties.frozenRowCount'
    }},
    {'repeatCell': {
        'range': {'sheetId': 0, 'startRowIndex': 0, 'endRowIndex': 1},
        'cell': {'userEnteredFormat': {'textFormat': {'bold': True}}},
        'fields': 'userEnteredFormat.textFormat.bold'
    }}
]}).execute()

Google Slides

Endpoint: https://slides.googleapis.com/v1 Scope: https://www.googleapis.com/auth/presentations

slides = build('slides', 'v1', credentials=creds)

# Create presentation
presentation = slides.presentations().create(
    body={'title': 'My Presentation'}
).execute()
pres_id = presentation['presentationId']

# Read presentation
pres = slides.presentations().get(presentationId=pres_id).execute()
slide_ids = [s['objectId'] for s in pres.get('slides', [])]

# Add a new slide
slides.presentations().batchUpdate(presentationId=pres_id, body={'requests': [
    {'createSlide': {
        'insertionIndex': 1,
        'slideLayoutReference': {'predefinedLayout': 'TITLE_AND_BODY'}
    }}
]}).execute()

# Replace placeholder text
slides.presentations().batchUpdate(presentationId=pres_id, body={'requests': [
    {'replaceAllText': {
        'containsText': {'text': '{{title}}', 'matchCase': False},
        'replaceText': 'Q1 Report'
    }}
]}).execute()

# Get slide thumbnail
page_id = slide_ids[0]
thumb = slides.presentations().pages().getThumbnail(
    presentationId=pres_id,
    pageObjectId=page_id,
    thumbnailProperties_thumbnailSize='LARGE'
).execute()
image_url = thumb['contentUrl']

Google Drive

Endpoint: https://www.googleapis.com/drive/v3 Scope: https://www.googleapis.com/auth/drive

drive = build('drive', 'v3', credentials=creds)

# Create folder
folder = drive.files().create(body={
    'name': 'My Folder',
    'mimeType': 'application/vnd.google-apps.folder'
}).execute()
folder_id = folder['id']

# Upload file
from googleapiclient.http import MediaFileUpload
media = MediaFileUpload('report.pdf', mimetype='application/pdf')
file = drive.files().create(
    body={'name': 'report.pdf', 'parents': [folder_id]},
    media_body=media,
    fields='id'
).execute()

# Search files
results = drive.files().list(
    q="name contains 'report' and mimeType='application/pdf'",
    fields='files(id, name, modifiedTime)'
).execute()

# Share file
drive.permissions().create(
    fileId=file['id'],
    body={'type': 'user', 'role': 'reader', 'emailAddress': 'alice@example.com'},
    sendNotificationEmail=True
).execute()

# Export Google Doc to PDF
import io
from googleapiclient.http import MediaIoBaseDownload
request = drive.files().export_media(fileId=doc_id, mimeType='application/pdf')
fh = io.BytesIO()
downloader = MediaIoBaseDownload(fh, request)
done = False
while not done:
    _, done = downloader.next_chunk()
with open('document.pdf', 'wb') as f:
    f.write(fh.getvalue())

# Move file
drive.files().update(
    fileId=file['id'],
    addParents=folder_id,
    removeParents='root',
    fields='id, parents'
).execute()

# Copy file (e.g., from template)
copy = drive.files().copy(
    fileId='TEMPLATE_FILE_ID',
    body={'name': 'New Document from Template', 'parents': [folder_id]}
).execute()

Gmail

Endpoint: https://gmail.googleapis.com/gmail/v1 Scope: https://www.googleapis.com/auth/gmail.modify

import base64
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

gmail = build('gmail', 'v1', credentials=creds)

# Send email
def send_email(to, subject, body):
    msg = MIMEText(body)
    msg['to'] = to
    msg['subject'] = subject
    raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
    gmail.users().messages().send(userId='me', body={'raw': raw}).execute()

send_email('alice@example.com', 'Hello', 'This is the body.')

# Send with attachment
msg = MIMEMultipart()
msg['to'] = 'alice@example.com'
msg['subject'] = 'Report'
msg.attach(MIMEText('Please find the report attached.'))
with open('report.pdf', 'rb') as f:
    from email.mime.application import MIMEApplication
    part = MIMEApplication(f.read(), Name='report.pdf')
    part['Content-Disposition'] = 'attachment; filename="report.pdf"'
    msg.attach(part)
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
gmail.users().messages().send(userId='me', body={'raw': raw}).execute()

# Search emails
results = gmail.users().messages().list(
    userId='me', q='from:boss@company.com subject:urgent is:unread'
).execute()

# Read email
msg_id = results['messages'][0]['id']
msg = gmail.users().messages().get(userId='me', id=msg_id, format='full').execute()
subject = next(h['value'] for h in msg['payload']['headers'] if h['name'] == 'Subject')

# Create label and apply
label = gmail.users().labels().create(
    userId='me', body={'name': 'AI-Processed'}
).execute()
gmail.users().messages().modify(
    userId='me', id=msg_id,
    body={'addLabelIds': [label['id']], 'removeLabelIds': ['UNREAD']}
).execute()

# Create draft
raw_draft = base64.urlsafe_b64encode(MIMEText('Draft body').as_bytes()).decode()
gmail.users().drafts().create(
    userId='me', body={'message': {'raw': raw_draft}}
).execute()

# Set vacation responder
gmail.users().settings().updateVacation(
    userId='me',
    body={
        'enableAutoReply': True,
        'responseSubject': 'Out of Office',
        'responseBodyPlainText': 'I am OOO until Monday.',
        'startTime': '1704067200000',  # Unix ms
        'endTime':   '1704326400000'
    }
).execute()

Google Calendar

Endpoint: https://www.googleapis.com/calendar/v3 Scope: https://www.googleapis.com/auth/calendar

from datetime import datetime, timedelta
import pytz

calendar = build('calendar', 'v3', credentials=creds)

# Create event
event = calendar.events().insert(
    calendarId='primary',
    body={
        'summary': 'Team Standup',
        'description': 'Daily sync',
        'start': {'dateTime': '2026-03-15T09:00:00+09:00', 'timeZone': 'Asia/Seoul'},
        'end':   {'dateTime': '2026-03-15T09:30:00+09:00', 'timeZone': 'Asia/Seoul'},
        'attendees': [
            {'email': 'alice@example.com'},
            {'email': 'bob@example.com'},
        ],
        'conferenceData': {
            'createRequest': {'requestId': 'meeting-001', 'conferenceSolutionKey': {'type': 'hangoutsMeet'}}
        },
        'recurrence': ['RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR']
    },
    conferenceDataVersion=1
).execute()
meet_link = event.get('hangoutLink')

# List today's events
now = datetime.utcnow().isoformat() + 'Z'
end_of_day = (datetime.utcnow() + timedelta(hours=24)).isoformat() + 'Z'
events_result = calendar.events().list(
    calendarId='primary',
    timeMin=now, timeMax=end_of_day,
    singleEvents=True, orderBy='startTime'
).execute()
events = events_result.get('items', [])

# Check free/busy
body = {
    'timeMin': now,
    'timeMax': end_of_day,
    'items': [{'id': 'alice@example.com'}, {'id': 'bob@example.com'}]
}
freebusy = calendar.freebusy().query(body=body).execute()

# Block time / set OOO
calendar.events().insert(
    calendarId='primary',
    body={
        'summary': 'Out of Office',
        'eventType': 'outOfOffice',
        'start': {'date': '2026-03-20'},
        'end':   {'date': '2026-03-22'}
    }
).execute()

# Share calendar
calendar.acl().insert(
    calendarId='primary',
    body={'role': 'reader', 'scope': {'type': 'user', 'value': 'alice@example.com'}}
).execute()

Google Chat

Endpoint: https://chat.googleapis.com/v1 Scope: https://www.googleapis.com/auth/chat.messages

chat = build('chat', 'v1', credentials=creds)

# Send message to a space
space_name = 'spaces/SPACE_ID'  # From Chat URL
chat.spaces().messages().create(
    parent=space_name,
    body={
        'text': 'Hello from AI agent! 🤖',
        'cards_v2': [{
            'cardId': 'card1',
            'card': {
                'header': {'title': 'Update', 'subtitle': 'Automated report'},
                'sections': [{'widgets': [{'textParagraph': {'text': 'Task completed.'}}]}]
            }
        }]
    }
).execute()

# Create a new space
space = chat.spaces().create(
    body={
        'spaceType': 'SPACE',
        'displayName': 'Project Alpha'
    }
).execute()

# List spaces
spaces = chat.spaces().list().execute()

# Add member to space
chat.spaces().members().create(
    parent=space['name'],
    body={'member': {'name': 'users/alice@example.com', 'type': 'HUMAN'}}
).execute()

# Find or create direct message
dm = chat.spaces().findDirectMessage(name='users/alice@example.com').execute()

Google Forms

Endpoint: https://forms.googleapis.com/v1 Scope: https://www.googleapis.com/auth/forms.body

forms = build('forms', 'v1', credentials=creds)

# Create form
form = forms.forms().create(body={
    'info': {'title': 'Customer Feedback Survey', 'documentTitle': 'Customer Feedback'}
}).execute()
form_id = form['formId']

# Add questions
forms.forms().batchUpdate(formId=form_id, body={'requests': [
    {
        'createItem': {
            'item': {
                'title': 'How satisfied are you?',
                'questionItem': {
                    'question': {
                        'required': True,
                        'scaleQuestion': {
                            'low': 1, 'high': 5,
                            'lowLabel': 'Not satisfied', 'highLabel': 'Very satisfied'
                        }
                    }
                }
            },
            'location': {'index': 0}
        }
    },
    {
        'createItem': {
            'item': {
                'title': 'Any comments?',
                'questionItem': {
                    'question': {
                        'required': False,
                        'textQuestion': {'paragraph': True}
                    }
                }
            },
            'location': {'index': 1}
        }
    }
]}).execute()

# Get form responses
responses = forms.forms().responses().list(formId=form_id).execute()
for r in responses.get('responses', []):
    for qid, ans in r.get('answers', {}).items():
        print(qid, ans.get('textAnswers', {}).get('answers', []))

Admin SDK — Directory API

Endpoint: https://admin.googleapis.com Scope: https://www.googleapis.com/auth/admin.directory.user Requires: Service account with domain-wide delegation

from google.oauth2 import service_account

SA_FILE = 'service-account.json'
SCOPES  = ['https://www.googleapis.com/auth/admin.directory.user',
           'https://www.googleapis.com/auth/admin.directory.group']
creds = service_account.Credentials.from_service_account_file(
    SA_FILE, scopes=SCOPES
).with_subject('admin@yourdomain.com')

admin = build('admin', 'directory_v1', credentials=creds)

# Create user
admin.users().insert(body={
    'primaryEmail': 'newuser@yourdomain.com',
    'name': {'givenName': 'New', 'familyName': 'User'},
    'password': 'TemporaryPassword123!',
    'changePasswordAtNextLogin': True
}).execute()

# List users
users_result = admin.users().list(domain='yourdomain.com', maxResults=100).execute()
for user in users_result.get('users', []):
    print(user['primaryEmail'], user.get('suspended', False))

# Suspend user
admin.users().update(
    userKey='user@yourdomain.com',
    body={'suspended': True}
).execute()

# Add user to group
admin.members().insert(
    groupKey='team@yourdomain.com',
    body={'email': 'user@yourdomain.com', 'role': 'MEMBER'}
).execute()

# List groups
groups = admin.groups().list(domain='yourdomain.com').execute()

Apps Script API

Endpoint: https://script.googleapis.com/v1 Scope: https://www.googleapis.com/auth/script.projects

script = build('script', 'v1', credentials=creds)

# Run a deployed function
response = script.scripts().run(
    scriptId='DEPLOYED_SCRIPT_ID',
    body={
        'function': 'myFunction',
        'parameters': ['arg1', 42]
    }
).execute()
result = response.get('response', {}).get('result')

Common Automation Patterns

Pattern 1: Create Document from Template

def create_doc_from_template(drive, docs, template_id, replacements, dest_folder_id=None):
    """Clone a template Google Doc and fill in placeholders."""
    body = {'name': replacements.get('{{title}}', 'New Document')}
    if dest_folder_id:
        body['parents'] = [dest_folder_id]
    copy = drive.files().copy(fileId=template_id, body=body).execute()
    new_id = copy['id']
    requests = [
        {'replaceAllText': {'containsText': {'text': k, 'matchCase': False}, 'replaceText': v}}
        for k, v in replacements.items()
    ]
    if requests:
        docs.documents().batchUpdate(documentId=new_id, body={'requests': requests}).execute()
    return new_id

Pattern 2: Bulk Append to Spreadsheet

def bulk_append_rows(sheets, spreadsheet_id, sheet_name, rows):
    """Append multiple rows to a sheet in one API call."""
    sheets.spreadsheets().values().append(
        spreadsheetId=spreadsheet_id,
        range=f'{sheet_name}!A1',
        valueInputOption='USER_ENTERED',
        insertDataOption='INSERT_ROWS',
        body={'values': rows}
    ).execute()

Pattern 3: Create Meeting Notes Document

def create_meeting_notes(calendar, drive, docs, event_id):
    """Create a Google Doc for meeting notes and share with attendees."""
    event = calendar.events().get(calendarId='primary', eventId=event_id).execute()
    attendees = [a['email'] for a in event.get('attendees', [])]
    title = f"Meeting Notes: {event['summary']}{event['start'].get('dateTime', event['start'].get('date'))}"
    doc = docs.documents().create(body={'title': title}).execute()
    doc_id = doc['documentId']
    for email in attendees:
        drive.permissions().create(
            fileId=doc_id,
            body={'type': 'user', 'role': 'writer', 'emailAddress': email},
            sendNotificationEmail=True
        ).execute()
    return doc_id

Pattern 4: Form Response to Sheet

def sync_form_to_sheet(forms, sheets, form_id, spreadsheet_id):
    """Sync all form responses to a Google Sheet."""
    responses = forms.forms().responses().list(formId=form_id).execute()
    form_data = forms.forms().get(formId=form_id).execute()
    questions = {
        item['itemId']: item.get('title', '')
        for item in form_data.get('items', [])
        if 'questionItem' in item
    }
    headers = ['Timestamp'] + list(questions.values())
    rows = [headers]
    for resp in responses.get('responses', []):
        row = [resp.get('createTime', '')]
        for qid in questions:
            ans = resp.get('answers', {}).get(qid, {})
            text_ans = ans.get('textAnswers', {}).get('answers', [{}])
            row.append(text_ans[0].get('value', '') if text_ans else '')
        rows.append(row)
    sheets.spreadsheets().values().update(
        spreadsheetId=spreadsheet_id,
        range='Sheet1!A1',
        valueInputOption='USER_ENTERED',
        body={'values': rows}
    ).execute()

Rate Limits & Best Practices

API Quota Retry Strategy
Docs API 300 req/min/user Exponential backoff on 429
Sheets API 300 req/min Batch operations reduce quota usage
Drive API 1,000 req/100 sec Use fields param to reduce payload
Gmail API 250 quota units/user/sec batchModify for bulk operations
Calendar API 1,000,000 req/day Use timeMin/timeMax to limit list results
Admin SDK 10 user creates/domain/sec Add time.sleep(0.15) between creates
import time
from googleapiclient.errors import HttpError

def api_call_with_retry(func, *args, max_retries=5, **kwargs):
    """Wrapper that retries on 429/503 with exponential backoff."""
    for attempt in range(max_retries):
        try:
            return func(*args, **kwargs).execute()
        except HttpError as e:
            if e.resp.status in (429, 503) and attempt < max_retries - 1:
                wait = (2 ** attempt) + 0.1
                print(f"Rate limit hit, waiting {wait:.1f}s...")
                time.sleep(wait)
            else:
                raise

Scopes Reference

Product Read Scope Write Scope
Docs auth/documents.readonly auth/documents
Sheets auth/spreadsheets.readonly auth/spreadsheets
Slides auth/presentations.readonly auth/presentations
Drive auth/drive.readonly auth/drive
Gmail auth/gmail.readonly auth/gmail.modify
Calendar auth/calendar.readonly auth/calendar
Chat auth/chat.messages.readonly auth/chat.messages
Forms auth/forms.body.readonly auth/forms.body
Admin SDK auth/admin.directory.user.readonly auth/admin.directory.user

References

Weekly Installs
127
GitHub Stars
48
First Seen
4 days ago
Installed on
gemini-cli112
codex109
opencode104
kimi-cli103
github-copilot103
amp103