◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-05-03Advanced

A Personal Secretary AI on Gemini API and Google Workspace — Auth, Orchestration, and Approval Gates

Designing a personal secretary AI across Gmail and Google Calendar with the Gemini API: OAuth2 scope design, Function Calling orchestration, approval gates for write tools, and ninety days of measured production cost, with full Python code.

gemini-api286google-workspace7function-calling20gmail5google-calendaragent10python108

✦ Premium Article

On the eighth morning, the only thing waiting in my terminal was a single line: invalid_grant. The daily summary that had arrived on time every day until then simply stopped. I hadn't touched a character of the code. What had changed was the one thing I'd never looked at — my OAuth consent screen was still set to "Testing".

The hard part of a secretary AI that spans Gmail and Calendar turned out not to be Gemini Function Calling. It was everything around it: how long the auth actually lives, how finely to slice scopes, and where to hand control back to a human before a write tool fires.

What follows is the implementation I've been running once every morning for about ninety days, along with the mistakes I made getting there. I've tried to leave in not just the working code but the reason each piece has to be written that way if you want it to survive production.

Why Secretary AI Projects Usually Fail

Before touching any code, here are three patterns that kill these projects.

Anti-pattern 1: Sequential pipeline design

Gmail → AI processes → writes to Calendar. This seems clean until you try to add Drive or Docs. The integrations become tangled, and the whole thing collapses under its own weight. The right approach from day one is to let Gemini Function Calling orchestrate tool selection — the AI decides what to call and when.

Anti-pattern 2: Mixing OAuth2 and service accounts

"OAuth2 for personal use, service accounts for teams" sounds reasonable until you need to debug a permissions error at 11pm. For personal secretary AI, pick OAuth2 and stay consistent. Service accounts are for domain-wide delegation and CI/CD pipelines.

Anti-pattern 3: Passing everything to the model without filtering

Sending your entire inbox to Gemini sounds like a good idea until you get a bill for several dollars from a single morning run. Filtering before the API call — not after — is the design constraint that keeps this usable at personal scale.

Google Workspace API Authentication Design

Two authentication approaches matter here:

OAuth2 (Authorization Code Flow): For accessing your own data — your Gmail, your Calendar, your Drive. Tokens expire hourly, so refresh token management is required. This is what you want for personal use.

Service Accounts: For domain-wide access in headless environments (CI/CD, server-to-server). Not needed for a personal secretary AI.

Here's a production-ready authentication module with automatic token refresh:

# workspace_auth.py
import logging
from pathlib import Path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
 
logger = logging.getLogger(__name__)
 
# Principle of least privilege: only request what you actually need
SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.compose",
    "https://www.googleapis.com/auth/calendar.readonly",
    "https://www.googleapis.com/auth/drive.readonly",
]
 
TOKEN_PATH = Path("~/.config/secretary-ai/token.json").expanduser()
CREDENTIALS_PATH = Path("~/.config/secretary-ai/credentials.json").expanduser()
 
def get_credentials() -> Credentials:
    """
    Get or refresh OAuth2 credentials.
    First run launches a browser auth flow.
    Subsequent runs load from token file and auto-refresh if expired.
    """
    creds = None
 
    if TOKEN_PATH.exists():
        creds = Credentials.from_authorized_user_file(str(TOKEN_PATH), SCOPES)
 
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            try:
                creds.refresh(Request())
                logger.info("Access token refreshed successfully")
            except Exception as e:
                logger.warning(f"Token refresh failed, re-auth required: {e}")
                creds = None
 
        if not creds:
            if not CREDENTIALS_PATH.exists():
                raise FileNotFoundError(
                    f"credentials.json not found at: {CREDENTIALS_PATH}\n"
                    "Download an OAuth2 client ID from Google Cloud Console"
                )
            flow = InstalledAppFlow.from_client_secrets_file(
                str(CREDENTIALS_PATH), SCOPES
            )
            creds = flow.run_local_server(port=0)
 
        TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)
        TOKEN_PATH.write_text(creds.to_json())
        logger.info("Token saved to disk")
 
    return creds
 
def build_service(service_name: str, version: str):
    """Build a Google API service client"""
    creds = get_credentials()
    return build(service_name, version, credentials=creds)

The scope separation between gmail.readonly and gmail.compose is intentional. If a bug causes an unintended write operation, readonly prevents the damage. Apply least-privilege thinking to API scopes the same way you would to IAM policies.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Splitting OAuth2 scopes by read and write, and gating only the write tools behind human approval
✦What changed in output and cost when the model behind gemini-flash-latest was swapped
✦How to decide whether to keep your own orchestrator now that Managed Agents are in preview
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.
◈ API / SDK2026-09-04
Screen Loop Seam Clicks With Numbers Before You Hand the Audio to Gemini
An ambient loop that clicks only at the wrap point. Here is the numeric prescreen I run before sending anything to audio understanding, why an absolute threshold fails, and how AAC encoding quietly rebuilt the seam I had just repaired.
◈ API / SDK2026-07-15
Sample what you already accepted — an audit budget that catches silent quality drift
A confidence gate only ever looks at output the model hesitated on. Silent drift sinks into the batch that sailed through. Working from a fixed 30-minutes-a-day review budget, this walks through deriving detection time from the binomial, reallocating the same budget across risk strata, and catching slow decay with a cumulative monitor.