.agents/skills/google-workspace/SKILL.md
Create, read, update, and manage all Google Workspace documents and services via REST APIs. Use when working with Google Docs, Google Sheets, Google Slides, Google Drive, Gmail, Google Calendar, Google Chat, Google Forms, Google Meet, Admin SDK, or Apps Script. Triggers on: "Google Doc", "Google Sheet", "spreadsheet", "Google Slides", "presentation", "Google Drive", "Drive folder", "Gmail", "send email", "Google Calendar", "calendar event", "schedule meeting", "Google Chat", "Google Forms", "survey form", "Google Meet", "Workspace user", "Apps Script", "구글 문서", "구글 시트", "스프레드시트", "구글 슬라이드", "프레젠테이션", "구글 드라이브", "지메일", "이메일 보내기", "구글 캘린더", "일정 추가", "회의 예약", "구글 챗", "구글 폼", "설문지", "워크스페이스 사용자".
npx skillsauth add Reinasboo/Bountylab google-workspaceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
# 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
pip install --upgrade \
google-api-python-client \
google-auth-httplib2 \
google-auth-oauthlib
# 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
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
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()
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']
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': '[email protected]'},
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()
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('[email protected]', 'Hello', 'This is the body.')
# Send with attachment
msg = MIMEMultipart()
msg['to'] = '[email protected]'
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:[email protected] 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()
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': '[email protected]'},
{'email': '[email protected]'},
],
'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': '[email protected]'}, {'id': '[email protected]'}]
}
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': '[email protected]'}}
).execute()
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/[email protected]', 'type': 'HUMAN'}}
).execute()
# Find or create direct message
dm = chat.spaces().findDirectMessage(name='users/[email protected]').execute()
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', []))
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('[email protected]')
admin = build('admin', 'directory_v1', credentials=creds)
# Create user
admin.users().insert(body={
'primaryEmail': '[email protected]',
'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='[email protected]',
body={'suspended': True}
).execute()
# Add user to group
admin.members().insert(
groupKey='[email protected]',
body={'email': '[email protected]', 'role': 'MEMBER'}
).execute()
# List groups
groups = admin.groups().list(domain='yourdomain.com').execute()
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')
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
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()
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
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()
| 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
| 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 |
development
Security code review for vulnerabilities. Use when asked to "security review", "find vulnerabilities", "check for security issues", "audit security", "OWASP review", or review code for injection, XSS, authentication, authorization, cryptography issues. Provides systematic review with confidence-based reporting.
development
Implement security best practices for web applications and infrastructure. Use when securing APIs, preventing common vulnerabilities, or implementing security policies. Handles HTTPS, CORS, XSS, SQL Injection, CSRF, rate limiting, and OWASP Top 10.
development
Create responsive web designs that work across all devices and screen sizes. Use when building mobile-first layouts, implementing breakpoints, or optimizing for different viewports. Handles CSS Grid, Flexbox, media queries, viewport units, and responsive images.
content-media
Produce programmable videos with Remotion using scene planning, asset orchestration, and validation gates for automated, brand-consistent video content.