Gmail Pub/Sub
Deliver Gmail Pub/Sub to OpenClaw webhooks via gogcli.
Goal: Gmail watch β Pub/Sub push β '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gog gmail watch serve'</code>' β OpenClaw webhook.
Prerequisites
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gcloud'</code>' installed and logged in ('<a href="https://docs.cloud.google.com/sdk/docs/install-sdk" className="text-emerald-400 hover:text-emerald-300 transition-colors">'install guide'</a>').
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gog'</code>' (gogcli) installed and authorized for your Gmail account ('<a href="https://gogcli.sh/" className="text-emerald-400 hover:text-emerald-300 transition-colors">'gogcli.sh'</a>').
- OpenClaw hooks enabled (see '<a href="/automation/webhook" className="text-emerald-400 hover:text-emerald-300 transition-colors">'Webhooks'</a>').
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'tailscale'</code>' logged in ('<a href="https://tailscale.com/" className="text-emerald-400 hover:text-emerald-300 transition-colors">'tailscale.com'</a>'). Supported setup uses Tailscale Funnel as public HTTPS endpoint.
Other tunnel services work but are DIY/unsupported and require manual wiring.
Currently, we support Tailscale.
Example hook configuration (enables Gmail preset mapping):
{
hooks: {
enabled: true,
token: "OPENCLAW_HOOK_TOKEN",
path: "/hooks",
presets: ["gmail"],
},
}To deliver Gmail summaries to chat, use mapping to override preset
Set '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'deliver'</code>' + optional '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'channel'</code>'/'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'to'</code>':
{
hooks: {
enabled: true,
token: "OPENCLAW_HOOK_TOKEN",
presets: ["gmail"],
mappings: [
{
match: { path: "gmail" },
action: "agent",
wakeMode: "now",
name: "Gmail",
sessionKey: "hook:gmail:{{messages[0].id}}",
messageTemplate: "New email from {{messages[0].from}}
Subject: {{messages[0].subject}}
{{messages[0].snippet}}
{{messages[0].body}}",
model: "openai/gpt-5.2-mini",
deliver: true,
channel: "last",
// to: "+15551234567"
},
],
},
}If you want a fixed channel, set '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'channel'</code>' + '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'to'</code>'. Otherwise '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'channel: "last"'</code>'
uses the last delivery route (falls back to WhatsApp).
To force Gmail to run a cheaper model, set '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'model'</code>' in mapping
('<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'provider/model'</code>' or alias). If you enforce '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'agents.defaults.models'</code>', include it there.
To set default model and thinking level specifically for Gmail hooks, add
'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.model'</code>' / '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.thinking'</code>' in config:
{
hooks: {
gmail: {
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
thinking: "off",
},
},
}Notes:
- Per-hook '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'model'</code>'/'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'thinking'</code>' in mapping still overrides these defaults.
- Fallback order: '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.model'</code>' β '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'agents.defaults.model.fallbacks'</code>' β primary (auth/rate limit/timeout).
- If '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'agents.defaults.models'</code>' is set, Gmail model must be in allowlist.
- By default, Gmail hook content is wrapped with external content safety boundary.
To disable (dangerous), set '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.allowUnsafeExternalContent: true'</code>'.
To further customize payload handling, add '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.mappings'</code>' or JS/TS transform modules
under '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.transformsDir'</code>' (see '<a href="/automation/webhook" className="text-emerald-400 hover:text-emerald-300 transition-colors">'Webhooks'</a>').
Wizard (recommended)
Use OpenClaw assistant to wire everything together (installs deps via brew on macOS):
openclaw webhooks gmail setup --account [email protected]
Defaults:
- Uses Tailscale Funnel as public push endpoint.
- Writes '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail'</code>' config for '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'openclaw webhooks gmail run'</code>'.
- Enables Gmail hook preset ('<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.presets: ["gmail"]'</code>').
Path notes: When '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'tailscale.mode'</code>' is enabled, OpenClaw automatically sets
'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.serve.path'</code>' to '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'/'</code>' and keeps public path at
'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.tailscale.path'</code>' (default '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'/gmail-pubsub'</code>') because Tailscale
strips set-path prefix before proxy.
If backend needs to receive prefix path, set
'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.tailscale.target'</code>' (or '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'--tailscale-target'</code>') to full URL, e.g.
'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'http://127.0.0.1:8788/gmail-pubsub'</code>' and match '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.serve.path'</code>'.
Want custom endpoint? Use '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'--push-endpoint <url>'</code>' or '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'--tailscale off'</code>'.
Platform notes: On macOS, wizard installs '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gcloud'</code>', '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gogcli'</code>' and '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'tailscale'</code>'
via homebrew; on Linux install them manually first.
Gateway auto-start (recommended):
- When '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.enabled=true'</code>' and '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'hooks.gmail.account'</code>' are set, gateway starts
'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gog gmail watch serve'</code>' and auto-updates watch.
- Set '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'OPENCLAW_SKIP_GMAIL_WATCHER=1'</code>' to opt-out (useful if you run daemon yourself).
- Don't run manual daemon concurrently or you'll get
'<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'listen tcp 127.0.0.1:8788: bind: address already in use'</code>'.
Manual daemon (start '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gog gmail watch serve'</code>' + auto-renewal):
openclaw webhooks gmail run
One-time setup
1. Select GCP project that owns the OAuth client used by '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gog'</code>'.
gcloud auth login gcloud config set project <project-id>
Note: Gmail watch requires Pub/Sub topic to be in same project as OAuth client.
2. Enable APIs:
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
3. Create topic:
gcloud pubsub topics create gog-gmail-watch
4. Allow Gmail push to publish:
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch --member=serviceAccount:[email protected] --role=roles/pubsub.publisher
Start watch
gog gmail watch start --account [email protected] --label INBOX --topic projects/<project-id>/topics/gog-gmail-watch
Save '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'history_id'</code>' from output (for debugging).
Run push handler
Local example (shared token auth):
gog gmail watch serve --account [email protected] --bind 127.0.0.1 --port 8788 --path /gmail-pubsub --token <shared> --hook-url http://127.0.0.1:18789/hooks/gmail --hook-token OPENCLAW_HOOK_TOKEN --include-body --max-bytes 20000
Notes:
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'--token'</code>' protects push endpoint ('<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'x-gog-token'</code>' or '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'?token='</code>').
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'--hook-url'</code>' points to OpenClaw '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'/hooks/gmail'</code>' (mapping; isolated run + summary to main).
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'--include-body'</code>' and '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'--max-bytes'</code>' control body snippet sent to OpenClaw.
Recommended: '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'openclaw webhooks gmail run'</code>' wraps same flow and auto-updates watch.
Expose handler (advanced, unsupported)
If you need non-Tailscale tunnel, wire manually and use public URL in push
subscription (unsupported, no guardrails):
cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate
Use generated URL as push endpoint:
gcloud pubsub subscriptions create gog-gmail-watch-push --topic gog-gmail-watch --push-endpoint "https://<public-url>/gmail-pubsub?token=<shared>"
Production: Use stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run:
gog gmail watch serve --verify-oidc --oidc-email <svc@...>
Test
Send message to watched inbox:
gog gmail send --account [email protected] --to [email protected] --subject "watch test" --body "ping"
Check watch status and history:
gog gmail watch status --account [email protected] gog gmail history --account [email protected] --since <historyId>
Troubleshooting
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'Invalid topicName'</code>': project mismatch (topic not in OAuth client project).
- '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'User not authorized'</code>': topic missing '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'roles/pubsub.publisher'</code>'.
- Empty messages: Gmail push only provides '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'historyId'</code>'; fetch via '<code className="bg-white/10 px-1.5 py-0.5 rounded text-emerald-300 text-sm">'gog gmail history'</code>'.
Cleanup
gog gmail watch stop --account [email protected] gcloud pubsub subscriptions delete gog-gmail-watch-push gcloud pubsub topics delete gog-gmail-watch