As Google Apps Script developers, we are used to waiting. We wait for new runtime features, we wait for quotas to reset, and recently, we have been waiting for a first-class way to integrate Gemini into our projects.
With the recent release of the Vertex AI Advanced Service, the wait is technically over. But as detailed in Justin Poehnelt’s recent post, Using Gemini in Apps Script, you might find yourself asking if this was the solution we were actually looking for.
While the new service undoubtedly reduces the boilerplate code required to call Google’s AI models, it brings its own set of frustrations that leave me, and others in the community, feeling somewhat underwhelmed.
The “Wrapper” Trap
On the surface, the new VertexAI service looks like a win. As Justin highlights, replacing complex UrlFetchApp headers with a single VertexAI.Endpoints.generateContent() call is a significant cleanup.
However, this convenience comes with an administrative price tag. The Vertex AI Advanced Service requires a standard Google Cloud Project, understandable for billing, but requires the creation of an oAuth consent screen. For the majority of internal enterprise applications, I would imagine either a service account or a https://www.googleapis.com/auth/cloud-platform scope and associated IAM will be the preferred approach. This removes the need for a consent screen and, in the case of Service Accounts, rules out the Vertex AI Advanced Service.
It begs the question: Why didn’t Google take the approach of the Google Gen AI SDK?
In the Node.js and JavaScript world, the new Google Gen AI SDK offers a unified interface. You can start with a simple API key (using Google AI Studio) for prototyping, and switch to Vertex AI (via OAuth) for production, all without changing your core code logic. The Apps Script service, by contrast, locks us strictly into the “Enterprise” Vertex path. We seem to have traded boilerplate code for boilerplate configuration.
A Third Way: The Community Approach
If you are looking for that Unified SDK experience I mentioned earlier, where you can use the standard Google AI Studio code patterns within Apps Script, there is a third way.
I have published a library, GeminiApp, which wraps UrlFetchApp but mimics the official Google Gen AI SDK for Node.js. This allows you to write code that looks and feels like the modern JavaScript SDK, handling the complex UrlFetchApp configuration under the hood.
As you can see in the comparison above, the Advanced Service (left) abstracts away the request complexity, the UrlFetchApp method (middle) gives you the transparency and control you often need in production, and the GeminiApp library (right) offers a balance of both.
Disclaimer: As the creator of this library, I admit some bias, but it was built specifically to address the gap.
It is important to note a distinction in scope. Both the Google Gen AI SDK and GeminiApp are focused strictly on generative AI features. The Vertex AI Advanced Service, much like the platform it wraps, offers a broader range of methods beyond just content generation.
If your needs extend into those wider Vertex AI capabilities, but you still require the authentication flexibility of UrlFetchApp (such as using Service Accounts), I have a solution for that as well. My Google API Client Library Generator for Apps Script includes a build for the full Vertex AI (AI Platform) API. This gives you the comprehensive coverage of the Advanced Service with the architectural flexibility of an open-source library.
Here is how you can use the generated client library to authenticate with a Service Account, something impossible with the official Advanced Service:
/**
* Example using the generated Aiplatform library with a Service Account.
* Library: https://github.com/mhawksey/Google-API-Client-Library-Generator-for-Apps-Script/tree/main/build/Aiplatform
*/
function callGemini(prompt) {
const projectId = 'GOOGLE_CLOUD_PROJECT_ID';
const region = 'us-central1';
const modelName = 'gemini-2.5-flash';
const modelResourceName = `projects/${projectId}/locations/${region}/publishers/google/models/${modelName}`;
const serviceAccountToken = getServiceAccountToken_();
const vertexai = new Aiplatform({
token: serviceAccountToken
});
const payload = {
contents: [{
role: 'user',
parts: [{
text: prompt
}]
}],
generationConfig: {
temperature: 0.1,
maxOutputTokens: 2048
}
};
const result = vertexai.projects.locations.publishers.models.generateContent({
model: modelResourceName,
requestBody: payload
});
return result.data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
}
When “Advanced” Means “Behind”
There is another catch that Justin uncovered during his testing: the service struggles with the bleeding edge.
If you are trying to access the latest “Preview” models to prototype, such as the highly anticipated gemini-3-pro-preview, the advanced service may fail you. It appears the wrapper doesn’t yet support the auto-discovery needed for these newer endpoints.
In his companion post, UrlFetchApp: The Unofficial Documentation, Justin reminds us why UrlFetchApp is still the backbone of Apps Script development. When the “official” wrapper doesn’t support a specific header or a beta model, UrlFetchApp is the only way to bypass the limitations.
The Verdict
The Vertex AI service is a welcome addition for stable, enterprise-focused applications. But for developers, particularly those who want to test the latest Gemini 3 capabilities, it feels rigid compared to the flexibility seen in other Google developer ecosystems.
It serves as a good reminder that in Apps Script, convenience services are great, but understanding the underlying HTTP requests via UrlFetchApp extends what you can achieve.
If you’ve built a Google Chat bot, you’ve likely hit this wall: the API sends you a membership event with a User ID… but omits the email field entirely.
For Google Workspace developers, this limitation in the Chat API is a frequent stumbling block. As detailed in the User resource documentation, while you can use an email address as an alias in requests (e.g., referencing users/[email protected]), the API insists on returning only the canonical resource name (e.g., users/123456789). This ID corresponds to the user’s profile in the People API or Admin SDK, but the email itself is stripped from the response, forcing developers to perform a secondary lookup.
/**
* Fetches user details from the People API.
* @private
* @param {string} userResourceName The "users/{user}" string from the Chat API.
* @return {{name: string, displayName: string, email: string|null}} An object with user details.
*/
function getUserDetails_(userResourceName) {
const defaultUserDetails = {
name: userResourceName,
displayName: userResourceName.replace('users/', 'Unknown User '),
email: null,
};
// Fail fast for app users or invalid formats
if (!userResourceName.startsWith('users/') || userResourceName === 'users/app') {
return defaultUserDetails;
}
try {
const peopleApiResourceName = userResourceName.replace(/^users\//, 'people/');
const person = People.People.get(peopleApiResourceName, {
personFields: 'names,emailAddresses',
});
const displayName = person.names?.[0]?.displayName ?? defaultUserDetails.displayName;
const email = person.emailAddresses?.[0]?.value ?? null;
return {
name: userResourceName,
displayName: displayName,
email: email,
};
} catch (e) {
console.warn(`Could not fetch details for ${userResourceName} from People API: ${e.message}`);
return defaultUserDetails;
}
}
However, bridging this gap for a Service Account leads to a security dilemma. The People API often returns empty fields because the Service Account lacks a contact list. You might find yourself reaching for Domain-Wide Delegation to impersonate an admin—effectively using a sledgehammer to crack a nut.
In a recent guide, Justin Poehnelt outlines a more secure strategy that avoids granting blanket domain access. By assigning a custom “Users > Read” Admin Role directly to a Service Account, developers can resolve emails securely without the risks associated with full impersonation.
The Strategy at a Glance
Custom Roles:Â Create a strictly read-only role in the Admin Console.
Direct Assignment:Â Assign this role specifically to the Service Account’s email.
No Long-Lived Keys:Â Use Application Default Credentials (ADC) in production and Service Account Impersonation for local development.
This approach ensures your bot has just enough permission to identify users, keeping your security team happy and your audit logs clean. For more information into the implementation, including the specific configuration steps, I encourage you to read Justin’s full post linked below.
We have all been there: You build a beautiful, automated dashboard in Google Sheets. It’s dynamic, it’s collaborative, and it’s perfectly integrated with your data sources. Then, the stakeholder request comes in: “Can you email this to me as an Excel file every Monday morning?”
For Google Workspace developers, the “Excel Requirement” is often a stumbling block. While Google Sheets is powerful, the bridge to Excel can be shaky. In this post, I’m going to explore four architectural approaches to solving this problem. If you would like to play along here is the Google Sheet Invoice Template and the Excel Invoice Template.
1. The Low-Code Agent (AppSheet Automation)
Before you write a single line of code, you should consider if the platform can do the heavy lifting for you. Since AppSheet is now a core part of Google Workspace, it offers a native “No-Code” way to generate Excel files that is robust, scheduled, and template-based.
The Strategy
Instead of writing a script to fetch data and build a file, you connect your Google Sheet to an AppSheet app. You then configure an Automation that triggers on a schedule or a data change.
The Workflow:
Trigger:Â A new row is added to your “Invoices” sheet (or a weekly schedule fires).
Task:Â AppSheet runs a “Create a new file” task.
Template: It reads a simplified Excel template stored in Drive, replaces tags like <<[Qty]>> with actual data, and generates the file.
Delivery:Â The file is saved to Drive or emailed directly to the client.
Why This Wins
Zero Maintenance:Â No scripts to debug, no OAuth tokens to manage, and no breaking changes when APIs update.
Native Templates:Â It supports complex Excel formatting out of the box.
The Limitation
Syntax:Â You must use AppSheet’s specific templating syntax (<< >>) inside your Excel file.
Scaling:Â Generating reports with thousands of line items can hit automation timeout limits.
2. The Native Export (Apps Script)
If you need more granular control or don’t want to spin up an AppSheet app, the most common starting point is attempting to do everything within Apps Script. You create a temporary Google Sheet, fill it with data, and then export it using the Drive API.
The Code
In this scenario, we assume you have a Google Sheet version of your Invoice template. We first write the specific line items to the Sheet, then export it.
function generateInvoiceExport() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('InvoiceTemplate');
// 1. Prepare Invoice Data
const invoiceData = [
{ desc: "Web Development - Phase 1", qty: 40, unit: 100 },
{ desc: "Server Hosting (Annual)", qty: 1, unit: 250 },
{ desc: "Domain Registration", qty: 2, unit: 15 },
{ desc: "Maintenance Retainer", qty: 10, unit: 85 }
];
// 2. Map data to the Sheet structure (Cols B, C, D, E, F)
// B=Desc, E=Qty, F=Unit. (C and D are skipped/empty)
const rows = invoiceData.map(item => [item.desc, "", "", item.qty, item.unit]);
// 3. Write data to the Google Sheet
// Matches Template: Start at Row 19, Column B (2)
if (rows.length > 0) {
// Corrected: ensure the number of columns (5) matches your data structure
sheet.getRange(19, 2, rows.length, 5).setValues(rows);
}
// 4. Construct the export URL for the populated sheet
const fileName = `Invoice_${new Date().toISOString().slice(0,10)}.xlsx`;
const url = `https://docs.google.com/spreadsheets/d/${ss.getId()}/export?format=xlsx&gid=${sheet.getSheetId()}`;
// 5. Fetch and Save
const params = {
method: "GET",
headers: { "Authorization": `Bearer ${ScriptApp.getOAuthToken()}` },
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, params);
if (response.getResponseCode() === 200) {
const blob = response.getBlob().setName(fileName);
const file = DriveApp.createFile(blob);
console.log(`Success: Invoice saved to Drive (ID: ${file.getId()})`);
} else {
console.log(`Error: ${response.getContentText()}`);
}
};
The Limitations
Speed & Scale:Â The overhead of creating a temporary sheet and exporting it adds up. Generating 50 invoices in a loop will take a long time.
The “Source” Gap: Your source material must be a Google Sheet, meaning you lose Excel-specific features like Power Query or VBA macros from your template.
3. The Browser Round-Trip (ExcelJS)
If you need to maintain stricter fidelity to an existing Excel template (preserving fonts, colors, and layouts) and are interested in speed, you can use the “Round Trip” technique.
This method uses the user’s browser as a processing engine. You fetch the Invoice.xlsx template from Drive, process it client-side with ExcelJS, and send the result back.
Parallel Fetching (Client):Â The browser fires concurrent requests to Apps Script to fetch the template (Base64) and invoice data.
Client-Side Processing:Â ExcelJS loads the template, injects data into specific cells (preserving layout), and recompiles the file.
Upload:Â The new file is encoded and sent back to Drive.
The Code
This snippet focuses on the core orchestration logic. For the full UI setup and helper functions, see the linked project.
/* --- CLIENT SIDE (Core Logic) --- */
async function startGeneration() {
// 1. Parallel Fetching
// Fire both requests at once to speed up the wait time
const [templateResponse, invoiceData] = await Promise.all([
runGoogleScript('getTemplateBase64', null),
runGoogleScript('getInvoiceData')
]);
// 2. Load into ExcelJS
const workbook = new ExcelJS.Workbook();
const binaryString = atob(templateResponse.base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } await workbook.xlsx.load(bytes.buffer); // 3. Modify the Sheet const sheet = workbook.worksheets[0]; let currentRow = 19; // Starting at Row 19 based on template invoiceData.forEach((item, index) => {
// Ensure we do not write past the visual template limit (e.g. Row 22)
if (currentRow + index > 22) return;
const row = sheet.getRow(currentRow + index);
// Inject data into specific cells (B, E, F)
row.getCell(2).value = item.desc;
row.getCell(5).value = item.qty;
row.getCell(6).value = item.unit;
// ExcelJS preserves the existing styles of these cells!
row.commit();
});
// 4. Export & Save
const outBuffer = await workbook.xlsx.writeBuffer();
// Convert buffer to base64 (simplified for brevity)
let binary = '';
const outBytes = new Uint8Array(outBuffer);
for (let i = 0; i < outBytes.length; i++) {
binary += String.fromCharCode(outBytes[i]);
}
const outBase64 = btoa(binary);
// Upload back to Drive
await runGoogleScript('saveProcessedFile', outBase64, `Invoice_${Date.now()}.xlsx`);
}
/* --- SERVER SIDE (Apps Script) --- */
function getTemplateBase64(templateId) {
const file = DriveApp.getFileById(templateId);
return {
base64: Utilities.base64Encode(file.getBlob().getBytes())
};
}
function saveProcessedFile(base64Content, fileName) {
const blob = Utilities.newBlob(
Utilities.base64Decode(base64Content),
MimeType.MICROSOFT_EXCEL,
fileName
);
return { url: DriveApp.createFile(blob).getUrl() };
}
The Trade-off
Payload Limits: google.script.run calls can become unstable when the payload exceeds 50MB.
Base64 Inflation:Â Remember that Base64 encoding increases file size by ~33%.
4. The Enterprise Engine (Cloud Functions + Python)
For robust, high-fidelity reporting, we need to leave the Google Sheets conversion pipeline behind. By using Google Cloud Functions with Python, we can manipulate native Excel files directly using libraries like openpyxl.
The “User Identity” Pattern
Instead of managing complex Service Account permissions, this solution uses the User’s Identity to perform Drive operations.
Apps Script: Generates an OIDC ID Token (for identity), using getIdentityToken()) and an OAuth Access Token (for Drive permissions) with getOAuthToken().
Cloud Function: Verifies the ID Token and uses the Access Token to initialise the Drive API client as the user.
Open cloud-fn.py and update APPS_SCRIPT_CLIENT_ID with your Apps Script’s OAuth Client ID. Note: You can find this ID by running the logClientId() helper function in Code.js.
Configuration: Update YOUR_CLOUD_FUNCTION_URL in callInvoiceEngine() with your deployed function URL.
The Code
Google Apps Script (Code.js)
function callInvoiceEngine() {
const url = "YOUR_CLOUD_FUNCTION_URL";
// 1. Get Tokens
const accessToken = ScriptApp.getOAuthToken();
const idToken = ScriptApp.getIdentityToken(); // Requires 'openid' scope in manifest
// --- LOGGING FOR LOCAL TESTING ---
console.log("--- CREDENTIALS FOR LOCAL TESTING ---");
console.log("ID Token:", idToken);
console.log("Access Token:", accessToken);
console.log("-------------------------------------");
const payload = {
templateId: "ID_OF_TEMPLATE_FILE",
folderId: "ID_OF_DESTINATION_FOLDER",
// Matches the same dataset used in our other examples
data: [
{ desc: "Web Development - Phase 1", qty: 40, unit: 100 },
{ desc: "Server Hosting (Annual)", qty: 1, unit: 250},
{ desc: "Domain Registration", qty: 2, unit: 15},
{ desc: "Maintenance Retainer", qty: 10, unit: 85}
],
// The Magic Key: Passing the active user's Access Token for Drive operations
userToken: accessToken
};
const options = {
method: "post",
contentType: "application/json",
headers: {
"Authorization": `Bearer ${idToken}`
},
payload: JSON.stringify(payload)
};
UrlFetchApp.fetch(url, options);
}
function logClientId() {
// Helper to find the Project's OAuth Client ID
var token = ScriptApp.getOAuthToken();
var url = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + token;
var response = UrlFetchApp.fetch(url);
var data = JSON.parse(response.getContentText());
console.log('Client ID:', data.issued_to);
}
Pro Tip: Batching Requests
If you need to generate 50 invoices at once, don’t loop through them one by one. Use UrlFetchApp.fetchAll(requests) to fire off parallel requests to your Cloud Function. Since the heavy lifting happens in the cloud, Apps Script just acts as the orchestrator.
import requests
import json
def run_test():
print("--- Local Cloud Function Tester ---")
print("Ensure you are running: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080\n")
# 1. Gather Tokens & IDs
print("Paste your Apps Script Tokens (use Logger.log in GAS):")
id_token = input("1. ID Token (ScriptApp.getIdentityToken()): ").strip()
if not id_token: return
access_token = input("2. Access Token (ScriptApp.getOAuthToken()): ").strip()
if not access_token: return
template_id = input("3. Google Sheet/Excel Template ID: ").strip()
folder_id = input("4. Destination Folder ID: ").strip()
# 2. Prepare Payload
payload = {
"templateId": template_id,
"folderId": folder_id,
"data": [
{ "desc": "Local Test Item 1", "qty": 1, "unit": 10 },
{ "desc": "Local Test Item 2", "qty": 5, "unit": 20 }
],
"userToken": access_token
}
headers = {
"Authorization": f"Bearer {id_token}",
"Content-Type": "application/json"
}
# 3. Send Request
url = "http://localhost:8080"
print(f"\nSending POST request to {url}...")
try:
response = requests.post(url, json=payload, headers=headers)
print(f"\nStatus Code: {response.status_code}")
print(f"Response Body: {response.text}")
except requests.exceptions.ConnectionError:
print(f"\n[!] Could not connect to {url}.")
print("Is the functions-framework running?")
if __name__ == "__main__":
run_test()
Local Test Script (test_request.py)
Before deploying, you can test the entire flow locally to ensure your tokens and permissions are working correctly.
Start the Function:Â functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080
Run the Tester: python3 test_request.py (Paste tokens from Apps Script logs when prompted).
import requests
import json
def run_test():
print("--- Local Cloud Function Tester ---")
print("Ensure you are running: functions-framework --target=generate_excel_report --source=cloud-fn.py --port=8080\n")
# 1. Gather Tokens & IDs
print("Paste your Apps Script Tokens (use Logger.log in GAS):")
id_token = input("1. ID Token (ScriptApp.getIdentityToken()): ").strip()
if not id_token: return
access_token = input("2. Access Token (ScriptApp.getOAuthToken()): ").strip()
if not access_token: return
template_id = input("3. Google Sheet/Excel Template ID: ").strip()
folder_id = input("4. Destination Folder ID: ").strip()
# 2. Prepare Payload
payload = {
"templateId": template_id,
"folderId": folder_id,
"data": [
{ "desc": "Local Test Item 1", "qty": 1, "unit": 10 },
{ "desc": "Local Test Item 2", "qty": 5, "unit": 20 }
],
"userToken": access_token
}
headers = {
"Authorization": f"Bearer {id_token}",
"Content-Type": "application/json"
}
# 3. Send Request
url = "http://localhost:8080"
print(f"\nSending POST request to {url}...")
try:
response = requests.post(url, json=payload, headers=headers)
print(f"\nStatus Code: {response.status_code}")
print(f"Response Body: {response.text}")
except requests.exceptions.ConnectionError:
print(f"\n[!] Could not connect to {url}.")
print("Is the functions-framework running?")
if __name__ == "__main__":
run_test()
Why This Wins
True Excel Support:Â You are editing the actual binary file, not a conversion.
Separation of Concerns:Â The heavy lifting is done in the cloud, not in the browser or the Apps Script runtime.
Summary: Which Tool for Which Job?
Feature
AppSheet
Apps Script (Native)
Client-Side (ExcelJS)
Cloud Functions (Python)
Complexity
Very Low
Low
High
High
Cost
Free
Free
Free
GCP Usage (Low)
Best For
Standard Workflows
Simple Reports
Interactive Template Fills
Enterprise Reports
Final Thoughts
There is no “one size fits all” here. The “Excel Requirement” is a spectrum:
For quick internal tools: Stick to Method 1 (AppSheet) and Method 2 (Native). It’s fast to build and free.
For polished client reports:Â Method 3 (ExcelJS)Â is your best friend. It keeps the heavy lifting in the browser and preserves your branding.
For enterprise scale: If you are generating hundreds of invoices a week, Method 4 (Cloud Functions) is the robust choice. It decouples the generation logic from the spreadsheet UI.
Choose the architecture that fits your volume and your “pixel-perfect” tolerance. I’m also keen to hear have got another ‘Excel Generator’ approach? Happy scripting!
“Clicking through the Google [Workspace] Admin Console to verify hundreds of settings is manual labour. How to create an agent with Gemini 3 (Flash) that will check the settings continuously?”
In a recent post, Ivan Kutil explores how to move beyond manual checks for the Google Workspace Admin console by building a specialised “Auditing Agent” using the Agent Development Kit (ADK) and the Policy API.
“Clicking through the Google [Workspace] Admin Console to verify hundreds of settings is manual labour. How to create an agent with Gemini 3 (Flash) that will check the settings continuously?”
The Architecture: Policy API meets ADK
At the heart of Ivan’s solution is a very repeatable pattern, bridging the gap between Google Workspace APIs, in this case the Policy API, and a natural language interface. By using the ADK, developers can create tools that allow an LLM to “handshake” with Workspace data without writing a custom function for every single API endpoint:
ADK simplifies interacting with external REST APIs by automatically generating callable tools directly from an OpenAPI Specification (v3.x). This eliminates the need to manually define individual function tools for each API endpoint [ref].
In Ivan’s example, he uses AI Studio to generate the OpenAPI 3 YAML file required by the OpenAPIToolset in ADK. Google currently defines many of its APIs through the Google Discovery service in JSON syntax, and Ivan highlights the need for transformation. Gemini is very good at this, but there are a number of community tools and repositories to transform the Discovery Document into an OpenAPI file, such as the StackQL Provider Google project and its associated YAML repository (the Policy API is found in the Cloud Identity service).
Note: While the Policy API provides a powerful programmatic window into your Workspace domain, it is worth noting that it does not yet provide 100% coverage of all settings available in the Admin Console. Developers should verify the specific policy types supported when designing their auditing logic.
Agentic Auditing in Action
The resulting agent allows an administrator to simply ask questions about their Workspace environment, the agent then fetches live data, help identify risks (such as missing MFA or risky API access), and provides a structured summary with actionable steps.
This project serves as a powerful blueprint for developers looking to build “agentic” workflows enhances capabilities by grounding them with data from Workspace APIs with intelligent, conversational automation.
The “USB-C for AI” has officially arrived at Google.
If you have been following the rapid rise of Anthropic’s Model Context Protocol (MCP), you know it promises a standardised way for AI models to connect to data and tools. Until now, connecting an LLM to your Google Drive or BigQuery data meant building custom connectors or relying on fragmented community solutions.
Here is an overview of the different approaches emerging for MCP tools and what the future looks like for Workspace developers.
The Official Path: Managed MCP Servers
Google’s announcement represents a massive shift in how they view AI agents. Rather than asking developers to wrap every granular API endpoint (like compute.instances.insert) into a tool, Google is releasing Managed MCP Servers.
Think of these as “Client Libraries for Agents.” Just as Google provides SDKs for Python or Node.js to handle the heavy lifting of authentication and typing, these managed servers provide a simplified, reasoned interface for agents.
The announcement lists a massive rollout over the “next few months” covering everything from Cloud Run and Storage to the Android Management API. The goal described is to make MCP the standard way agents connect to the entire Google ecosystem. To start with Google announced MCPs for:
Google Maps:Â instead of just raw data, the agent gets “Grounding Lite” to accurately answer location queries without hallucinating.
BigQuery: The agent can interpret schemas and execute queries in place, meaning massive datasets don’t need to be moved into the context window.
Google Kubernetes Engine (GKE):Â Agents can diagnose issues and optimize costs without needing to string together brittle CLI commands.
The Community Path: Privacy-First and Scriptable
While managed servers are excellent for scale and standardisation, there are some other notable approaches using local execution.
1. The Gemini CLI Workspace Extension As detailed in a recent tutorial by Romin Irani, the official google-workspace extension for the Gemini CLI takes a privacy-first approach. Instead of running in the cloud, the MCP server runs locally on your machine.
It uses your own local OAuth token, meaning your data flow is strictly Google Cloud <-> User's Local Machine <-> Gemini CLI. This is ideal for tasks like searching Drive, checking your Calendar, or drafting Gmail replies where you want to ensure no third-party intermediary is processing your data.
2. The Power of Apps Script (gas-fakes) For true customisability, Kanshi Tanaike has been pushing Google Apps Script. A recent proof-of-concept using gas-fakes CLI demonstrates how to build MCP tools directly using Apps Script.
This is a big step for Apps Script developers. It allows you to write standard GAS code (e.g., DriveApp.getFilesByName()) and expose it as a tool to an MCP client like Gemini CLI or Google Antigravity. Because it uses the gas-fakes sandbox, you can execute these scripts locally, bridging the gap between your local AI agent and your cloud-based Workspace automation.
The Future: A Universal MCP Strategy?
We are likely moving toward a world where every Google product launch includes three things: API documentation, Client SDKs, and an Official MCP Server Endpoint.
For developers, the choice will come down to granularity and control:
Use Managed Servers when you need reliable, standard access to robust services like BigQuery or Maps with zero infrastructure overhead.
Use Community/Local Tools (like the Workspace extension or gas-fakes) when you need rapid prototyping, deep customization, or strict data privacy on your local machine.
The “Agentic” future is here, and whether you are using Google’s managed infrastructure or writing your own tools, the ecosystem is ready for you to build.
This tutorial shows you how to publish AI agents to Google Workspace as Google Workspace add-ons, using Apps Script or HTTP endpoints. After your publish your add-on, your users can interact with the AI agents within their workflows.
Google has released a comprehensive new tutorial that demonstrates how to bridge the gap between complex AI agents and the daily workflows of Google Workspace users. The Travel Concierge sample uses the Agent Development Kit (ADK) to deploy a conversational, multi-agent AI directly into the Google Workspace platform.
While many AI demos focus on standalone chat interfaces, this solution stands out by implementing the agent as a Google Workspace Add-on. This means the AI isn’t just a browser tab; it is a helpful sidebar accessible directly within Gmail, Calendar, Drive, Docs, Sheets, and Slides, as well as a fully functioning Google Chat app.
Key Features for Developers
The sample provides a robust blueprint for developers looking to build “agentic” workflows. Key capabilities include:
Multi-Agent Architecture:Â The solution uses Vertex AI to manage a “Travel Concierge” that orchestrates sub-agents for specific tasks, utilising tools like the Google Maps Platform Places API and Google Search Grounding.
Context Awareness:Â In applications like Gmail, the agent can read the context of the currently selected email to help plan trips based on actual correspondence.
Rich User Interfaces:Â The add-on moves beyond simple text, utilising the Card framework to display rich widgets and interactive elements.
Cross-Platform Persistence:Â User sessions are managed by Vertex AI, ensuring that a conversation started in Google Chat can be seamlessly continued in a Google Doc sidebar.
Flexible Implementation
The Apps Script implementation demonstrates how to connect a standard Workspace Add-on to the powerful Vertex AI backend using UrlFetchApp. It serves as an excellent reference for handling synchronous execution limits and rendering complex agent responses within the constraints of the Add-on sidebar.
Whether you are looking to build a travel planner or a complex enterprise assistant, this sample provides the architectural patterns needed to bring your AI agents to where your users actually work.
How to build a server-less crawler using Google Apps Script to create a unified knowledge base for AI-assisted development.
The Goal: A Unified Context for Gemini
When building Google Workspace add-ons, particularly those using newer features like Workspace Flows, the documentation is often spread across dozens of nested pages. While excellent for browsing, this structure presents a challenge when working with Large Language Models (LLMs) like Gemini.
The primary motivation for this project was to create a single, consolidated “Knowledge Doc” containing the complete technical specification for Workspace Flows. By feeding this unified document into Gemini, I can:
Control the Context:Â Ensure the model answers based on the specific, official documentation rather than outdated training data or hallucinated APIs.
Accelerate Development:Â Ask complex architectural questions (“How do I build a flow that integrates X and Y?”) and get answers grounded in the full API reference without the latency of multiple web searches.
Bridge the Knowledge Gap:Â Work effectively with brand-new products and services that haven’t yet been absorbed into the LLM’s core training set.
This article details how I built the tool to create this document—a server-less crawler that runs entirely within Google Apps Script. If you are less concerned about the how and more about the end result here is the [Shared] Google Workspace Flows Guide
The Technical Challenge: Apps Script vs. The DOM
My initial plan was straightforward:
Fetch the HTML of each documentation page.
Parse it into Markdown (using a library like Turndown or Showdown).
Convert that Markdown into a Google Doc.
However, I hit a major roadblock immediately. Most JavaScript parsing libraries rely on the DOM (window, document, DOMParser). Google Apps Script runs in a server-side environment (similar to a Node.js runtime but without standard packages) where no DOM exists. Libraries like Turndown crashed instantly with ReferenceError: document is not defined.
The Solution: A Hybrid HTML Pipeline
Instead of fighting to make a browser library work on the server, I pivoted to a hybrid approach:
Cheerio for Parsing: I used the Cheerio library (which parses HTML using pure JavaScript strings, no DOM required) to “clean” the content.
Direct HTML Injection: Instead of converting to Markdown (which lost formatting for tables and images), I switched to the Google Drive API which can natively convert HTML files into Google Docs.
This pipeline proved superior because it preserved:
Code Blocks:Â <pre>Â tags with specific formatting.
Tables:Â Complex data tables rendered perfectly.
Images:Â <img>Â tags with absolute URLs were automatically fetched and embedded by Google Drive.
Important Note: This script was developed and tested specifically for Google Workspace Flows documentation. Web scraping inherently carries the risk that the target website’s HTML structure may change over time. Other documentation sites will use different CSS classes and HTML structures. While the core logic (crawling + HTML cleaning + Docs conversion) remains valid, you will need to inspect the source of your target site and update the Cheerio selectors (like .devsite-article-body or .devsite-book-nav) to match its specific layout.
Step 1: The Crawler
First, we needed to find all the relevant pages. Traversing the DOM tree proved fragile—class names change, and nesting levels vary.
The robust solution was Path Filtering. We fetch the main navigation bar and simply filter for any link that starts with our target root URL.
functionextractLinksByPath(html, rootUrl) {
const $ = Cheerio.load(html);
const links = newSet();
$('.devsite-book-nav a').each(function() {
let href = $(this).attr('href');
if (href) {
if (href.startsWith('/')) href = CONFIG.BASE_URL + href;
if (href.startsWith(rootUrl) && !href.includes('#')) links.add(href);
}
});
returnArray.from(links);
}
Step 2: The HTML Cleaner
We couldn’t just dump the raw page HTML into a Doc; it would include navigation bars, “Feedback” buttons, and footer links. We used Cheerio to strip these out.
We also encountered a specific challenge with Definition Lists (<dl>, <dt>, <dd>). Google Docs doesn’t have a native “Definition List” element, so they often flattened into messy text.
The fix was to transform them into semantic HTML paragraphs with inline styling to force the visual layout we wanted:
Even with clean HTML, the Google Docs converter can be stubborn. It sometimes ignores semantic headings (<h1>) or allows images to overflow the page width.
To fix this, we added a post-processing step. After the HTML file is converted to a Google Doc, we open it with DocumentApp to programmatically enforce styles and constraints.
Fixing Headings:Â We scan for our specific “Title” styling (24pt Bold) and force the Heading 1 style.
functionapplyHeadingsToDoc(docId) {
const doc = DocumentApp.openById(docId);
const body = doc.getBody();
const paragraphs = body.getParagraphs();
paragraphs.forEach(p => {
const text = p.getText();
if (text.length === 0) return;
const fontSize = p.getAttributes().FONT_SIZE;
const isBold = p.getAttributes().BOLD;
// If it looks like a H1 (24pt + Bold), make it a H1!if (fontSize >= 24 && isBold) {
p.setHeading(DocumentApp.ParagraphHeading.HEADING1);
}
});
doc.saveAndClose();
}
Resizing Images:Â We also scan for images wider than the standard page width (e.g., 600px) and scale them down to fit, ensuring the document layout remains printable and clean.
Once generated, this document becomes the cornerstone of my development workflow:
Consolidate:Â I run the script to pull the latest documentation into a fresh Google Doc.
Enrich: I use tools like Gemini Deep Research to find community patterns, or architectural best practices and paste them directly into the same doc.
Contextualise:Â This single document is then uploaded to Gemini (or another LLM workspace), providing a focused, hallucination-resistant context for answering questions and generating code.
By combining the UrlFetchApp for crawling, Cheerio for parsing, and the Drive API for document generation, we built a custom scraping pipeline in under 200 lines of code without spinning up a single server. If you’d like to make your own feel free to copy this script project which comes with a compiled Apps Script friendly version of Cheerio
Google Workspace Flows is currently available as part of the Gemini Alpha program for Google Workspace customers. If you don’t have access to Workspace Flows you will need to request access via your Google Workspace Administrator.
As developers, we often find ourselves building complex automations to glue different services together. But with the introduction of Google Workspace Flows, Google is handing some of that power directly to business users, allowing them to string together tasks—like summarising emails or posting to Chat—without writing a single line of code.
But where does that leave us? Right in the driver’s seat.
While Flows provides a “no-code” interface for users, it offers a robust “code” backend for us. Developers can extend the platform by building custom steps, in Google Apps Script. These steps act as reusable building blocks that users can drop into their flows to perform specific, business-critical tasks that aren’t available out of the box.
If you have an existing Google Workspace Add-on, you can even extend its functionality to include these Flows steps. Since Google Workspace Flows is currently part of the Gemini Alpha program, custom steps are intended for internal use and testing within your own Google Workspace organization rather than public distribution. You can deploy them as test deployments or private add-ons for your domain, but a public listing is not yet supported.
Here is a high-level overview of how it works and where to find the documentation to get started.
The Anatomy of a Step
Building a custom step in Apps Script isn’t too dissimilar from building an Editor Add-on, but there are specific architectural differences to be aware of. A step essentially consists of three parts:
The Definition (Manifest): You define the step’s identity, required inputs, and expected outputs in the appsscript.json file.
The Configuration (UI):Â You build a card interface (using CardService) that allows the user to configure the step when they add it to their flow.
The Execution (Logic):Â You write the actual function that processes the inputs and returns the outputs.
Key Capabilities
The documentation provided by Google is comprehensive. Here are the key areas you should explore to understand what is possible:
1. Configuration Cards & Variables
Unlike a static settings page, configuration cards in Flows are dynamic. You can build cards that accept input variables from previous steps. For example, if a user has a “New Email” trigger, your step can ingest the email body as a variable.
2. Passing Data
Your step doesn’t just run in isolation; it talks to the rest of the flow. By defining output variables, your script can return data (like a calculated value or a file ID) that subsequent steps can use.
3. Robust Validation
To ensure users don’t break your script with bad data, Flows supports two types of validation. You can use Client-side validation (using Common Expression Language, or CEL) for instant feedback on the UI, or Server-side validation for complex checks against external databases.
4. Handling Complexity
For more advanced use cases, simple strings and integers might not be enough. The platform supports Custom Resources for grouping complex data structures (like a CRM lead object) and Dynamic Variables for inputs that change based on context (like selecting a specific question from a Google Form).
Getting Started
If you are part of the Gemini Alpha program and want to get your hands dirty, the best place to start is the Quickstart guide. It walks you through building a simple calculator step that takes two numbers and an operator to output a result.
With the announcement of Google Workspace Flows at Google Cloud Next 2025, Workspace developers and power users have been eager to see how this new platform integrates with the ecosystem we know and love. Currently available in the Gemini for Workspace Alpha programme, Workspace Flows promises to bridge the gap between simple tasks and complex, AI-driven processes.
For developers seasoned in the Google Workspace ecosystem, the promise of agentic AI is not just about automating tasks, but about creating intelligent, nuanced interactions with the services we know so well. Google’s Agent Development Kit (ADK) provides the foundation, but integrating the familiar, user-centric OAuth 2.0 flow into this new world requires a deliberate architectural approach.
This article explores the patterns for building a sophisticated, multi-capability agent. It explores why you might choose a custom tool implementation (ADK’s “Journey 2”) over auto-generation, and presents a reusable authentication pattern that you can apply to any Workspace API.
The full source code for this project is available in the companion GitHub repository. It’s designed as a high-fidelity local workbench, perfect for development, debugging, and rapid iteration.
The Architectural Crossroads: Journey 1 vs. Journey 2
Journey 1: The “Auto-Toolify” Approach. This involves using components like OpenAPIToolset or the pre-built GoogleApiToolSet sets. You provide an API specification, and the ADK instantly generates tools for the entire API surface. This is incredibly fast for prototyping but can lead to an agent that is “noisy” (has too many tools and scopes) and lacks the robustness to handle API-specific complexities like pagination.
Journey 2: The “Crafted Tool” Approach. This involves using FunctionTool to wrap your own Python functions. This is the path taken in this project. It requires more initial effort but yields a far superior agent for several reasons:
Control: This approach exposes only the high-level capabilities needed (e.g., search_all_chat_spaces), not every raw API endpoint.
Robustness:Â Logic can be built directly into the tools to handle real-world challenges like pagination, abstracting this complexity away from the LLM.
Efficiency:Â The tool can pre-process data from the API, returning a concise summary to the LLM and preserving its valuable context window.
Journey 2 was chosen because the goal is not just to call an API, but to provide the agent with a reliable, intelligent capability.
The Core Implementation: A Reusable Pattern
The cornerstone of this integration is a single, centralised function: get_credentials. This function lives in agent.py and is called by every tool that needs to access a protected resource.
It elegantly manages the entire lifecycle of an OAuth token within the ADK session by following a clear, four-step process:
Check Cache:Â It first looks in the session state (tool_context.state) for a valid, cached token.
Refresh:Â If a token exists but is expired, it uses the refresh token to get a new one and updates the cache.
Check for Auth Response: If no token is found, it checks if the user has just completed the OAuth consent flow using tool_context.get_auth_response().
Request Credentials: If all else fails, it formally requests credentials via tool_context.request_credential(), which signals the ADK runtime to pause and trigger the interactive user flow.
This pattern is completely generic. You can see in the agent.py file how, by simply changing the SCOPES constant, you could reuse this exact function for Google Drive, Calendar, Sheets, or any other Workspace API.
Defining the Agent and Tools
With the authentication pattern defined, building the agent becomes a matter of composition. The agent.py file also defines the “Orchestrator/Worker” structure—a best practice that uses a fast, cheap model (gemini-2.5-flash) for routing and a more powerful, specialised model (gemini-2.5-pro) for analysis.
The Other Half: The Client-Side Auth Flow
The get_credentials function is only half the story. When it calls tool_context.request_credential(), it signals to the runtime that it needs user authentication. The cli.py script acts as that runtime, or “Agent Client.”
The cli.py script is responsible for the user-facing interaction. As you can see in the handle_agent_run function within cli.py, the script’s main loop does two key things:
It iterates through agent events and uses a helper to check for the specific adk_request_credential function call.
When it detects this request, it pauses, prints the authorisation URL for the user, and waits to receive the callback URL.
It then bundles this callback URL into a FunctionResponse and sends it back to the agent, which can then resume the tool call, this time successfully.
This client-side loop is the essential counterpart to the get_credentials pattern.
Production Considerations: Moving Beyond the Workbench
This entire setup is designed as a high-fidelity local workbench. Before moving to a production environment, several changes are necessary:
Persistent Sessions: The InMemorySessionService is ephemeral. For production, you must switch to a persistent service like DatabaseSessionService (backed by a database like PostgreSQL) or VertexAiSessionService. This is critical for remembering conversation history and, most importantly, for securely storing the user’s OAuth tokens.
Secure Credential Storage:Â While caching tokens in the session state is acceptable for local testing, production systems should encrypt the token data in the database or use a dedicated secret manager.
Runtime Environment: The cli.py would be replaced by a web server framework (like FastAPI or Flask) if you are deploying this as a web application, or adapted to the specific requirements of a platform like Vertex AI Agent Engine.
By building with this modular, Journey 2 approach, you create agents that are not only more capable and robust but are also well-architected for the transition from local development to a production-grade deployment.
Explore the Code
Explore the full source code in the GitHub repository to see these concepts in action. The README.md file provides all the necessary steps to get the agent up and running on your local machine. By combining the conceptual overview in this article with the hands-on code, you’ll be well-equipped to build your own robust, production-ready agents with the Google ADK.