skills/googleworkspace-cli/SKILL.md
Unified CLI interface for Google Workspace APIs (Drive, Gmail, Calendar, Sheets, Docs, Chat, Admin, etc.) with structured output and multi-service support
npx skillsauth add arisng/github-copilot-fc googleworkspace-cliInstall 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.
The googleworkspace-cli agent skill provides a unified, production-ready interface to 18+ Google Workspace services through the gws command-line tool. It eliminates authentication boilerplate, API fragmentation, and credential management complexity—enabling Copilot agents to orchestrate email campaigns, calendar workflows, document collaboration, and admin tasks without manually learning each service's API. Use this skill when agents need to integrate with enterprise Google Workspace environments, automate workflows across multiple services, or manage bulk operations securely.
# Windows (using Scoop or manual download)
scoop install gws
# Or download from GitHub directly
Invoke-WebRequest -Uri "https://github.com/googleworkspace/cli/releases/download/latest/gws-windows-amd64.exe" `
-OutFile "$env:PROGRAMFILES\gws\gws.exe"
# Linux/macOS
curl -L https://github.com/googleworkspace/cli/releases/download/latest/gws-linux-amd64 -o /usr/local/bin/gws
chmod +x /usr/local/bin/gws
# Interactive OAuth2 flow (one-time setup)
gws auth setup
# Verify authentication status
gws auth status
# Output:
# Account: [email protected]
# Domain: example.com
# Scopes: gmail,drive,calendar,sheets,docs,chat,admin
# Expires: 2026-05-01
# Export authentication token for non-interactive use
gws auth export --format env >> .env
# Then load in CI/CD pipeline
cat .env | docker run --env-file /dev/stdin gws:latest gws gmail search --query "from:team"
# Test connectivity to all services
gws health check
# List available services
gws service list
# Test a simple Gmail operation
gws gmail search --query "is:unread" --limit 1 --output json
Scenario: Find urgent emails from leadership and tag them with a custom label.
# Search for urgent emails
$emails = gws gmail search --query 'from:([email protected] OR [email protected]) subject:(urgent OR critical)' `
--limit 50 --output json | ConvertFrom-Json
# Apply "Urgent" label to each email
foreach ($email in $emails.messages) {
gws gmail modify --id $email.id `
--add-label-ids "URGENT" `
--remove-label-ids "INBOX" `
--output json
}
# Send auto-reply to sender
$firstEmail = $emails.messages[0]
gws gmail send `
--to $firstEmail.headers.From `
--subject "Re: $($firstEmail.headers.Subject)" `
--body "I've received your urgent message and prioritized it. Will respond within 2 hours." `
--output json
Scenario: Create a meeting notes document, share it with the team, and send a calendar invite.
# Create a new Google Doc
$doc = gws docs create --title "Team Meeting - Q2 Planning" `
--description "Collaborative notes and action items" `
--output json | ConvertFrom-Json
$docId = $doc.documentId
# Share document with team (editor access)
gws drive share --file-id $docId `
--role editor `
--emails-file teams.txt `
--output json
# Insert initial content
gws docs append --document-id $docId `
--text "# Meeting Agenda
## Topics
- Q2 OKRs
- Resource Planning
- Timeline
## Notes
(To be filled in during meeting)
## Action Items
- [ ] Action 1
- [ ] Action 2
" `
--output json
# Create calendar event and invite attendees
gws calendar create `
--summary "Team Meeting - Q2 Planning" `
--start "2026-04-15T10:00:00Z" `
--end "2026-04-15T11:00:00Z" `
--attendees-file teams.txt `
--conference-type hangoutsMeet `
--description "Meeting notes: $(https://docs.google.com/document/d/$docId/edit)" `
--output json
Scenario: Find when all team members are available and suggest optimal meeting times.
# List attendees
$attendees = @("[email protected]", "[email protected]", "[email protected]")
# Check availability for next 5 business days
$startTime = (Get-Date).AddDays(1).ToUniversalTime().ToString('o')
$endTime = (Get-Date).AddDays(6).ToUniversalTime().ToString('o')
$freebusy = gws calendar freebusy `
--emails $attendees `
--time-min $startTime `
--time-max $endTime `
--output json | ConvertFrom-Json
# Analyze results to find best time slots (3+ hours of free time for all)
$availability = @()
foreach ($slot in $freebusy.calendars) {
if ($slot.busy.Count -lt 2) { # Less than 2 busy blocks = mostly free
$availability += $slot
}
}
Write-Host "Best times for all attendees:"
$availability | ForEach-Object { Write-Host " $_" }
# Create meeting at suggested time
if ($availability.Count -gt 0) {
gws calendar create `
--summary "Team Sync" `
--start "2026-04-16T14:00:00Z" `
--end "2026-04-16T15:00:00Z" `
--attendees $attendees `
--conference-type hangoutsMeet `
--output json
}
# Search with advanced queries
gws gmail search --query 'from:boss label:work is:unread after:2026-03-01' --limit 100 --output json
# Read email thread with all messages
gws gmail get-thread --thread-id "abc123" --format full --output json
# Send email with attachment
gws gmail send `
--to "[email protected]" `
--subject "Report" `
--body "Please find attached" `
--attachments "report.pdf" "data.xlsx" `
--output json
# Modify multiple emails (batch operation)
gws gmail batch-modify `
--ids "msg1,msg2,msg3" `
--add-labels "important" `
--remove-labels "inbox" `
--output json
# Label management
gws gmail list-labels --output json
gws gmail create-label --name "project-alpha" --color FF6D00 --output json
# Archive messages older than 30 days
gws gmail search --query 'before:2026-02-01' --output json | \
jq '.messages[].id' | \
xargs -I {} gws gmail modify --id {} --add-labels "ARCHIVE" --output json
# List files and folders
gws drive list --query "name contains 'Report' and trashed=false" --spaces drive --limit 50 --output json
# Create folder
gws drive create-folder --name "Q2 2026 Planning" --parent "root" --output json
# Upload file
gws drive upload --file "presentation.pptx" --name "Q2 Strategy" --parent "ABC123" --output json
# Share file with multiple people
gws drive share --file-id "XYZ789" `
--role editor `
--emails "[email protected],[email protected]" `
--notify --output json
# Grant team-wide access
gws drive share --file-id "ABC123" --role reader --type domain --domain "example.com" --output json
# Update file permissions
gws drive update-permission --file-id "ABC123" --permission-id "perm456" --role commenter --output json
# Move file to team drive
gws drive update --file-id "ABC123" --new-parent "team-drive-id" --output json
# Audit file access
gws drive get-permissions --file-id "ABC123" --output json
gws drive list-revisions --file-id "ABC123" --limit 10 --output json
# List events for a date range
gws calendar list `
--calendar-id "primary" `
--time-min "2026-04-01T00:00:00Z" `
--time-max "2026-04-30T23:59:59Z" `
--max-results 100 `
--output json
# Create event with recurrence
gws calendar create `
--summary "Weekly Sync" `
--description "Team synchronization" `
--start "2026-04-21T09:00:00" `
--end "2026-04-21T10:00:00" `
--recurrence "FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12" `
--timezone "America/Los_Angeles" `
--attendees "[email protected],[email protected]" `
--conference-type hangoutsMeet `
--output json
# Find available time slots (busy times)
gws calendar freebusy `
--emails "[email protected],[email protected],[email protected]" `
--time-min "2026-04-15T00:00:00Z" `
--time-max "2026-04-22T23:59:59Z" `
--output json
# Update event
gws calendar update --event-id "evt123" `
--start "2026-04-21T10:00:00" `
--end "2026-04-21T11:00:00" `
--summary "Weekly Sync (Rescheduled)" `
--notify-attendees `
--output json
# Delete recurring event instance
gws calendar delete --event-id "evt123" --calendar-id "primary" --send-notifications --output json
# List calendars
gws calendar list-calendars --summary-only --output json
# Read data from spreadsheet
gws sheets get --spreadsheet-id "ABC123" --range "Sheet1!A1:D100" --output json
# Write data to cells
gws sheets update --spreadsheet-id "ABC123" --range "Sheet1!A1" `
--values '[[Name,Email,Status],[Alice,[email protected],Active],[Bob,[email protected],Inactive]]' `
--output json
# Append rows
gws sheets append --spreadsheet-id "ABC123" --range "Sheet1!A:D" `
--values '[[Charlie,[email protected],Active]]' `
--output json
# Create new spreadsheet
gws sheets create --title "Q2 OKRs" --sheets "Engineering,Product,Sales" --output json
# Add sheet to existing spreadsheet
gws sheets add-sheet --spreadsheet-id "ABC123" --title "New Tab" --output json
# Clear sheet
gws sheets clear --spreadsheet-id "ABC123" --range "Sheet1" --output json
# Batch update (formulas, formatting, etc.)
gws sheets batch-update --spreadsheet-id "ABC123" `
--requests '[{"updateCells":{"range":{"sheetId":0},"rows":[{"values":[{"userEnteredFormula":{"formula":"=SUM(B2:B100)"}}]}]}}]' `
--output json
# Export spreadsheet as CSV
gws sheets export --spreadsheet-id "ABC123" --range "Sheet1" --format csv > output.csv
# Create document
gws docs create --title "Product Requirements" --output json
# Append text
gws docs append --document-id "doc123" `
--text "# Introduction\n\nThis is a sample document." `
--output json
# Insert table
gws docs insert-table --document-id "doc123" --rows 5 --columns 3 `
--location 1 --output json
# Insert image
gws docs insert-image --document-id "doc123" --image-url "https://example.com/image.png" `
--width 200 --height 150 --location 1 --output json
# Insert page break
gws docs insert-page-break --document-id "doc123" --location 100 --output json
# Update text (replace)
gws docs update --document-id "doc123" --request-type replaceText `
--old-text "placeholder" --new-text "actual content" --output json
# Add comment
gws docs insert-comment --document-id "doc123" `
--text "Please review this section" `
--anchor-text "Introduction" `
--resolved false `
--output json
# Get document content
gws docs get --document-id "doc123" --output json
# List spaces
gws chat list-spaces --filter "displayName:'engineering'" --limit 50 --output json
# Send message to space
gws chat send-message `
--space "spaces/AAAABBBBCCCCDDDD" `
--text "Team: The deployment is complete." `
--output json
# Send message with card
gws chat send-card `
--space "spaces/AAAABBBBCCCCDDDD" `
--card-json '{
"sections": [
{
"widgets": [
{
"textParagraph": {
"text": "<b>Deployment Status</b>\nAll services running"
}
}
]
}
]
}' `
--output json
# Create new space
gws chat create-space --display-name "Project Alpha" --space-type ROOM --output json
# Add user to space
gws chat add-member --space "spaces/AAAABBBBCCCCDDDD" `
--member "users/[email protected]" `
--role MANAGER `
--output json
# List messages in thread
gws chat list-messages --space "spaces/AAAABBBBCCCCDDDD" --limit 100 --output json
# Reply to message (create thread)
gws chat send-message `
--space "spaces/AAAABBBBCCCCDDDD" `
--parent-message-id "msg123" `
--text "Thanks for the update!" `
--output json
# Create user
gws admin create-user `
--first-name "Alice" --last-name "Smith" `
--email "[email protected]" `
--password "TempPassword123!" `
--org-unit "/Engineering" `
--output json
# Suspend user
gws admin suspend-user --email "[email protected]" --output json
# Update user
gws admin update-user --email "[email protected]" `
--first-name "Alicia" `
--phone "555-0123" `
--department "Product Engineering" `
--output json
# List users
gws admin list-users --query "orgUnitPath='/Engineering' and suspended=false" `
--limit 100 `
--output json
# Create group
gws admin create-group --name "engineering-team" --email "[email protected]" `
--description "All engineers" --output json
# Add member to group
gws admin add-group-member --group "[email protected]" `
--member "[email protected]" --member-type USER --output json
# List group members
gws admin list-group-members --group "[email protected]" --limit 500 --output json
# Get org report (license usage)
gws admin get-customer-report --report-type licenses --output json
# List organizational units
gws admin list-org-units --parent "/" --output json
# Create OU
gws admin create-org-unit --name "Engineering" --parent "/" --output json
Use case: Extract structured information from emails (receipts, confirmations, notifications) and log to a spreadsheet.
function Sync-Emails-To-Sheet {
param(
[string]$Query = 'from:[email protected] subject:Invoice',
[string]$SpreadsheetId
)
# Search for emails matching query
$emails = gws gmail search --query $Query --limit 100 --output json | ConvertFrom-Json
# Extract data from each email
$rows = @()
foreach ($email in $emails.messages) {
$fullEmail = gws gmail get --id $email.id --format full --output json | ConvertFrom-Json
$rows += @(
$fullEmail.headers.Date,
$fullEmail.headers.From,
$fullEmail.headers.Subject,
$fullEmail.snippet
)
}
# Append to spreadsheet
if ($rows.Count -gt 0) {
gws sheets append --spreadsheet-id $SpreadsheetId `
--range "Sheet1!A:D" `
--values $rows `
--output json
Write-Host "Synced $($rows.Count) emails to spreadsheet"
}
}
Use case: Automatically find meeting times when everyone is available.
function Create-Meeting-If-Available {
param(
[string[]]$Attendees,
[string]$Title,
[int]$DurationMinutes = 60
)
# Get free/busy data
$freebusy = gws calendar freebusy `
--emails $Attendees `
--time-min ((Get-Date).AddDays(1).ToUniversalTime().ToString('o')) `
--time-max ((Get-Date).AddDays(7).ToUniversalTime().ToString('o')) `
--output json | ConvertFrom-Json
# Find best time (morning, all-day availability)
$slots = @()
for ($day = 1; $day -le 5; $day++) {
$date = (Get-Date).AddDays($day)
for ($hour = 9; $hour -le 17; $hour++) {
$slotStart = $date.Date.AddHours($hour)
# Check if all attendees are free in this slot (simplified logic)
$slots += [PSCustomObject]@{
Start = $slotStart
Duration = $DurationMinutes
Score = 1 # In real scenario: calculate based on preferences
}
}
}
# Book the best slot
$bestSlot = $slots | Sort-Object Score -Descending | Select-Object -First 1
if ($bestSlot) {
gws calendar create `
--summary $Title `
--start $bestSlot.Start.ToUniversalTime().ToString('o') `
--end ($bestSlot.Start.AddMinutes($bestSlot.Duration)).ToUniversalTime().ToString('o') `
--attendees $Attendees `
--conference-type hangoutsMeet `
--output json
Write-Host "Meeting created: $($bestSlot.Start)"
}
}
Use case: Detect over-shared files and remediate access violations.
function Audit-Drive-Permissions {
param(
[string]$FolderQuery = "name contains 'Confidential'",
[string[]]$AllowedEmails
)
# List files matching criteria
$files = gws drive list --query $FolderQuery --spaces drive --output json | ConvertFrom-Json
$violations = @()
foreach ($file in $files.files) {
# Get detailed permissions
$perms = gws drive get-permissions --file-id $file.id --output json | ConvertFrom-Json
# Check each permission
foreach ($perm in $perms.permissions) {
if ($perm.emailAddress -and $perm.emailAddress -notin $AllowedEmails) {
if ($perm.role -eq 'editor' -or $perm.role -eq 'owner') {
$violations += [PSCustomObject]@{
File = $file.name
FileId = $file.id
UnauthorizedUser = $perm.emailAddress
Role = $perm.role
PermissionId = $perm.id
}
}
}
}
}
# Report and remediate
if ($violations.Count -gt 0) {
Write-Host "⚠️ Found $($violations.Count) permission violations:"
$violations | Format-Table
# Remove unauthorized access
foreach ($violation in $violations) {
gws drive delete-permission --file-id $violation.FileId `
--permission-id $violation.PermissionId `
--output json
Write-Host " ✓ Removed $($violation.UnauthorizedUser) from $($violation.File)"
}
}
}
Use case: Bulk provision user accounts from CSV and track progress.
function Provision-Users-From-Csv {
param(
[string]$CsvPath,
[string]$OrgUnit = "/",
[string[]]$GroupsToAdd,
[switch]$DryRun
)
# Read CSV
$users = Import-Csv $CsvPath
$results = @()
foreach ($user in $users) {
Write-Host "Processing: $($user.Email)"
if ($DryRun) {
Write-Host " [DRY RUN] Would create user: $($user.FirstName) $($user.LastName)"
} else {
try {
# Create user
$result = gws admin create-user `
--first-name $user.FirstName `
--last-name $user.LastName `
--email $user.Email `
--password "TempPassword123!" `
--org-unit $OrgUnit `
--output json | ConvertFrom-Json
# Add to groups
foreach ($group in $GroupsToAdd) {
gws admin add-group-member --group $group `
--member $user.Email `
--member-type USER `
--output json | Out-Null
}
$results += [PSCustomObject]@{
Email = $user.Email
Status = 'Created'
UserId = $result.id
Timestamp = Get-Date
}
} catch {
$results += [PSCustomObject]@{
Email = $user.Email
Status = 'Failed'
Error = $_.Exception.Message
Timestamp = Get-Date
}
}
}
}
# Export results
$results | Export-Csv "provisioning-results.csv" -NoTypeInformation
# Send summary
$successCount = ($results | Where-Object { $_.Status -eq 'Created' }).Count
$failureCount = ($results | Where-Object { $_.Status -eq 'Failed' }).Count
gws gmail send --to "[email protected]" `
--subject "User Provisioning Summary" `
--body "Provisioned: $successCount, Failed: $failureCount" `
--output json
}
cmdkey CLI)security CLI)pass (encrypted GPG store)Principle of least privilege: Request only required scopes during authentication.
# Request specific scopes (not all)
gws auth setup --scopes "gmail.send,gmail.readonly,calendar.readonly"
# Check current scopes
gws auth status
# Revoke token if needed
gws auth revoke
Common scopes:
gmail.readonly — Read emails onlygmail.send — Send emailsgmail.modify — Modify labels, archive, etc.calendar.events — Create/modify calendar eventsdrive.readonly — Read files (not modify)drive.file — Create/modify files created by app onlysheets.readonly — Read spreadsheetsadmin.directory.user.readonly — Read-only admin accessAlways use --dry-run before destructive operations:
# Preview without executing
gws gmail batch-modify --ids "msg1,msg2,msg3" `
--remove-labels "INBOX" `
--dry-run --output json
# If output looks correct, execute
gws gmail batch-modify --ids "msg1,msg2,msg3" `
--remove-labels "INBOX" `
--output json
All inputs are validated to prevent injection attacks:
# Sanitization is automatic:
# - Path traversal blocked: gws drive upload --file "../../secret.txt" → ERROR
# - SQL injection prevention: gws admin list-users --query "'; DROP TABLE--" → ERROR
# - Command injection: Special characters escaped automatically
# - XPath attacks: XML input validated against schema
# Explicitly sanitize if needed
gws admin create-user --first-name $input `
--sanitize ` # Removes HTML, scripts, etc.
--output json
All API calls are logged to a secure audit log:
# View audit logs
gws logs list --since "1 hour ago" --output json
# Filter by operation
gws logs list --operation "gmail.send" --output json
# Export audit logs (CSV for analysis)
gws logs export --format csv --since "7 days ago" > audit-log.csv
# Audit log includes:
# - Timestamp
# - User email
# - Operation (service.action)
# - Resource ID (file ID, email ID, etc.)
# - Result (success/failure)
# - Duration (ms)
# - IP address
# Example: Prevent email content leakage
# (Model Armor automatically enforces this)
# gws gmail get --id "msg123" --format full → ❌ Model Armor blocks if agent hasn't declared need
# gws gmail get --id "msg123" --format metadata → ✓ Allowed (headers only, no body)
# Agents must declare data access needs in their instructions
Error: 403 Forbidden: The user has not granted the application the required scopes.
Solution:
gws auth revoke
gws auth setup
Error: 403 Forbidden: Permission denied. User must have appropriate permissions in the Google Cloud project.
Solution:
gws auth status
gws auth revoke
gws auth setup --scopes "gmail.modify,drive.file,calendar.events,sheets,docs,chat,admin.directory.user"
Error: 429 Too Many Requests: Quota exceeded for quota metric 'Gmail API Queries' and limit 'USER_100sec'.
Solution:
# Add delay between batch operations
gws gmail batch-modify --ids $ids --page-delay 1000 `
--output json
# Fetch smaller batches
gws gmail search --query $query --limit 10 --page-limit 5 `
--output json
$retryCount = 0
while ($retryCount -lt 3) {
try {
$result = gws gmail search --query $query --output json
break
} catch {
$retryCount++
$waitTime = [Math]::Pow(2, $retryCount)
Write-Host "Rate limited. Retrying in $waitTime seconds..."
Start-Sleep -Seconds $waitTime
}
}
Error: 401 Unauthorized: OAuth token has expired.
Solution:
gws auth login --refresh
gws auth revoke
gws auth setup
Error: 404 Not Found: File not found: invalid file ID.
Solution:
# Get file ID from Drive UI or list:
gws drive list --query "name='myfile.txt'" --output json
gws drive list --query "name='myfile.txt' and trashed=true" --output json
gws drive share --file-id $fileId --role reader --emails "[email protected]"
Error: 408 Request Timeout: The request timed out.
Solution:
gws --timeout 120 sheets get --spreadsheet-id $id --range "Sheet1!A:Z" `
--output json
gws sheets get --spreadsheet-id $id --range "Sheet1!A1:D1000" `
--paginate --page-size 500 `
--output json
# Process 1000 rows at a time
for ($i = 1; $i -le 100000; $i += 1000) {
$end = $i + 1000
gws sheets get --spreadsheet-id $id `
--range "Sheet1!A$i:D$end" `
--output json
}
| Version | Date | Changes | |---------|-----------|---------| | 1.0 | 2026-04-01 | Initial release: 18+ service APIs, security hardening, multi-format output |
For issues, questions, or feature requests:
gws --version && gws auth statusdevops
Programmatically create tldraw whiteboards and visualize them with a self-hosted tldraw instance. Create boards with shapes, text, and connectors, then deploy to a self-hosted server for collaborative editing and gallery management.
tools
Execute Google Cloud Platform operations using the gcloud CLI (and gsutil/bq where applicable). Use when the user wants to: authenticate with GCP, manage GCP resources, deploy applications, configure projects or IAM, view logs, run SQL/BigQuery, or interact with any GCP service from the command line. Triggers on phrases like "gcloud", "Google Cloud CLI", "deploy to GCP", "create a VM", "Cloud Run", "GKE cluster", "Cloud Storage bucket", "set GCP project", "service account", "Cloud Functions", "App Engine deploy", or any request to manage Google Cloud resources via command line.
testing
Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
development
Session-scoped git commit orchestrator that commits only current-session changes and leaves unrelated dirty worktree edits untouched. Inherits git-atomic-commit for atomic grouping and commit message execution, and git-commit-scope-constitution for scope governance and validation. Use when asked to commit this session only or isolate commits from mixed worktree state.