If your OpenClaw agent still polls Gmail every couple of minutes, you are paying the latency tax. Gmail’s watch → Pub/Sub → push pipeline lets you know about new mail in seconds, not minutes. Below is the exact sequence I used to wire real-time Gmail events into an OpenClaw workflow running on ClawCloud. The whole thing sits on top of Google Cloud Pub/Sub; no cron jobs, no busy loops.

Why bother with Gmail push notifications?

Polling looks easy—until you count API quota. Gmail allows 500 users.messages.list calls per minute per project. With multiple labels or users, you either cache aggressively or hit the limit. Push notifications remove 99 % of those calls. You still need an occasional users.watch refresh (every 7 days) and a follow-up users.messages.get per new mail, but the constant listing traffic disappears.

Prerequisites and versions used

  • Google Cloud SDK 462.0.0 (anything ≥ 430 works)
  • Node.js 22.3.0 on the OpenClaw side
  • OpenClaw v0.18.4 (current HEAD on main)
  • Gmail API enabled for the target Google Workspace or @gmail.com account
  • A ClawCloud workspace with a running gateway URL (mine was https://agent-catnip.claw.run)

1. Create / configure the Google Cloud project

1.1 Spin up the project

If you already have a GCP project with both Gmail API and Pub/Sub enabled, skip to the next H2.

Otherwise:

# Pick a project ID that is unique PROJECT_ID="openclaw-gmail-hooks" gcloud projects create "$PROJECT_ID" --set-as-default # Enable required services SERVICES="gmail.googleapis.com pubsub.googleapis.com cloudbuild.googleapis.com" for s in $SERVICES; do gcloud services enable $s --project $PROJECT_ID done

1.2 Service account for Pub/Sub push

Pub/Sub will push messages to our OpenClaw webhook. I let Pub/Sub sign the JWT itself; no extra Cloud Run auth step.

gcloud iam service-accounts create pubsub-openclaw \ --description="Push Gmail notifications to OpenClaw" \ --project $PROJECT_ID # Grant Pub/Sub Publisher to Gmail and Cloud Pub/Sub Admin to ourselves PUBSUB_SA="service-$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')@gcp-sa-pubsub.iam.gserviceaccount.com" OUR_SA="pubsub-openclaw@$PROJECT_ID.iam.gserviceaccount.com" gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:$PUBSUB_SA" --role="roles/pubsub.publisher" gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:$OUR_SA" --role="roles/pubsub.admin"

The extra admin role is only needed while we iterate. Drop it later for principle-of-least-privilege.

2. Create the Pub/Sub topic and push subscription

2.1 Topic

gcloud pubsub topics create gmail-events --project $PROJECT_ID

2.2 Expose an OpenClaw webhook

Inside the OpenClaw gateway (~/openclaw/gateway/config/production.yaml):

webhooks: gmailPubSub: path: "/hooks/gmail" method: "POST" # OpenClaw will verify JWT before handing over to tools verifyGoogleJwt: true handler: "./workflows/gmail-handler.ts"

Commit & push. ClawCloud redeploys in ~15 s. The public endpoint is now:

https://agent-catnip.claw.run/hooks/gmail

2.3 Subscription

gcloud pubsub subscriptions create gmail-events-to-openclaw \ --topic gmail-events \ --push-endpoint="https://agent-catnip.claw.run/hooks/gmail" \ --push-auth-service-account=$OUR_SA \ --ack-deadline=30 \ --project $PROJECT_ID

The --ack-deadline of 30 s is enough for the typical OpenClaw tool-chain fan-out; adjust if your workflow does heavier work inline.

3. Give Gmail permission to publish

This is the step everybody misses and then Google responds with HTTP 404. Gmail itself must be able to publish to the topic. Run:

gcloud pubsub topics add-iam-policy-binding gmail-events \ --member="serviceAccount:gmail-api-push@system.gserviceaccount.com" \ --role="roles/pubsub.publisher" \ --project $PROJECT_ID

Without this, the users.watch call will return error 404: Topic not found even though the topic exists.

4. Fire the users.watch call

Your OpenClaw integration key comes from Composio but the watch endpoint has to be called with the Gmail OAuth token for the end user. The quickest way is:

  1. Use Google’s OAuth playground to grab a fresh access token with https://www.googleapis.com/auth/gmail.readonly.
  2. Replace the token in the curl below.
ACCESS_TOKEN="ya29.a0AfB..." # 1-hour lifetime curl -X POST \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "labelIds": ["INBOX"], "topicName": "projects/'$PROJECT_ID'/topics/gmail-events", "labelFilterAction": "include" }' \ "https://gmail.googleapis.com/gmail/v1/users/me/watch"

The response contains historyId and expiration. Note that Gmail kills the watch after 7 days or when the user removes access. Add an OpenClaw cron.task to renew it daily:

import { gmail } from "openclaw/tools/gmail"; cron("0 3 * * *", async () => { await gmail.watch({ labelIds: ["INBOX"], topic: "projects/${process.env.PROJECT_ID}/topics/gmail-events", }); });

5. Handling Pub/Sub messages inside OpenClaw

5.1 Minimal TypeScript handler

import { gmail } from "openclaw/tools/gmail"; import { store } from "openclaw/memory"; export default async function handle(req, res) { // 1. Google verifies JWT; we trust X-Goog-Channel-* const message = JSON.parse(Buffer.from(req.body.message.data, "base64").toString()); const { emailAddress, historyId } = message; // 2. Fetch the delta const history = await gmail.users.history.list({ userId: "me", startHistoryId: historyId - 1, historyTypes: ["messageAdded"] }); for (const h of history.data.history || []) { for (const m of h.messages || []) { const full = await gmail.users.messages.get({ userId: "me", id: m.id, format: "metadata", metadataHeaders: ["Subject", "From"] }); // 3. Persist subject for later context const subj = full.data.payload.headers.find(h => h.name === "Subject").value; await store.set(`email:${m.id}:subject`, subj); // 4. Kick off a downstream workflow, e.g. auto-label await run("label-new-mail", { messageId: m.id }); } } res.status(204).end(); // ack }

I keep handler latency under 10 s so the 30 s ack deadline never bites me. If you need fan-out to Slack/Discord HTTP calls, push those onto an internal queue instead of blocking the webhook reply.

6. Security knobs worth tightening

  • JWT audience: The token Google signs uses the push endpoint URL as aud. OpenClaw’s verifyGoogleJwt middleware checks that by default. No action required unless you alias the domain.
  • VPC Service Controls: If your compliance team blocks public internet Pub/Sub pushes, switch to pull and let OpenClaw poll Pub/Sub instead. More code, no external traffic.
  • Least privilege: After setup, drop roles/pubsub.admin from $OUR_SA. It only needs Subscriber.

7. Troubleshooting checklist

The Gmail watch flow is notoriously opaque. These are the errors I hit and what fixed them:

  • 404 Topic not found on users.watch → forgot to grant gmail-api-push@system.gserviceaccount.com publisher rights.
  • No messages arriving → the subscription is pull, not push. Re-create with --push-endpoint.
  • HTTP 401 on webhook → mismatch between --push-auth-service-account and the aud your handler expects. Print req.jwtPayload to debug.
  • Messages delayed by minutes → your handler took longer than the ack deadline; Pub/Sub redelivered after backoff. Check Cloud Logging for duplicate message IDs.

8. Operational tips in production

  1. Metrics: Expose gmail_webhook_latency_seconds and gmail_pubsub_redeliveries_total via Prometheus. Hook Grafana alerting at p95 > 5 s.
  2. Backfill: If the agent goes dark for hours, rerun users.messages.list with q="newer_than:2d" to catch up.
  3. Rotation: The daily cron that renews users.watch also rotates the OAuth refresh token if needed. Composio’s secret manager handles storage.
  4. Multi-user: One Pub/Sub topic can host events from multiple mailboxes. The message payload includes emailAddress; route in the handler.

9. Where to go next

At this point your OpenClaw agent reacts to new Gmail messages within ~2 s, persists context, and fans out to any of the 800+ Composio tools. Most teams move on to:

  • Automatic ticket creation in Jira when an email hits support@.
  • Triggering an OpenAI summarizer and posting to Slack.
  • Feeding message subjects into a vector store for RAG-based reply suggestions.

The key takeaway: stop polling, use Gmail’s native push, and let OpenClaw do the heavy lifting.