Gmail
Receive labelled emails and send threaded replies, or use standalone Gmail APIs without the Chat runtime.
Create a new bot
Scaffold a Next.js bot with the Gmail adapter using create-chat-sdk:
pnpm create chat-sdk@latest my-bot --adapter gmail memory -yThe generated app uses the Memory state adapter for local development. To add the adapter to an existing app, start at Install.
Install
pnpm add @chat-adapter/gmailQuick start
The adapter reads its GMAIL_* configuration from the environment. Complete Platform setup before starting a mailbox watch. Use Node.js 20 or newer.
Install Redis state and set REDIS_URL for this example. Shared persistent state preserves mailbox cursors and processed-message receipts across restarts.
pnpm add @chat-adapter/state-redisEligible incoming emails with the configured label are delivered as mentions. If several arrive in the same conversation during one sync, the adapter dispatches the newest and leaves earlier emails available through thread history. Sending a reply does not require an @mention in the email.
thread.post() sends a real email immediately. The intake label controls which messages trigger handlers, not which mailbox data the OAuth token can read. Authorize senders before giving them access to privileged actions.
Platform setup
Authorize a mailbox
Enable the Gmail API and Cloud Pub/Sub API in your Google Cloud project. Configure an OAuth consent screen and OAuth client, then authorize the mailbox user with Google's web-server OAuth flow. Request offline access (access_type=offline) to obtain a refresh token.
Use these Gmail scopes:
| Scope | Purpose |
|---|---|
https://www.googleapis.com/auth/gmail.readonly | Read messages, labels and history, and watch the mailbox |
https://www.googleapis.com/auth/gmail.send | Send replies |
https://www.googleapis.com/auth/gmail.compose | Optional, for drafts through the standalone API |
The adapter refreshes access tokens but does not implement the consent flow. A revoked or expired refresh token requires renewed authorization. Before a public launch, review Google's verification requirements for sensitive and restricted scopes.
Configure Pub/Sub
Follow Gmail's push setup and authenticated push subscription guide:
- Create a topic in the Google Cloud project making the Gmail watch request.
- Grant
gmail-api-push@system.gserviceaccount.comthe Pub/Sub Publisher role on that topic. - Create a separate service account for push authentication, such as
gmail-push@PROJECT.iam.gserviceaccount.com. - Ensure the Pub/Sub service agent can create OIDC tokens for that account. Grant Service Account Token Creator on the push account if needed. The operator configuring the subscription also needs permission to act as that account.
- Create an authenticated push subscription targeting
https://your-domain.com/api/webhooks/gmail. Select the push service account and set the audience to that same URL. Keep payload wrapping enabled.
The publisher identity sends events to the topic; the push service account authenticates delivery to your webhook. They are not interchangeable. If your host requires separate platform authentication, configure that as well.
Select the intake label
Create a Gmail label, such as Agent, then use listGmailLabels() from @chat-adapter/gmail/api to find its ID:
Set the environment variables with your own project and mailbox values. Environment variables describes each one.
Labels apply to individual messages. Labelling a conversation affects its existing messages, not future replies. Use a Gmail filter if follow-ups should enter automatically. thread.subscribe() does not apply Gmail labels.
Start and renew the watch
After the webhook is reachable, run this from a server-side setup job with the same environment and state store:
Schedule watch() daily and a separate periodic sync() to recover missed notifications. Google requires watch renewal at least every seven days and recommends daily renewal. The adapter does not start a scheduler.
The first watch saves a starting cursor; it does not import previously labelled mail. Send or label a new incoming email after setup to test delivery. Renewing a watch preserves the existing cursor, so it does not skip pending changes.
Configuration
Prop
Type
Provide either clientId, clientSecret and refreshToken, or accessToken. Explicit configuration takes precedence over environment variables; explicit OAuth credentials also take precedence over GMAIL_ACCESS_TOKEN. Only the exact environment value true enables reply-all.
Environment variables
| Variable | Required | Description |
|---|---|---|
GMAIL_MAILBOX | Yes | Mailbox email address. Use the actual address, not me |
GMAIL_LABEL_ID | Yes | ID of the Gmail label that marks incoming handoffs |
GMAIL_SUBSCRIPTION | Yes | Full Pub/Sub subscription name |
GMAIL_CLIENT_ID | Yes, unless GMAIL_ACCESS_TOKEN is set | OAuth client ID |
GMAIL_CLIENT_SECRET | Yes, unless GMAIL_ACCESS_TOKEN is set | OAuth client secret |
GMAIL_REFRESH_TOKEN | Yes, unless GMAIL_ACCESS_TOKEN is set | Mailbox user's OAuth refresh token |
GMAIL_ACCESS_TOKEN | No | Access token used instead of OAuth refresh credentials |
GMAIL_PUBSUB_AUDIENCE | Yes, unless webhookVerifier is set | Expected audience of the Pub/Sub push JWT |
GMAIL_PUBSUB_SERVICE_ACCOUNT_EMAIL | Yes, unless webhookVerifier is set | Service account selected for push authentication |
GMAIL_TOPIC_NAME | For watch() | Full Pub/Sub topic name, unless you pass it to watch(topicName) |
GMAIL_REPLY_ALL | No | Set to true to include the original To and Cc recipients in replies |
Webhooks
Built-in verification checks the Google-signed JWT, issuer, audience, expiration and push service-account email, then validates the wrapped payload and subscription. The root adapter also rejects notifications for a different mailbox.
A custom webhookVerifier replaces JWT authentication and takes precedence over audience and service-account configuration. It may read the request body. Falsy results or thrown errors reject the request; subscription, payload and mailbox checks still apply. Never use an always-true verifier on a public endpoint.
Replies and private delivery
Replies preserve the original subject, Gmail thread ID, In-Reply-To and References, matching Gmail's threading requirements. By default, the recipient is the original Reply-To, or From when absent.
Set replyAll: true to include the original To and Cc recipients, excluding the configured mailbox and duplicates. Bcc recipients are never copied. The adapter does not discover mailbox aliases or expand mailing lists.
thread.postEphemeral(userId, message, { fallbackToDM: true }) sends a permanent private email to the selected email address only, with no Cc or Bcc. It retains the subject and reply headers in an existing Gmail thread and adds a (private only) notice to the body. The result has usedFallback: true. Without the fallback, the adapter sends nothing.
Outside an active message handler, reply context comes from the latest incoming email. To return to a group conversation after a private exchange, use thread.reply(originalMessageId, message) with the original group message. Replies do not automatically quote earlier bodies or copy attachments.
Thread history can contain both private and group messages. Do not copy private history into a group response or shared model context. A sender's address and a private conversation are not proof that the recipient is authorized to receive secrets.
For a new direct email, call gmail.openDM(address) and post to the returned route. Each post to that recipient route starts a new email with subject Private message; use the returned native thread ID for follow-ups. Calling openDM() alone sends nothing.
Standalone APIs
These entrypoints work without importing the Chat runtime:
| Import | Use |
|---|---|
@chat-adapter/gmail/api | OAuth tokens, messages, drafts, labels, history and watches |
@chat-adapter/gmail/format | MIME parsing, composition and serializable reply context |
@chat-adapter/gmail/webhook | Pub/Sub parsing and authentication |
Use the lower-level APIs when your application owns routing, sessions, approvals and retries:
createGmailDraft() saves without sending. sendGmailMessage() sends immediately. Continuations preserve mailbox identity, recipients, subject and reply headers as serializable data. API options accept a token string or resolver, plus optional fetch and signal.
Native events and history
createGmailWebhookVerifier() verifies authentication and the configured subscription, returning the decoded notification and original wrapped Pub/Sub envelope. It does not read messages, store a cursor or dispatch a Chat event. Check emailAddress against your authorized mailbox before any reads or routing. parseGmailNotification() only parses; it does not authenticate.
listGmailHistory({ startHistoryId, pageToken }, options) returns one page of additions, deletions and label changes without fetching email bodies. History can include sent and received mail. Use the specific change arrays instead of also processing the general messages references, which can duplicate them.
Keep history IDs and page tokens as opaque strings. Follow every page and save the final history cursor only after processing is durable. The notification's historyId is a wake-up signal, not a cursor to save before processing. History order is not email-date order.
listGmailMessages() supports search, label filtering and pagination. getGmailThread() returns ordered message pointers, not hydrated messages. Call getGmailMessage() and parseGmailMessage() only for the emails you need.
With standalone APIs, your application owns mailbox serialization, deduplication, cursor persistence, renewal and expired-history recovery. A 404 for an old history cursor requires a full sync. Finish processing or durably enqueue the verified event before acknowledging Pub/Sub; do not start untracked background work and return success.
Thread IDs
Examples: gmail:YWdlbnRAZXhhbXBsZS5jb20:18c2f0a1b2c3d4e5, gmail:YWdlbnRAZXhhbXBsZS5jb20:dm:YWxleEBleGFtcGxlLmNvbQ.
mailbox and recipient are base64url-encoded email addresses. threadId is the native Gmail thread ID. The channel ID is gmail:{mailbox}, so every thread in a mailbox belongs to one channel.
Limitations
- The root adapter uses durable receipts to suppress already-handled or superseded emails. When history expires, it scans the current intake label, which can include older emails never previously handled.
- Sent mail, drafts, spam and trash do not trigger handoffs. Label eligibility is checked before loading bodies and again before dispatch.
- Oversized or malformed emails and failed handler handoffs are recorded and logged without automatic replay. An acknowledged notification means its changes were accounted for, not that every handler succeeded. Inspect logs and reconcile side effects before retrying.
- Email sending and state writes are not transactional. A failed request can have an unknown send outcome; check the mailbox before sending again. API acceptance does not prove delivery to the recipient.
- Streamed text is buffered into one email, not live-edited. The adapter limits buffered stream text to 1 MiB and raw incoming or composed outgoing MIME to 25 MiB. These are adapter limits, not Gmail quotas.
- Sent messages cannot be edited or recalled through the adapter. Cards render as plain text; buttons do not produce actions.
bot.shutdown()does not unregister the watch. For permanent disconnection, stop scheduled jobs and usestopGmailMailbox()from/api. It stops notifications for the mailbox, not only this label, and does not revoke OAuth consent or erase state.
Feature support
Messaging
| Feature | Supported |
|---|---|
| Post message | |
| Message replies | |
| Edit message | |
| Delete message | |
| File uploads | |
| Streaming | Buffered email |
| Scheduled messages |
Rich content
| Feature | Supported |
|---|---|
| Card format | Plain text |
| Buttons | |
| Link buttons | Text links |
| Select menus | |
| Tables | Plain text |
| Charts | |
| Fields | Plain text |
| Images in cards | |
| Modals |
Conversations
| Feature | Supported |
|---|---|
| Slash commands | |
| Mentions | Label-based intake |
| Add reactions | |
| Remove reactions | |
| Typing indicator | |
| Mark as read | |
| Message edit events | |
| Message delete events | |
| DMs | Email to recipient |
| Ephemeral messages | Permanent private email |
| User lookup | |
| Parent subject | |
| Native client | |
| Custom API endpoint |
Message history
| Feature | Supported |
|---|---|
| Fetch messages | |
| Fetch single message | |
| Fetch thread info | |
| Fetch channel messages | |
| List threads | |
| Fetch channel info | |
| Post channel message |