BundledProductivityVersion 1.2.0

Google Workspace Skill for Hermes Agent: Gmail, Calendar, Drive, Docs, Sheets

Gmail, Calendar, Drive, Docs, Sheets via gws CLI or Python.

Written by Neura Market from the official Hermes Agent documentation for Google Workspace. Commands, paths, and version numbers are reproduced from the source unchanged.

Read the official documentation

If you run Hermes Agent and need it to read your inbox, manage your calendar, or pull files from Drive, this bundled skill is the direct route. It wraps Gmail, Calendar, Drive, Contacts, Sheets, and Docs behind a single Python CLI, with OAuth handled once during setup. You would reach for it when you want an agent that can act on your Google data, not just answer questions about it.

What it does

The skill gives Hermes a command-line interface to the most common Google Workspace operations. You can search and read email, send messages and replies, list and create calendar events, find and download Drive files, upload new ones, share them, read and write spreadsheet cells, and create or append to documents. Every command returns JSON, so the agent can parse results and decide the next step without scraping text output.

The skill has two execution backends. If the gws CLI is installed on the system, the skill uses it for broader coverage. Otherwise it falls back to a bundled Python client. Either way, the output contract stays the same, so your agent code does not need to know which backend ran.

Before you start

This skill ships with Hermes Agent, so there is nothing to install separately. It runs on Linux, macOS, and Windows. The real prerequisite is a Google Cloud OAuth client. You need a project with the Gmail API, Google Calendar API, Google Drive API, Google Sheets API, Google Docs API, and People API enabled, plus an OAuth 2.0 Client ID of type "Desktop app". If the app is still in Testing, you must add your Google account as a test user.

The setup script is non-interactive, which means you drive it step by step and it works from the CLI, Telegram, Discord, or any other platform Hermes supports. You define a shorthand first so the commands are short enough to type repeatedly:

GSETUP="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/setup.py"

Step 0: Check if already set up

$GSETUP --check

If it prints AUTHENTICATED, skip to Usage, setup is already done.

Step 1: Triage, ask the user what they need

Before starting OAuth setup, ask the user TWO questions:

Question 1: "What Google services do you need? Just email, or also Calendar/Drive/Sheets/Docs?"

  • Email only → They don't need this skill at all. Use the himalaya skill instead, it works with a Gmail App Password (Settings → Security → App Passwords) and takes 2 minutes to set up. No Google Cloud project needed. Load the himalaya skill and follow its setup instructions.
  • Email + Calendar → Continue with this skill, but use --services email,calendar during auth so the consent screen only asks for the scopes they actually need.
  • Calendar/Drive/Sheets/Docs only → Continue with this skill and use a narrower --services set like calendar,drive,sheets,docs.
  • Full Workspace access → Continue with this skill and use the default all service set.

Question 2: "Does your Google account use Advanced Protection (hardware security keys required to sign in)? If you're not sure, you probably don't, it's something you would have explicitly enrolled in."

  • No / Not sure → Normal setup. Continue below.
  • Yes → Their Workspace admin must add the OAuth client ID to the org's allowed apps list before Step 4 will work. Let them know upfront.

Step 2: Create OAuth credentials (one-time, ~5 minutes)

Tell the user:

You need a Google Cloud OAuth client. This is a one-time setup:

  1. Create or select a project: https://console.cloud.google.com/projectselector2/home/dashboard
  2. Enable the required APIs from the API Library: https://console.cloud.google.com/apis/library Enable: Gmail API, Google Calendar API, Google Drive API, Google Sheets API, Google Docs API, People API
  3. Create the OAuth client here: https://console.cloud.google.com/apis/credentials Credentials → Create Credentials → OAuth 2.0 Client ID
  4. Application type: "Desktop app" → Create
  5. If the app is still in Testing, add the user's Google account as a test user here: https://console.cloud.google.com/auth/audience Audience → Test users → Add users
  6. Download the JSON file and tell me the file path

Important Hermes CLI note: if the file path starts with /, do NOT send only the bare path as its own message in the CLI, because it can be mistaken for a slash command. Send it in a sentence instead, like: The JSON file path is: ~/Downloads/client_secret_....json

Once they provide the path:

$GSETUP --client-secret /path/to/client_secret.json

If they paste the raw client ID / client secret values instead of a file path, write a valid Desktop OAuth JSON file for them yourself, save it somewhere explicit (for example ~/Downloads/hermes-google-client-secret.json), then run --client-secret against that file.

Step 3: Get authorization URL

Use the service set chosen in Step 1. Examples:

$GSETUP --auth-url --services email,calendar --format json
$GSETUP --auth-url --services calendar,drive,sheets,docs --format json
$GSETUP --auth-url --services all --format json

This returns JSON with an auth_url field and also saves the exact URL to ~/.hermes/google_oauth_last_url.txt.

Agent rules for this step:

  • Extract the auth_url field and send that exact URL to the user as a single line.
  • Tell the user that the browser will likely fail on http://localhost:1 after approval, and that this is expected.
  • Tell them to copy the ENTIRE redirected URL from the browser address bar.
  • If the user gets Error 403: access_denied, send them directly to https://console.cloud.google.com/auth/audience to add themselves as a test user.

Step 4: Exchange the code

The user will paste back either a URL like http://localhost:1/?code=4/0A...&scope=... or just the code string. Either works. The --auth-url step stores a temporary pending OAuth session locally so --auth-code can complete the PKCE exchange later, even on headless systems:

$GSETUP --auth-code "THE_URL_OR_CODE_THE_USER_PASTED" --format json

If --auth-code fails because the code expired, was already used, or came from an older browser tab, it now returns a fresh fresh_auth_url. In that case, immediately send the new URL to the user and have them retry with the newest browser redirect only.

Step 5: Verify

$GSETUP --check

Should print AUTHENTICATED. Setup is complete, token refreshes automatically from now on.

Notes

  • Token is stored at ~/.hermes/google_token.json and auto-refreshes.
  • Pending OAuth session state/verifier are stored temporarily at ~/.hermes/google_oauth_pending.json until exchange completes.
  • If gws is installed, google_api.py points it at the same ~/.hermes/google_token.json credentials file. Users do not need to run a separate gws auth login flow.
  • To revoke: $GSETUP --revoke

Usage

All commands go through the API script. Set GAPI as a shorthand:

GAPI="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py"

Gmail

# Search (returns JSON array with id, from, subject, date, snippet)
$GAPI gmail search "is:unread" --max 10
$GAPI gmail search "from:boss@company.com newer_than:1d"
$GAPI gmail search "has:attachment filename:pdf newer_than:7d"

# Read full message (returns JSON with body text)
$GAPI gmail get MESSAGE_ID

# Send
$GAPI gmail send --to user@example.com --subject "Hello" --body "Message text"
$GAPI gmail send --to user@example.com --subject "Report" --body "<h1>Q4</h1><p>Details...</p>" --html
$GAPI gmail send --to user@example.com --subject "Hello" --from '"Research Agent" <user@example.com>' --body "Message text"

# Reply (automatically threads and sets In-Reply-To)
$GAPI gmail reply MESSAGE_ID --body "Thanks, that works for me."
$GAPI gmail reply MESSAGE_ID --from '"Support Bot" <user@example.com>' --body "Thanks"

# Labels
$GAPI gmail labels
$GAPI gmail modify MESSAGE_ID --add-labels LABEL_ID
$GAPI gmail modify MESSAGE_ID --remove-labels UNREAD

The search command accepts the same operators you would type into Gmail's search box, so is:unread, from:, and newer_than: all work. The --max flag caps how many results come back, which keeps the JSON payload small. Reading a full message by ID gives you the body text, which is what you need for summarization or extraction. Sending supports plain text or HTML, and you can override the display name and address with --from. Replying threads automatically, so the conversation stays together in Gmail. Label operations let you mark messages read or move them into folders.

Calendar

# List events (defaults to next 7 days)
$GAPI calendar list
$GAPI calendar list --start 2026-03-01T00:00:00Z --end 2026-03-07T23:59:59Z

# Create event (ISO 8601 with timezone required)
$GAPI calendar create --summary "Team Standup" --start 2026-03-01T10:00:00-06:00 --end 2026-03-01T10:30:00-06:00
$GAPI calendar create --summary "Lunch" --start 2026-03-01T12:00:00Z --end 2026-03-01T13:00:00Z --location "Cafe"
$GAPI calendar create --summary "Review" --start 2026-03-01T14:00:00Z --end 2026-03-01T15:00:00Z --attendees "alice@co.com,bob@co.com"

# Delete event
$GAPI calendar delete EVENT_ID

Listing events without arguments shows the next week. You can narrow the window with explicit start and end times, but those must be ISO 8601 with a timezone offset or a trailing Z for UTC. Creating an event requires the same format; the command will not guess a timezone for you. Attendees are a comma-separated list in a single string. Deleting takes the event ID you get from a list or create call.

Drive

# Search existing files
$GAPI drive search "quarterly report" --max 10
$GAPI drive search "mimeType='application/pdf'" --raw-query --max 5

# Get metadata for a single file
$GAPI drive get FILE_ID

# Upload a local file (auto-detects MIME type)
$GAPI drive upload /path/to/report.pdf
$GAPI drive upload /path/to/image.png --name "Logo.png" --parent FOLDER_ID

# Download (binary files download as-is; Google-native files export to a
# sensible default — Docs→pdf, Sheets→csv, Slides→pdf, Drawings→png)
$GAPI drive download FILE_ID
$GAPI drive download DOC_ID --output ~/doc.pdf
$GAPI drive download DOC_ID --export-mime text/plain --output ~/doc.txt

# Create a folder
$GAPI drive create-folder "Reports"
$GAPI drive create-folder "Q4" --parent FOLDER_ID

# Share
$GAPI drive share FILE_ID --email alice@example.com --role reader
$GAPI drive share FILE_ID --email alice@example.com --role writer --notify
$GAPI drive share FILE_ID --type anyone --role reader        # anyone with link
$GAPI drive share FILE_ID --type domain --domain example.com --role reader

# Delete — defaults to trash (reversible). Use --permanent to skip the trash.
$GAPI drive delete FILE_ID
$GAPI drive delete FILE_ID --permanent

Drive search takes a plain text query by default, or a raw query when you pass --raw-query, which lets you filter by MIME type or other metadata. Uploads detect the file type automatically, but you can override the name and place the file in a specific folder. Downloads behave differently depending on the file: binary files come down as-is, while Google-native files export to a sensible format unless you override with --export-mime. Creating folders is straightforward, and sharing supports email addresses or link-based access with reader or writer roles. Deletion goes to the trash by default, which is reversible, and --permanent skips the trash entirely.

Contacts

$GAPI contacts list --max 20

This returns a list of contacts with names, email addresses, and phone numbers. It is useful when you need to resolve a name to an address before sending an email or adding an attendee.

Sheets

# Create a new spreadsheet
$GAPI sheets create --title "Q4 Budget"
$GAPI sheets create --title "Inventory" --sheet-name "Stock"

# Read
$GAPI sheets get SHEET_ID "Sheet1!A1:D10"

# Write
$GAPI sheets update SHEET_ID "Sheet1!A1:B2" --values '[["Name","Score"],["Alice","95"]]'

# Append rows
$GAPI sheets append SHEET_ID "Sheet1!A:C" --values '[["new","row","data"]]'

Sheets operations use A1 notation for ranges. Creating a spreadsheet optionally names the first sheet. Reading returns a nested array of cell values. Writing replaces the contents of the given range with the values you supply, and appending adds rows below the existing data. The --values argument is a JSON string, so you need to escape quotes carefully in a shell.

Docs

# Read
$GAPI docs get DOC_ID

# Create a new Doc (optionally seeded with body text)
$GAPI docs create --title "Meeting Notes"
$GAPI docs create --title "Draft" --body "First paragraph..."

# Append text to the end of an existing Doc
$GAPI docs append DOC_ID --text "Additional content to append"

Docs support reading the full content, creating a new document with an optional initial body, and appending text to the end of an existing one. This is enough for an agent to draft meeting notes, write a report, or add a section to a living document.

Output Format

All commands return JSON. Parse with jq or read directly. Key fields:

  • Gmail search: [{id, threadId, from, to, subject, date, snippet, labels}]
  • Gmail get: {id, threadId, from, to, subject, date, labels, body}
  • Gmail send/reply: {status: "sent", id, threadId}
  • Calendar list: [{id, summary, start, end, location, description, htmlLink}]
  • Calendar create: {status: "created", id, summary, htmlLink}
  • Drive search: [{id, name, mimeType, modifiedTime, webViewLink}]
  • Drive get: {id, name, mimeType, modifiedTime, size, webViewLink, parents, owners}
  • Drive upload: {status: "uploaded", id, name, mimeType, webViewLink}
  • Drive download: {status: "downloaded", id, name, path, mimeType}
  • Drive create-folder: {status: "created", id, name, webViewLink}
  • Drive share: {status: "shared", permissionId, fileId, role, type}
  • Drive delete: {status: "trashed" | "deleted", fileId, permanent}
  • Contacts list: [{name, emails: [...], phones: [...]}]
  • Sheets get: [[cell, cell, ...], ...]
  • Sheets create: {status: "created", spreadsheetId, title, spreadsheetUrl}
  • Docs create: {status: "created", documentId, title, url}
  • Docs append: {status: "appended", documentId, inserted_at, characters}

Because every command returns structured JSON, you can chain them in an agent loop: search for unread mail, extract a meeting time, create a calendar event, and send a confirmation, all without parsing prose.

Rules

  1. Never send email, create/delete calendar events, delete Drive files, share files, or modify Docs/Sheets without confirming with the user first. Show what will be done (recipients, file IDs, content, share role) and ask for approval. For drive delete, prefer the default trash (reversible) over --permanent.
  2. Check auth before first use, run setup.py --check. If it fails, guide the user through setup.
  3. Use the Gmail search syntax reference for complex queries, load it with skill_view("google-workspace", file_path="references/gmail-search-syntax.md").
  4. Calendar times must include timezone, always use ISO 8601 with offset (e.g., 2026-03-01T10:00:00-06:00) or UTC (Z).
  5. Respect rate limits, avoid rapid-fire sequential API calls. Batch reads when possible.

These rules exist because the skill can take irreversible actions. The confirmation requirement is not optional; an agent that sends mail or deletes files without asking is a liability. The timezone rule prevents the classic mistake of creating an event at the wrong hour. Rate limits matter because Google will throttle you if you fire off dozens of calls in a second.

Troubleshooting

ProblemFix
NOT_AUTHENTICATEDRun setup Steps 2-5 above
REFRESH_FAILEDToken revoked or expired, redo Steps 3-5
HttpError 403: Insufficient PermissionMissing API scope, $GSETUP --revoke then redo Steps 3-5
AUTHENTICATED (partial) or "Token missing scopes"New write capabilities (Drive write/delete, Docs create/edit) require re-authorization. $GSETUP --revoke then redo Steps 3-5 to grant the upgraded scopes.
HttpError 403: Access Not ConfiguredAPI not enabled, user needs to enable it in Google Cloud Console
ModuleNotFoundErrorRun $GSETUP --install-deps
Advanced Protection blocks authWorkspace admin must allowlist the OAuth client ID

Most failures trace back to one of three causes: the token is missing or stale, the API is not enabled, or the OAuth consent screen does not include the scopes you need. The table gives you the exact fix for each. When in doubt, revoke and redo the auth flow, it is the cleanest reset.

Revoking Access

$GSETUP --revoke

Run this when you want to disconnect the skill from your Google account entirely. It invalidates the stored token, and the next --check will report NOT_AUTHENTICATED.

When not to use it

The source is explicit about one case: if the user only needs email, skip this skill entirely. The himalaya skill works with a Gmail App Password, takes about two minutes to set up, and does not require a Google Cloud project. This skill's OAuth setup is worth the overhead only when you also need Calendar, Drive, Sheets, or Docs.

Limits and gotchas

  • The OAuth flow requires a Google Cloud project with six APIs enabled. That is a real barrier for a casual user, and the setup steps are manual.
  • Advanced Protection accounts will fail at Step 4 unless a Workspace admin allowlists the OAuth client ID. This is an organizational policy, not something you can fix from the skill.
  • Calendar events must include a timezone. Omitting it will cause an error or, worse, create an event at the wrong time.
  • Drive deletion defaults to trash, which is reversible, but --permanent is not. Use it only after explicit user confirmation.
  • The token auto-refreshes, but if it is revoked or expires, you will see REFRESH_FAILED and need to redo Steps 3-5.
  • Newer write capabilities require re-authorization. If you see AUTHENTICATED (partial), revoke and redo the flow to pick up the upgraded scopes.

What pairs with this

The related skill is himalaya, which covers email-only workflows with a much lighter setup. Use it when the user does not need the rest of Workspace. For anything beyond email, this skill is the one to load.

Skills the docs pair this with

More Productivity skills