Gmail connector setup

Operator·5 minutes to read

This page is the setup half — creating the OAuth app Gmail needs in Google's own console and getting its client id and secret into this deployment. Authorising a connection afterwards is the Authenticate control on the plugin sheet, which is what actually sends you to Google.

1. Create the OAuth app in Google Cloud

  1. Open the Google Cloud console and create a project (or pick an existing one) — this is your own project, not Botseon's.

  2. APIs & Services → Library: enable the Gmail API.

  3. APIs & Services → OAuth consent screen: choose External (unless your Google Workspace restricts it to internal), fill in an app name and your own e-mail as the support and developer contact. Publishing status can stay Testing for personal use — Google shows an "unverified app" screen for a testing app, which you will see and click through once; that screen, not a shared secret, is the actual cost of bringing your own app.

  4. APIs & Services → Credentials → Create Credentials → OAuth client ID, application type Web application.

  5. Under Authorized redirect URIs, add the redirect URL from docs/connectors/README.md — with the quickstart's default BOTSEON_PUBLIC_URL, that is:

    http://127.0.0.1:3000/api/oauth/gmail/callback
    
  6. Create it. Google shows you a client ID and a client secret once — copy both now.

2. Scopes

Request the narrowest set that covers triage-and-draft use (F-PLUG-1: minimum scopes per capability, read and write requested separately):

  • https://www.googleapis.com/auth/gmail.readonly — read messages and threads; no writes.
  • https://www.googleapis.com/auth/gmail.modify — read, label, archive, draft and move to the bin. Covers "draft a reply for approval" without granting permanent deletion or mailbox-settings access.
  • https://www.googleapis.com/auth/gmail.send — send a message the member approved.

The connector's catalogue row is what actually requests scopes at authorisation time (the scopes field in extensions/connectors/gmail/manifest.json: the read scope on Authenticate, all three on Allow sending); these are the three to have configured and ready on the OAuth consent screen above.

3. Enter the app

One command, with the client secret on standard input — there is no flag that takes it and no environment variable that holds it (see docs/connectors/README.md):

printf %s "$CLIENT_SECRET" | botseon oauth-app set \
  --org <organisation id> --slug gmail \
  --client-id <the client ID from step 1> \
  --auth-url https://accounts.google.com/o/oauth2/v2/auth \
  --token-url https://oauth2.googleapis.com/token \
  --revoke-url https://oauth2.googleapis.com/revoke \
  --configured-by <your user id>

It stores the secret encrypted and prints the redirect URL from step 1 back, so you can check the two match. botseon oauth-app show --org <organisation id> --slug gmail says whether an app is configured and under which client id; nothing prints the secret back.

Never put either value in a bot's description, a memory, or anywhere else a model reads as ordinary text.

4. What a bot can do with the connection

ToolWhat it doesAsks the member first?
gmail.search, gmail.get_messageFind and read mail; a message lists each attachment's name, type and sizeNo
gmail.get_attachmentRead a text or PDF attachment's text (a scan or an image is reported, not guessed)No
gmail.create_draftLeave a draft, with any attached files, in the member's Drafts; nothing is sentYes, unless the member always-allows it
gmail.apply_labels, gmail.create_labelLabel mail, and create a labelYes, unless the member always-allows it
gmail.reply, gmail.send, gmail.forwardSend mail; the card names the recipient and lists any attached fileYes, unless the member always-allows it
gmail.trashMove a message to the bin (recoverable in Gmail)Yes, every time: no always-allow rule covers it

5. Attached files: what to check when one does not go out

gmail.send, gmail.reply and gmail.create_draft take an attachments list: a file already in the mailbox (messageId and the attachmentId that gmail.get_message lists) or a text file the bot writes (filename and content). A mail with files goes up through Gmail's upload endpoint (https://gmail.googleapis.com/upload/gmail/v1/users/me/...?uploadType=multipart), on the same host as every other Gmail call, so nothing changes in your egress rules. No extra scope is needed: the three above already cover the download, the draft and the send. The request shapes and the limits are in docs/research/connectors/gmail-attachments-2026-10-21.md.

A member who reports "the file did not go out" has had one of these sentences in the run. Each is a refusal the driver gives before anything is sent:

The run saysWhat it meansWhat to do
"One of the files is a type Gmail blocks …"A program or script file (Gmail's own list of 54 types)Nothing on your side: Gmail never sends these. The member can share it another way, such as a Drive link
"The files add up to more than 25 MB …"Over Gmail's limit for one mailThe member sends fewer files per mail
"Each attachment is either a file already in the mailbox … or a text file to write …"The model mixed up the two kindsNothing to fix; the bot corrects the call
"that message has no such attachment"The attachment id is not on that message (often an old id after the message changed)The bot reads the message again and uses the id it lists now
"Gmail rejected that request as invalid"Gmail's own 400, for example an archive with a blocked file inside, or a Workspace admin's lower size limitCheck the Workspace admin's Gmail attachment settings

The nightly connector-live run checks this path against the Google test account: the case "attaches a mailbox file and a written file to a mail, and reads both back" in extensions/connectors/gmail/driver.live.test.ts. If it fails while the other Gmail cases pass, the upload endpoint or its shape changed. Read the upload guide again and update the research note.

Last verified against build c0f77aa.