← All skills
  • 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 named google_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 (or gws returns 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 setup or gws 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_ACCOUNT is empty or gws commands 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 gws with the default exec settings — NEVER pass host=sandbox (there is no sandbox runtime on this machine and the command will fail).

How it works

  • GWS_GOOGLE_ACCOUNT is 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 gws commands 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 gws command. Without --account, it acts as the default account, exactly like plain gws.
  • When the user names an account ("from my work email", "on my personal calendar"), run gws accounts and 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 --account fails with "not connected", show the user the accounts the accounts command 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-all and +forward are 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 gws output is structured JSON
  • For pagination, use --page-all or --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.

scripts/gws9.0 KB
#!/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[@]}"}