- Productivity
- v1.0.0
- gmail
- google-calendar
- google-drive
- sheets
- docs
Google Workspace
Send emails via Gmail, manage Google Calendar events, read/write Google Sheets, and access Google Drive files.
SKILL.md
Google Workspace (GWS)
How to use this skill — read first
- This is a set of shell commands, not an agent. Do the work yourself in
this conversation by running
gws …with your exec/shell tool. Never try to start a sub-agent or session namedgoogle_workspace— that is not allowed and does not exist. - Before telling the user you cannot reach their email, calendar or files,
check:
echo "$GWS_GOOGLE_ACCOUNT". If it prints an address, the account is connected — run the command. Only if it is empty (orgwsreturns an auth/token error) use the "not connected" message below.
"What's my latest / most recent email?"
gws gmail users messages list --params '{"userId": "me", "q": "in:inbox", "maxResults": 1}'
# then, with the id it returns:
gws gmail users messages get --params '{"userId": "me", "id": "<id>", "format": "metadata", "metadataHeaders": ["From", "Subject", "Date"]}'
Answer with the sender, subject and when it arrived. Use "q": "is:unread in:inbox" for unread mail.
IMPORTANT: Google Workspace access is managed through the MyClawBots dashboard. Do NOT run
gws auth setuporgws auth login— authentication is handled automatically via a token proxy. You MUST NOT attempt manual OAuth setup.
Prerequisites — MUST READ
- The user MUST connect their Google account in the MyClawBots dashboard before this skill can work.
- Connection is done at: Settings → Integrations → Google Workspace → Connect
- If
GWS_GOOGLE_ACCOUNTis empty orgwscommands fail with auth/token errors, you MUST tell the user:"Google Workspace isn't connected yet. Connect it at https://myclawbots.ai/settings/social — Google Workspace → Connect. Then open this agent and hit Save Settings so the connection reaches me, and I'll be able to access your Gmail, Calendar, Drive, and more."
- NEVER suggest manual OAuth flows, token generation, or credential setup. The dashboard handles everything.
- NEVER expose or discuss proxy URLs, token endpoints, or internal auth mechanisms with the user.
- Run
gwswith the default exec settings — NEVER passhost=sandbox(there is no sandbox runtime on this machine and the command will fail).
How it works
GWS_GOOGLE_ACCOUNTis set to the connected Google email (empty if not connected)- Authentication tokens are managed by a secure proxy (no secrets on this machine)
- You can use all
gwscommands directly — no setup needed once connected
More than one Google account
The user can connect several Google accounts (Settings → Connectors →
Google Workspace → Add account), and can change which one is the
default at any time. gws accounts always shows the current list and
default; GWS_GOOGLE_ACCOUNT is only the default from when you started, so
when you need to say which account you used, check gws accounts.
For anything involving a second account, run the wrapper that ships with
this skill — the full path below, not plain gws. It is always current;
the gws on your PATH may be older and reject --account.
/root/.openclaw/skills/google-workspace/scripts/gws accounts # connected accounts, current default marked
/root/.openclaw/skills/google-workspace/scripts/gws --account work@example.com gmail +send --to <email> --subject <subject> --body <text>
/root/.openclaw/skills/google-workspace/scripts/gws --account work@example.com calendar events list --params '{"calendarId": "primary", "timeMin": "...", "timeMax": "..."}'
- It takes every normal
gwscommand. Without--account, it acts as the default account, exactly like plaingws. - When the user names an account ("from my work email", "on my personal
calendar"), run
gws accountsand use the matching address with--account. - If it is unclear which account they mean and it matters (sending, creating, deleting), ask — never guess.
- Always say which account you used, e.g. "Sent from work@example.com".
- If
--accountfails with "not connected", show the user the accounts theaccountscommand lists. Never retry on the default account instead. - New accounts work immediately — no restart or Save Settings needed.
Quick reference
Gmail
- List emails:
gws gmail users messages list --params '{"userId": "me", "q": "newer_than:7d", "maxResults": 10}' - Send email: see Sending email below — always the full-path command.
- Read email:
gws gmail users messages get --params '{"userId": "me", "id": "<message_id>", "format": "full"}'
Calendar
- List events:
gws calendar events list --params '{"calendarId": "primary", "timeMin": "...", "timeMax": "..."}' - Create event:
gws calendar events insert --params '{"calendarId": "primary"}' --body '{"summary": "Meeting", "start": {...}, "end": {...}}'
Drive
- Search files:
gws drive files list --params '{"q": "name contains \"report\"", "pageSize": 10}' - Download file:
gws drive files get --params '{"fileId": "<file_id>", "alt": "media"}'
Contacts
- List contacts:
gws people people connections list --params '{"resourceName": "people/me", "pageSize": 20, "personFields": "names,emailAddresses"}'
Sheets
- Read data:
gws sheets spreadsheets values get --params '{"spreadsheetId": "ID", "range": "Sheet1!A1:D10"}' - Write data:
gws sheets spreadsheets values update --params '{"spreadsheetId": "ID", "range": "Sheet1!A1", "valueInputOption": "USER_ENTERED"}' --body '{"values": [["a","b"],["c","d"]]}'
Docs
- Read doc:
gws docs documents get --params '{"documentId": "ID"}'
Sending email
Always send with the copy of gws that ships with this skill (full path — it
works on every agent, while the gws on your PATH may be too old to send):
/root/.openclaw/skills/google-workspace/scripts/gws gmail +send --to a@x.com --subject "…" --body "…"
# optional: --cc b@y.com,c@z.com --bcc … --html --account other@gmail.com
# --attach /path/file.pdf (repeat for more files; -a works too)
- Show the user the email and send only when they approve it.
- MyClawBots adds an unsubscribe link to every email it sends, and the recipient can use it at any time. Do not remove or work around it.
- If sending fails with "unsubscribed", that person opted out of the user's emails. Never try to reach them another way (another account, another tool, Outlook, a different address of theirs). Tell the user.
- One email to the people involved, never the same message to a list. At most 20 recipients per email.
- Attachments: up to 10 files, 18 MB in total. Bigger files: share a Drive link in the body instead.
- No Gmail drafts.
+reply,+reply-alland+forwardare not available — send a new email with "Re: …" in the subject instead.
Proactive inbox triage
Applies only when GWS_GOOGLE_ACCOUNT is non-empty. If it is empty, this
section does not apply: do nothing, and do not mention Gmail at all.
On each scheduled check, look at recent mail and report only what needs the user's attention. The default outcome is silence — most inboxes, most of the time, contain nothing worth interrupting someone for.
gws gmail users messages list --params '{"userId": "me", "q": "is:unread newer_than:1d -category:promotions -category:social -category:updates", "maxResults": 25}'
Report these:
- Time-sensitive mail — deadlines, invoices or payments due, renewals, legal or account notices
- Threads where someone is visibly waiting on the user's reply
- Meeting invites that need a response
- Security and account notices — sign-in alerts, password resets, billing failures
Stay silent on these:
- Newsletters, promotions, social and updates categories
- Automated notifications that need no action
- Anything already read, replied to, or archived
- Routine mail that merely arrived
Rules:
- If nothing qualifies, say nothing. Do not send "nothing to report", do not send a summary of how many messages you checked, do not announce that you looked. An empty inbox scan produces no message at all.
- When something does qualify, report at most 5 items, most urgent first, one line each: sender — subject — why it matters — how old it is.
- Summarise. Never paste full message bodies, and keep addresses, phone numbers and account identifiers out of the report unless the user asks for them.
- Read only. Never reply, archive, label or delete during a triage pass — say what you found and let the user decide.
Notes
- All
gwsoutput is structured JSON - For pagination, use
--page-allor--page-limit N - Always confirm before sending emails or creating calendar events
- For scripting, pipe output through
jq
Scripts
1 file ship with this skill. They are installed beside the SKILL.md.
#!/bin/bash
# gws-proxy — Fetches a fresh access token from the proxy, then delegates to gws.
# Two copies on every agent, same file: /usr/local/bin/gws (baked into the
# image) and /root/.openclaw/skills/google-workspace/scripts/gws (synced with
# the skill at boot, so it is current even on an older image). Multi-account
# work uses the skill copy for that reason.
#
# Env vars used:
# GWS_TOKEN_PROXY_URL — e.g. http://my-claw-bots-prod.internal:3001/google/token
# PROXY_API_KEY — API key for the proxy
# GWS_ACCOUNT — optional: which connected Google account to act as
#
# If both are set, fetches a short-lived access token and exports
# GOOGLE_WORKSPACE_CLI_TOKEN so gws uses it automatically.
#
# Multiple Google accounts (the gws CLI itself dropped multi-account support,
# so the account is chosen here, before gws runs):
# gws accounts — list connected accounts, default first
# gws --account work@x.com gmail … — act as that account for this call
# Without --account (or GWS_ACCOUNT) the agent's default account is used.
set -euo pipefail
GWS_REAL="${GWS_REAL_BIN:-/usr/local/bin/gws-real}"
ACCOUNT="${GWS_ACCOUNT:-}"
ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
--account)
if [ $# -lt 2 ] || [ -z "$2" ]; then
echo "gws-proxy: --account needs an email address (see: gws accounts)" >&2
exit 2
fi
ACCOUNT="$2"
shift 2
;;
--account=*)
ACCOUNT="${1#--account=}"
shift
;;
*)
ARGS+=("$1")
shift
;;
esac
done
# ── Sending Gmail ────────────────────────────────────────────────────────
# Agent access tokens cannot send mail: every email goes through the
# api-proxy's /google/send, which refuses recipients who unsubscribed and adds
# an unsubscribe link + List-Unsubscribe headers (Google data-safety review,
# 2026-10-03). So `gmail +send` is handled here, and the other send-capable
# helpers point at it instead of failing with a permissions error.
if [ "${ARGS[0]:-}" = "gmail" ]; then
case "${ARGS[1]:-}" in
+reply|+reply-all|+forward)
echo "gws-proxy: ${ARGS[1]} is not available. Use: gws gmail +send --to <email> --subject \"Re: …\" --body \"…\"" >&2
exit 2 ;;
esac
case "${ARGS[1]:-} ${ARGS[2]:-} ${ARGS[3]:-}" in
"users messages send"|"users drafts send"|"users messages import"|"users messages insert")
echo "gws-proxy: send email with: gws gmail +send --to <email> --subject \"…\" --body \"…\"" >&2
exit 2 ;;
esac
if [ "${ARGS[1]:-}" = "+send" ]; then
if [ -z "${GWS_TOKEN_PROXY_URL:-}" ] || [ -z "${PROXY_API_KEY:-}" ]; then
echo "gws-proxy: Google Workspace is not configured on this machine" >&2
exit 1
fi
TO=""; CC=""; BCC=""; SUBJECT=""; BODY=""; HTML=false; ATTACH=()
i=2
while [ $i -lt ${#ARGS[@]} ]; do
flag="${ARGS[$i]}"; val="${ARGS[$((i+1))]:-}"
case "$flag" in
--to) TO="$val"; i=$((i+2)) ;;
--cc) CC="$val"; i=$((i+2)) ;;
--bcc) BCC="$val"; i=$((i+2)) ;;
--subject) SUBJECT="$val"; i=$((i+2)) ;;
--body) BODY="$val"; i=$((i+2)) ;;
--html) HTML=true; i=$((i+1)) ;;
--from) i=$((i+2)) ;; # the account is chosen with --account
--attach|-a)
[ -n "$val" ] || { echo "gws-proxy: --attach needs a file path" >&2; exit 2; }
[ -f "$val" ] && [ -r "$val" ] || { echo "gws-proxy: cannot read attachment: $val" >&2; exit 2; }
ATTACH+=("$val"); i=$((i+2)) ;;
--draft)
echo "gws-proxy: Gmail drafts are not available; show the user the text and send once they approve" >&2
exit 2 ;;
--dry-run)
echo "gws-proxy: dry run — would send to ${TO:-?}: ${SUBJECT:-?}"; exit 0 ;;
*) echo "gws-proxy: unknown option for gmail +send: $flag" >&2; exit 2 ;;
esac
done
# Attachments go base64 through temp files: a few MB would overflow
# the argument limit if passed to jq/curl on the command line.
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
echo '[]' > "$WORK/att.json"
for f in ${ATTACH[@]+"${ATTACH[@]}"}; do
base64 < "$f" | tr -d '\n' > "$WORK/data.b64"
jq --arg name "$(basename "$f")" --rawfile data "$WORK/data.b64" \
'. + [{filename: $name, data: $data}]' "$WORK/att.json" > "$WORK/att.next" && mv "$WORK/att.next" "$WORK/att.json"
done
jq -cn --arg agent "${PROJECT_ID:-}" --arg account "$ACCOUNT" --arg to "$TO" --arg cc "$CC" \
--arg bcc "$BCC" --arg subject "$SUBJECT" --arg body "$BODY" --argjson html "$HTML" \
--slurpfile att "$WORK/att.json" \
'{agent_id: $agent, to: $to, cc: $cc, bcc: $bcc, subject: $subject, body: $body, html: $html}
+ (if $account == "" then {} else {account: $account} end)
+ (if ($att[0] | length) > 0 then {attachments: $att[0]} else {} end)' > "$WORK/body.json"
RESP=$(curl -s -X POST -H "x-api-key: $PROXY_API_KEY" -H "Content-Type: application/json" \
--data-binary @"$WORK/body.json" "${GWS_TOKEN_PROXY_URL%/token}/send" 2>/dev/null || true)
if echo "$RESP" | jq -e '.id' >/dev/null 2>&1; then
echo "$RESP" | jq -r '"Sent from \(.from) to \(.to | join(", "))\((if (.cc|length)>0 then " (cc \(.cc|join(", ")))" else "" end))\((if ((.attachments // [])|length)>0 then " with \(.attachments|join(", "))" else "" end)). Message id: \(.id). It includes an unsubscribe link."'
exit 0
fi
DETAIL=$(echo "$RESP" | jq -r '[.error, .detail] | map(select(. != null)) | join(" — ")' 2>/dev/null || true)
echo "gws-proxy: not sent — ${DETAIL:-could not reach the send service}" >&2
exit 1
fi
fi
if [ "${ARGS[0]:-}" = "accounts" ]; then
if [ -z "${GWS_TOKEN_PROXY_URL:-}" ] || [ -z "${PROXY_API_KEY:-}" ]; then
echo "gws-proxy: Google Workspace is not configured on this machine" >&2
exit 1
fi
ACCOUNTS_URL="${GWS_TOKEN_PROXY_URL%/token}/accounts"
if [ -n "${PROJECT_ID:-}" ]; then
ACCOUNTS_URL="${ACCOUNTS_URL}?agent_id=${PROJECT_ID}"
fi
ACCOUNTS_JSON=$(curl -sf -H "x-api-key: $PROXY_API_KEY" "$ACCOUNTS_URL" 2>/dev/null || true)
if [ -z "$ACCOUNTS_JSON" ]; then
echo "gws-proxy: could not list Google accounts" >&2
exit 1
fi
echo "$ACCOUNTS_JSON" | jq -r '
if (.accounts | length) == 0 then "No Google account is connected."
else .accounts[] | .email + (if .default then " (default)" else "" end) end'
exit 0
fi
if [ -n "${GWS_TOKEN_PROXY_URL:-}" ] && [ -n "${PROXY_API_KEY:-}" ]; then
REQUEST_BODY=$(jq -cn --arg agent "${PROJECT_ID:-}" --arg account "$ACCOUNT" \
'{agent_id: $agent} + (if $account == "" then {} else {account: $account} end)')
# No -f: a 404 for an unknown account carries the list of real ones.
TOKEN_JSON=$(curl -s -X POST \
-H "x-api-key: $PROXY_API_KEY" \
-H "Content-Type: application/json" \
-d "$REQUEST_BODY" \
"$GWS_TOKEN_PROXY_URL" 2>/dev/null || true)
ACCESS_TOKEN=""
if [ -n "$TOKEN_JSON" ]; then
ACCESS_TOKEN=$(echo "$TOKEN_JSON" | jq -r '.access_token // empty' 2>/dev/null || true)
fi
if [ -n "$ACCESS_TOKEN" ]; then
export GOOGLE_WORKSPACE_CLI_TOKEN="$ACCESS_TOKEN"
elif [ -n "$ACCOUNT" ]; then
# Never fall back to the default account: that would act as the wrong
# person (send from the wrong address, write to the wrong calendar).
DETAIL=$(echo "$TOKEN_JSON" | jq -r '[.error, .detail] | map(select(. != null)) | join(": ")' 2>/dev/null || true)
KNOWN=$(echo "$TOKEN_JSON" | jq -r '(.accounts // []) | join(", ")' 2>/dev/null || true)
echo "gws-proxy: cannot use Google account $ACCOUNT${DETAIL:+ — $DETAIL}" >&2
if [ -n "$KNOWN" ]; then
echo "gws-proxy: connected accounts: $KNOWN" >&2
fi
exit 1
elif [ -n "$TOKEN_JSON" ]; then
echo "gws-proxy: WARNING — token response did not contain access_token" >&2
echo "gws-proxy: response: $TOKEN_JSON" >&2
else
echo "gws-proxy: WARNING — failed to fetch token from $GWS_TOKEN_PROXY_URL" >&2
fi
elif [ -n "$ACCOUNT" ]; then
echo "gws-proxy: Google Workspace is not configured on this machine" >&2
exit 1
fi
# Delegate to the real gws binary (renamed during Docker build). It carries a
# platform token, never a vault stand-in, so it bypasses the vault HTTPS proxy
# the exec wrapper may have set: it must not depend on the vault being up
# (docs/secrets-vault-v2.md D17).
exec env -u HTTPS_PROXY -u https_proxy -u NODE_USE_ENV_PROXY "$GWS_REAL" ${ARGS[@]+"${ARGS[@]}"}