How to Integrate AI Agents with Google Workspace

Complete guide to connecting AI agents with Google Workspace. Covers Gmail, Google Calendar, Docs, Drive, and Sheets using LangChain, CrewAI, and Lindy AI, with working Python code examples and OAuth2 setup.

Before You Start

Prerequisites: Google Workspace account, Google Cloud project with APIs enabled, Service Account or OAuth2 credentials, Basic understanding of AI agent concepts

Works with: LangChain, CrewAI, Lindy AI

Google Workspace is the productivity backbone for millions of organizations — and connecting an AI agent to Gmail, Calendar, Docs, Drive, and Sheets unlocks a level of automation that fundamentally changes how work gets done. An agent with full Google Workspace access can triage your inbox, schedule meetings, synthesize documents, extract data from spreadsheets, and coordinate across all these services in a single workflow.

This guide covers the authentication setup in detail, working Python examples for five Google services, and three end-to-end use cases with production-ready code.

What AI Agents Can Do With Google Workspace Access#

Here is what becomes possible once an agent has authenticated access to each Google service:

Gmail

  • Read and search messages with full body text and attachments
  • Send emails (plain text and HTML) on behalf of a user
  • Apply labels to organize and route messages
  • Mark messages as read, archive, or delete
  • Create draft responses and send when approved

Google Calendar

  • Read events across one or multiple calendars
  • Create new events with attendees, location, video links
  • Update existing events (reschedule, add attendees)
  • Check free/busy availability across a team
  • Delete or cancel events

Google Docs

  • Read document content including structure, tables, and formatting
  • Append content to existing documents
  • Create new documents from templates
  • Replace placeholder text with generated content
  • Share documents with specific users via Drive API

Google Drive

  • Search for files by name, type, content, or metadata
  • Download file content for processing
  • Upload new files and set sharing permissions
  • Move files between folders
  • Create folders and organize file structure

Google Sheets

  • Read cell ranges and entire sheets as structured data
  • Write values to specific cell ranges
  • Append rows to a sheet
  • Create new sheets and format cells
  • Batch update multiple ranges efficiently

For broader context on what agents accomplish across business workflows, see AI agent examples in business.


Authentication: Service Account vs OAuth2#

This is the most important architectural decision in your Google Workspace integration.

Service Account with Domain-Wide Delegation#

Use when: Your agent runs server-side, unattended, and needs to access Workspace resources without an interactive login flow.

How it works: A Service Account is a non-human Google identity. You grant it domain-wide delegation in your Google Workspace Admin Console, which allows it to impersonate any user in your domain and access their data.

Step 1: Create a Google Cloud Project

  1. Go to console.cloud.google.com
  2. Create a new project (e.g., "AI Agents Production")
  3. Enable the APIs you need: Gmail API, Google Calendar API, Google Sheets API, Google Docs API, Google Drive API

Step 2: Create a Service Account

  1. In your Cloud project: IAM & AdminService AccountsCreate Service Account
  2. Name it descriptively (e.g., workspace-agent@your-project.iam.gserviceaccount.com)
  3. No special roles needed at the project level for Workspace access
  4. Click Create and Continue, then Done
  5. Click the service account → KeysAdd KeyCreate new keyJSON
  6. Download and securely store the JSON key file

Step 3: Configure Domain-Wide Delegation

  1. In Google Workspace Admin Console: SecurityAccess and data controlAPI controlsManage Domain Wide Delegation
  2. Click Add new and enter:
    • Client ID: The client_id number from your service account JSON
    • OAuth Scopes: Add only the scopes your agent needs (see scope table below)

OAuth2 (for user-interactive flows)#

Use when: Building a product where end users grant your agent access to their own Google data.

OAuth2 requires a browser-based authorization flow. Download credentials.json from your Google Cloud project (OAuth 2.0 Client ID of type Desktop or Web), then use the google-auth-oauthlib library to run the flow and store the resulting token.json.

Required OAuth Scopes by Service#

| Service | Read scope | Write scope | |---|---|---| | Gmail | https://www.googleapis.com/auth/gmail.readonly | https://www.googleapis.com/auth/gmail.send | | Gmail (labels) | — | https://www.googleapis.com/auth/gmail.modify | | Calendar | https://www.googleapis.com/auth/calendar.readonly | https://www.googleapis.com/auth/calendar | | Docs | https://www.googleapis.com/auth/documents.readonly | https://www.googleapis.com/auth/documents | | Drive | https://www.googleapis.com/auth/drive.readonly | https://www.googleapis.com/auth/drive.file | | Sheets | https://www.googleapis.com/auth/spreadsheets.readonly | https://www.googleapis.com/auth/spreadsheets |


Installation#

pip install langchain langchain-openai google-api-python-client google-auth google-auth-httplib2 python-dotenv

Building Google Workspace Tools#

Gmail Tools#

import os
import base64
import json
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from google.oauth2 import service_account
from googleapiclient.discovery import build
from langchain.tools import tool
from dotenv import load_dotenv

load_dotenv()

SERVICE_ACCOUNT_FILE = os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON")
DELEGATE_EMAIL = os.getenv("GOOGLE_DELEGATE_EMAIL")  # The user to act as


def get_gmail_service():
    """Build an authenticated Gmail API service client using Service Account."""
    credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE,
        scopes=["https://www.googleapis.com/auth/gmail.modify",
                "https://www.googleapis.com/auth/gmail.send"]
    )
    delegated = credentials.with_subject(DELEGATE_EMAIL)
    return build("gmail", "v1", credentials=delegated)


@tool
def search_gmail(query: str, max_results: int = 10) -> str:
    """
    Search Gmail for messages matching a query.
    Use Gmail search syntax, e.g.:
    - 'from:boss@company.com is:unread'
    - 'subject:invoice after:2026/01/01'
    - 'label:agent-queue is:unread'
    Returns message IDs, senders, subjects, and snippets.
    """
    service = get_gmail_service()
    result = service.users().messages().list(
        userId="me",
        q=query,
        maxResults=max_results
    ).execute()

    messages = result.get("messages", [])
    if not messages:
        return f"No messages found for query: {query}"

    summaries = []
    for msg in messages[:5]:  # Fetch details for top 5
        detail = service.users().messages().get(
            userId="me", id=msg["id"], format="metadata",
            metadataHeaders=["From", "Subject", "Date"]
        ).execute()
        headers = {h["name"]: h["value"] for h in detail["payload"]["headers"]}
        summaries.append(
            f"ID: {msg['id']}\n"
            f"  From: {headers.get('From', 'Unknown')}\n"
            f"  Subject: {headers.get('Subject', 'No Subject')}\n"
            f"  Snippet: {detail.get('snippet', '')[:120]}"
        )
    return f"Found {len(messages)} message(s):\n\n" + "\n\n".join(summaries)


@tool
def read_gmail_message(message_id: str) -> str:
    """Read the full text body of a Gmail message by its ID."""
    service = get_gmail_service()
    message = service.users().messages().get(
        userId="me", id=message_id, format="full"
    ).execute()

    def extract_body(payload):
        if payload.get("body", {}).get("data"):
            return base64.urlsafe_b64decode(payload["body"]["data"]).decode("utf-8")
        for part in payload.get("parts", []):
            if part.get("mimeType") == "text/plain":
                if part.get("body", {}).get("data"):
                    return base64.urlsafe_b64decode(part["body"]["data"]).decode("utf-8")
        return "Could not extract body text"

    headers = {h["name"]: h["value"] for h in message["payload"]["headers"]}
    body = extract_body(message["payload"])
    return (
        f"From: {headers.get('From', 'Unknown')}\n"
        f"Subject: {headers.get('Subject', 'No Subject')}\n"
        f"Date: {headers.get('Date', 'Unknown')}\n\n"
        f"Body:\n{body[:2000]}"
    )


@tool
def send_email(to: str, subject: str, body: str, is_html: bool = False) -> str:
    """
    Send an email via Gmail.
    to: recipient email address
    is_html: set True if body contains HTML formatting
    """
    service = get_gmail_service()
    if is_html:
        msg = MIMEMultipart("alternative")
        msg["Subject"] = subject
        msg["To"] = to
        msg.attach(MIMEText(body, "html"))
    else:
        msg = MIMEText(body)
        msg["Subject"] = subject
        msg["To"] = to

    raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8")
    service.users().messages().send(userId="me", body={"raw": raw}).execute()
    return f"Email sent to {to} with subject '{subject}'"


@tool
def apply_gmail_label(message_id: str, label_name: str) -> str:
    """Apply a Gmail label to a message. Creates the label if it doesn't exist."""
    service = get_gmail_service()

    # Find or create the label
    labels_result = service.users().labels().list(userId="me").execute()
    label_id = None
    for label in labels_result.get("labels", []):
        if label["name"].lower() == label_name.lower():
            label_id = label["id"]
            break

    if not label_id:
        new_label = service.users().labels().create(
            userId="me",
            body={"name": label_name, "labelListVisibility": "labelShow", "messageListVisibility": "show"}
        ).execute()
        label_id = new_label["id"]

    service.users().messages().modify(
        userId="me", id=message_id,
        body={"addLabelIds": [label_id]}
    ).execute()
    return f"Label '{label_name}' applied to message {message_id}"

Google Calendar Tools#

from datetime import datetime, timedelta
import pytz

def get_calendar_service():
    credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE,
        scopes=["https://www.googleapis.com/auth/calendar"]
    )
    return build("calendar", "v3", credentials=credentials.with_subject(DELEGATE_EMAIL))


@tool
def list_calendar_events(days_ahead: int = 7, calendar_id: str = "primary") -> str:
    """
    List upcoming calendar events for the next N days.
    calendar_id: use 'primary' for the main calendar or a specific calendar ID.
    """
    service = get_calendar_service()
    now = datetime.utcnow().isoformat() + "Z"
    end_time = (datetime.utcnow() + timedelta(days=days_ahead)).isoformat() + "Z"

    result = service.events().list(
        calendarId=calendar_id,
        timeMin=now,
        timeMax=end_time,
        singleEvents=True,
        orderBy="startTime",
        maxResults=20
    ).execute()

    events = result.get("items", [])
    if not events:
        return f"No events found in the next {days_ahead} days"

    summaries = []
    for event in events:
        start = event["start"].get("dateTime", event["start"].get("date"))
        attendees = [a["email"] for a in event.get("attendees", [])]
        summaries.append(
            f"- {event['summary']} | Start: {start} | "
            f"Attendees: {', '.join(attendees[:3]) if attendees else 'None'}"
        )
    return f"Upcoming events:\n" + "\n".join(summaries)


@tool
def create_calendar_event(title: str, start_datetime: str, end_datetime: str,
                            attendees: str = None, description: str = None,
                            meet_link: bool = False) -> str:
    """
    Create a new Google Calendar event.
    start_datetime and end_datetime: ISO format, e.g. '2026-03-01T14:00:00-05:00'
    attendees: comma-separated email addresses
    meet_link: set True to auto-generate a Google Meet link
    """
    service = get_calendar_service()

    event_body = {
        "summary": title,
        "start": {"dateTime": start_datetime},
        "end": {"dateTime": end_datetime},
    }

    if attendees:
        event_body["attendees"] = [{"email": e.strip()} for e in attendees.split(",")]

    if description:
        event_body["description"] = description

    if meet_link:
        event_body["conferenceData"] = {
            "createRequest": {
                "requestId": f"agent-{int(datetime.now().timestamp())}",
                "conferenceSolutionKey": {"type": "hangoutsMeet"}
            }
        }

    created = service.events().insert(
        calendarId="primary",
        body=event_body,
        conferenceDataVersion=1 if meet_link else 0,
        sendUpdates="all"
    ).execute()

    return (
        f"Event created: '{title}'\n"
        f"  Start: {start_datetime}\n"
        f"  Event link: {created.get('htmlLink', 'N/A')}\n"
        f"  Meet link: {created.get('conferenceData', {}).get('entryPoints', [{}])[0].get('uri', 'None')}"
    )

Google Sheets Tools#

def get_sheets_service():
    credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE,
        scopes=["https://www.googleapis.com/auth/spreadsheets"]
    )
    return build("sheets", "v4", credentials=credentials.with_subject(DELEGATE_EMAIL))


@tool
def read_sheet_range(spreadsheet_id: str, range_notation: str) -> str:
    """
    Read a range from a Google Sheets spreadsheet.
    range_notation: e.g. 'Sheet1!A1:E20' or 'Data!A:F'
    Returns data as formatted rows.
    """
    service = get_sheets_service()
    result = service.spreadsheets().values().get(
        spreadsheetId=spreadsheet_id,
        range=range_notation
    ).execute()

    rows = result.get("values", [])
    if not rows:
        return f"No data found in range {range_notation}"

    # Format as table
    formatted = []
    for i, row in enumerate(rows[:50]):  # Limit to 50 rows
        formatted.append(" | ".join(str(cell) for cell in row))

    return f"Data from {range_notation} ({len(rows)} rows):\n" + "\n".join(formatted)


@tool
def write_sheet_range(spreadsheet_id: str, range_notation: str, values: str) -> str:
    """
    Write data to a Google Sheets range.
    values: JSON string of 2D array, e.g. '[["Name", "Score"], ["Alice", "95"], ["Bob", "87"]]'
    """
    service = get_sheets_service()
    values_list = json.loads(values)

    result = service.spreadsheets().values().update(
        spreadsheetId=spreadsheet_id,
        range=range_notation,
        valueInputOption="USER_ENTERED",
        body={"values": values_list}
    ).execute()

    updated_cells = result.get("updatedCells", 0)
    return f"Updated {updated_cells} cells in {range_notation}"


@tool
def append_sheet_rows(spreadsheet_id: str, sheet_name: str, values: str) -> str:
    """
    Append rows to the end of a Google Sheet.
    values: JSON string of 2D array of rows to append.
    """
    service = get_sheets_service()
    values_list = json.loads(values)

    result = service.spreadsheets().values().append(
        spreadsheetId=spreadsheet_id,
        range=f"{sheet_name}!A1",
        valueInputOption="USER_ENTERED",
        insertDataOption="INSERT_ROWS",
        body={"values": values_list}
    ).execute()

    return f"Appended {len(values_list)} row(s) to {sheet_name}"

Use Case 1: Email Triage and Response Agent#

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o", temperature=0)
email_tools = [search_gmail, read_gmail_message, send_email, apply_gmail_label]

email_triage_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are an email triage agent managing a professional inbox.

Your daily email triage workflow:
1. Search for unread emails in the last 24 hours
2. For each email, read the full content
3. Classify as: urgent_action_required, needs_response, informational, or spam
4. For emails needing a response:
   - Draft a professional reply appropriate to the context
   - Apply label 'Agent-Draft' and send the draft to the user for review
5. For informational emails: apply label 'Agent-Read' and mark as read
6. For urgent items: send a summary to the user's phone via SMS or flag as urgent

Always maintain professional tone. Never send emails on the user's behalf without explicit approval for important matters."""),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

email_agent = create_tool_calling_agent(llm, email_tools, email_triage_prompt)
email_executor = AgentExecutor(agent=email_agent, tools=email_tools, verbose=True, max_iterations=10)

result = email_executor.invoke({"input": "Run my morning email triage for today."})
print(result["output"])

This is a concrete implementation of the AI agent for customer service pattern applied to personal productivity.


Use Case 2: Meeting Scheduler Agent#

scheduling_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a meeting scheduling assistant with access to Google Calendar.

When asked to schedule a meeting:
1. Check the calendar for existing events in the proposed time window
2. Identify an open slot that works
3. Create the calendar event with:
   - Clear title
   - Description with meeting agenda (if provided)
   - All specified attendees
   - Google Meet link if it's a remote meeting
4. Confirm the created event details

When checking availability, look at the next 5 business days unless a specific date is given.
Always create events with at least 30 minutes duration unless specified."""),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

calendar_tools = [list_calendar_events, create_calendar_event]
schedule_agent = create_tool_calling_agent(llm, calendar_tools, scheduling_prompt)
schedule_executor = AgentExecutor(agent=schedule_agent, tools=calendar_tools, verbose=True)

result = schedule_executor.invoke({
    "input": "Schedule a 1-hour strategy review with alice@company.com and bob@company.com sometime next week. Include a Google Meet link and add agenda: Q2 planning review."
})
print(result["output"])

Use Case 3: Data Extraction Agent Using Sheets#

sheets_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are a data extraction and reporting agent for Google Sheets.

When asked to analyze a spreadsheet:
1. Read the relevant data range
2. Perform the requested analysis (calculate totals, find patterns, identify outliers)
3. Write the analysis results back to a summary sheet or append them as new rows
4. Provide a plain-language summary of your findings

Always handle missing or malformed data gracefully — report what you found rather than failing silently."""),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

sheets_tools = [read_sheet_range, write_sheet_range, append_sheet_rows]
sheets_agent = create_tool_calling_agent(llm, sheets_tools, sheets_prompt)
sheets_executor = AgentExecutor(agent=sheets_agent, tools=sheets_tools, verbose=True)

result = sheets_executor.invoke({
    "input": "Read the sales data from spreadsheet 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms range Sheet1!A1:F100, calculate total revenue by region, and write the summary to Summary!A1"
})
print(result["output"])

For more workflow patterns like these, see AI agent workflow examples.


Option: No-Code with Lindy AI#

Lindy AI provides native Google Workspace connectors that handle all authentication via OAuth2:

  1. In Lindy, go to IntegrationsProductivityGoogle Workspace
  2. Connect Gmail, Calendar, and Sheets separately — each requires its own OAuth authorization
  3. Define your agent's system prompt and which Google services it can access
  4. Set triggers (e.g., new email arrives, calendar event created) and actions (send email, create event, update Sheet)

Lindy is particularly effective for email-triggered workflows where you want a Gmail trigger to initiate a multi-step agent that reads a Sheet, drafts an email response, and schedules a follow-up calendar event — without writing Python.


Monitoring Google Cloud API Quotas#

Monitor your API usage to avoid unexpected quota exhaustion:

  1. In Google Cloud Console, go to APIs & ServicesDashboard
  2. Select the API (e.g., Gmail API)
  3. View Metrics tab for request rate, error rate, and latency

Default quotas to know:

| API | Default quota | Quota reset | |---|---|---| | Gmail API | 250 units/second/user | Continuous | | Calendar API | 50,000 requests/day | Midnight Pacific | | Sheets API | 300 requests/minute | Rolling | | Drive API | 1,000 requests/100 seconds | Rolling |

Request quota increases through APIs & Services → the API → QuotasEdit Quota for any API where your agent regularly approaches limits.

Implement backoff in all Google API calls:

from googleapiclient.errors import HttpError
import time

def google_api_with_retry(api_call, max_retries=5):
    """Execute a Google API call with exponential backoff on quota errors."""
    for attempt in range(max_retries):
        try:
            return api_call()
        except HttpError as e:
            if e.resp.status in [429, 500, 503]:
                wait_time = (2 ** attempt) + 0.5
                print(f"API error {e.resp.status}. Retrying in {wait_time:.1f}s...")
                time.sleep(wait_time)
            else:
                raise
    raise Exception(f"API call failed after {max_retries} attempts")

# Usage:
result = google_api_with_retry(
    lambda: service.users().messages().list(userId="me", q="is:unread").execute()
)

Best Practices for Workspace Agent Permissions#

Create a dedicated service account per agent role: A Gmail triage agent should have a different service account from your Sheets reporting agent. This limits blast radius and makes audit logging clearer.

Use the narrowest scopes possible: gmail.readonly instead of gmail.modify if the agent only reads. spreadsheets.readonly instead of spreadsheets if it only reads Sheets.

Rotate service account keys regularly: Generate new JSON keys quarterly and revoke old ones. Use Google Cloud Secret Manager to store and auto-rotate keys in production.

Never commit credentials to version control: Use environment variables or a secrets manager. Add *.json to your .gitignore if you're storing service account JSON files locally.

Audit agent activity: Use Google Cloud Audit Logs (Cloud Logging) to track all API calls made by your service account. Set alerts for unusual patterns (e.g., sudden spike in email sends).


Next Steps#

Your Google Workspace agent can now work across the most critical productivity tools in your organization: