While Google Apps Script (GAS) is a powerful tool for Google Workspace automation, platform and computational constraints often limit its ability to handle advanced workloads. Gemini Managed Agents provide remote Linux sandboxes equipped with bash execution. This article introduces an architecture integrating GAS with a Linux sandbox to execute tasks beyond the capabilities of Apps Script alone. By streaming generated artifacts directly from within the Linux sandbox to Google Drive, this approach bypasses API payload limits, eliminates token overhead, and achieves high-throughput cloud automation.
Following on from our recent post exploring Gemini Managed Agents and the Google Workspace CLI, Google Developer Expert Kanshi Tanaike has shared a number of examples of how Gemini Managed Agents can be used to overcome some of the limitations of Google Apps Script. The cornerstone of these solutions is leaning into the dedicated Linux sandbox created with Managed Agents and using ggsrun CLI tool to enable direct cloud-to-cloud streaming between the sandbox and Google Drive, negating any limitations with UrlFetchApp.
Some key highlights from Tanaike’s post are:
Heavy Compute Offloading: Offloads tasks impossible in Apps Script such as dynamic screenshots and JSON data generation with Playwright, audio synthesis with FFmpeg, and TypeScript compilation via esbuild.
Persistent Multi-Client Sandboxes: A single sandbox (environmentId) can be shared across Apps Script triggers and local Node.js workstations, preserving pre-installed toolchains and shared datasets.
Proven Performance Gains: Avoiding 50 MB response payload limits by streaming binary artifacts directly to Drive at high speeds.
Follow the source link for the complete technical breakdown and benchmark data.
Gemini Spark, announced at Google I/O 2026, was launched as “your 24/7 personal AI agent that helps you work more efficiently by autonomously taking action on your behalf, under your direction… Spark operates in a fully managed, secure runtime on Google Cloud, meaning you get enterprise-grade security without ever having to manage the underlying infrastructure.”
If you have access to Gemini Spark, you will already know it has deep integrations to read and write your Google Workspace data. Like most of Google’s products, the infrastructure used by Gemini Spark is also accessible to developers. In the case of Gemini Spark, a key component is Managed Agents:
Managed agents on the Gemini API give you a configurable agent harness. A single API call provisions a Linux sandbox where the agent reasons, executes code, manages files, and browses the web autonomously.
The particular flavour of Managed Agents I was interested in was:
Antigravity agent: General-purpose managed agent powered by Gemini 3.7 Flash. Runs code, manages files, and searches the web inside a secure Linux sandbox hosted by Google. You can extend it with your own instructions, skills, and data to build a custom agent.
To replicate Gemini Spark’s integration with Google Workspace, this demo project installs the Google Workspace CLI (gws) into an agent orchestrated from a Google Apps Script web app.
Turning Raw Drive Docs into Executive Briefings
The setup is deliberately modest. Imagine a Google Drive folder containing raw Google Docs. in my case, copies of Google Workspace Updates and Gemini Enterprise announcements. I want to convert this growing folder into structured, evidence-backed briefings for Workspace developers and business stakeholders, a clean source register, and visual summaries.
An Apps Script web app runs as the accessing user.
The user supplies a source folder ID and an output folder ID.
Apps Script starts a Managed Agent interaction.
A virtual environment is prepared with:
the gws Google Workspace CLI;
a Python document and analysis toolchain including python-pptx, python-docx, matplotlib, and pandas;
an agent policy (AGENTS.md) and workflow skill (SKILL.md);
a per-run .env file containing the current user’s OAuth access token, approved folder IDs, and optional date timestamps.
The agent accesses Drive through gws to find and retrieve relevant source documents using server-side queries.
It generates multi-format deliverables (Word, PowerPoint, chart, CSV) locally in /workspace/output.
It uploads the final reports to the selected output folder.
One of the biggest advantages of a Managed Agent over a basic API call is the ability to provision a full Linux sandbox, preconfigured with Python 3.12 and Node.js 22. You can also install additional libraries and binaries dynamically. In this project, the agent creates the following outputs before uploading them:
Output Asset
Tooling / Libraries
Pre-installed vs. Runtime
Purpose
source-register.csv
pandas
Pre-installed
Aggregates metadata, sorts, and filters document sources.
updates-summary-chart.png
matplotlib
Installed via pip
Visualizes update frequencies and product area breakdowns.
executive-presentation.pptx
python-pptx
Installed via pip
Compiles an executive presentation deck formatted into structured slides.
executive-briefing.docx
python-docx
Installed via pip
Generates a styled Word document with headers, callouts, and structured tables.
executive-briefing.md
Plain Text / Markdown
Native Filesystem
Provides a lightweight narrative briefing summary.
run-manifest.json
JSON / jq
Pre-installed
Audit record of all actions taken during the run.
Screenshots
Creating a Managed Agent
To allow the agent to interact with Drive on the user’s behalf without an interactive login prompt, Apps Script fetches the user’s OAuth token:
const userToken = ScriptApp.getOAuthToken();
To limit the potential blast radius, the appsscript.json manifest strictly scopes requested permissions:
Using drive.file ensures that write access is scoped strictly to files created or explicitly opened by the application, preventing modification of other Google Drive files.
Seamless Credential Delegation to gws
A common challenge with a headless agent environments is authentication. How do you allow a CLI tool to act on the user’s behalf without interactive browser logins (gws auth login) or baking long-lived service account keys into the agent definition?
The gws CLI addresses this by supporting environment-based authentication. When invoked, gws checks for the GOOGLE_WORKSPACE_CLI_TOKEN environment variable. By mounting a transient /workspace/.env file directly into the sandbox environment during interaction creation, gws automatically authenticates all Drive API operations using the active user’s short-lived OAuth token.
Because this token exists solely in memory within the temporary container instance, it naturally expires and is never saved to persistent storage.
Example Interaction Creation Request
The entire custom Managed Agent is created with a single API call. An example payload used in this demo project is included below:
The payload allows developers to include both an AGENTS.md and SKILL.md into the sandbox
System Instructions (.agents/AGENTS.md): Establishes boundaries requiring the agent to only read from SOURCE_FOLDER_ID, only write to OUTPUT_FOLDER_ID, and never disclose /workspace/.env. It instructs the agent to treat all retrieved Workspace text as untrusted reference data, ignoring any embedded instructions or prompt injections.
Workflow Skill (.agents/skills/update-intelligence/SKILL.md): Provides structured workflow guidance for using gws drive with field masks, executing Python scripts for artifact generation, and mapping file extensions to Google Drive MIME types during upload.
Tooling and Sandbox Bootstrapping (tools & bootstrap.sh)
As well as the instructions and policies, the payload also configures the agent with execution capabilities and installs the required runtime toolchain:
tools: [{ type: "code_execution" }]: Activates the Linux terminal and Python interpreter, allowing the Antigravity agent to write, execute, and debug scripts inside its container.
bootstrap.sh & requirements.txt: The payload requires the agent to execute /workspace/bootstrap.sh on startup. This script downloads the static gws CLI binary and installs the specified Python libraries (pandas, matplotlib, python-docx, python-pptx) dynamically.
Asynchronous Execution and State Persistence (background & store)
A workflow that inventories dozens of Google Docs, generates slide decks and Word documents, and uploads deliverables to Drive can take several minutes to run. Two flags make this practical from Google Apps Script:
background: true: Launches the agent run asynchronously, immediately returning an interaction_id. The frontend UI simply polls the interaction endpoint periodically for status and live step progress.
store: true: Persists the interaction transcript and environment state on Google’s infrastructure. This not only allows the web app to retrieve the final summary and token usage metrics when finished, but also enables multi-turn follow-ups—allowing users to ask the agent to revise or refine documents within the same container without re-bootstrapping.
Summary and References
Combining Managed Agents with the Google Workspace CLI unlocks opportunities to automate complex, multi-step document workflows. Combining the latest Gemini models via the Antigravity agent with a full 4 vCPU / 16 GB Linux sandbox gives developers the ability to build capable autonomous agent workflows directly from Google Apps Script.
The flip side of this, particularly with recent news stories of agents escaping sandboxes, is the implication of providing managed agents with a user token. This is no different from the OAuth token flow already used when extending applications; the difference here is that it is being passed to an agent that can “think” and “reason”. In Google Workspace, it is positive to see that agentic security and governance is being surfaced to administrators for built-in services, but it feels to me that the protocols and standards are still catching up. For users, my advice is to pause and think before you click continue in authentication flows.
File format conversion in Google Apps Script ranges from simple string manipulations to multi-stage pipeline workflows. Depending on your source format and required output, the optimal solution might be a quick service workaround, a native Drive API call or a dedicated library capable of handling indirect conversions and OCR.
The Lightweight Approach: Converting HTML to Plain Text
At the simplest end of the spectrum, you can convert markup into plain text without writing custom regular expressions or importing heavy third-party parsers.
A clever technique shared by Kanshi Tanaike in his guide on converting HTML to plain text uses the Gmail Service. You pass the HTML string into GmailApp.createDraft() and retrieve the formatted plain text using getPlainBody().
Code Example: HTML Conversion via Temporary Gmail Draft
/**
* Converts an HTML string to plain text using a temporary Gmail draft.
*/
function convertHtmlToPlainText() {
const htmlContent = '<div><h1>Quarterly Summary</h1><p>Project completed on <b>Schedule</b>.</p></div>';
// Create a temporary draft to utilise Gmail's HTML parser
const draft = GmailApp.createDraft('', '', '', { htmlBody: htmlContent });
const plainText = draft.getMessage().getPlainBody();
// Immediately delete the draft to clean up
draft.deleteDraft();
Logger.log(plainText);
// Output:
// Quarterly Summary
// Project completed on Schedule.
}
This clever workaround executes quickly and avoids manual string parsing. It is ideal when cleaning up web scraper output, preparing logs or stripping HTML tags from user input but comes with the downside of requiring a Gmail authorisation scope.
Direct Drive API Conversions
The Drive API provides a direct conversion mechanism. For example, by passing an HTML blob to Drive.Files.create(), Google Drive can convert this into a fully formatted Google Doc. You can then open the generated document using DocumentApp to extract clean text or manipulate document elements before trashing the temporary file.
Code Example: Converting HTML to a Doc and Extracting Text
To use this method, enable the Drive API under Services in the Apps Script editor (using Drive API v3).
/**
* Converts an HTML string into a Google Doc via Drive API,
* extracts the plain text from the Doc body, and trashes the file.
*/
function convertHtmlToDocAndExtractText() {
const htmlContent = '<h1>Project Summary</h1><p>The roll-out was successful across all regions.</p>';
const blob = Utilities.newBlob(htmlContent, MimeType.HTML);
const fileMetadata = {
name: 'Temporary Converted Doc',
mimeType: MimeType.GOOGLE_DOCS
};
// Create a Google Doc from the HTML blob using Drive API v3
const file = Drive.Files.create(fileMetadata, blob, { supportsAllDrives: true });
// Open the created Doc and extract text content
const doc = DocumentApp.openById(file.id);
const extractedText = doc.getBody().getText();
Logger.log(extractedText);
// Output:
// Project Summary
// The roll-out was successful across all regions.
// Trash the temporary Google Doc to maintain a clean Drive
DriveApp.getFileById(file.id).setTrashed(true);
}
This pattern provides a reliable way to turn web markup into native document elements or extract structured text while preserving structural hierarchy.
The Heavy-Duty Approach: Multi-Step Conversions and OCR
Direct API calls work well when Google Drive natively supports the source and target MIME type pairing. However, problems arise when there is no direct conversion path – for instance, converting a Microsoft Word file (.docx) directly into a PDF, or converting an image into editable text via Optical Character Recognition (OCR).
Path Resolution: The library evaluates the source MIME type and the target MIME type.
Direct Conversion: If a direct route exists (e.g. Google Doc to PDF), it executes the transformation immediately.
Indirect Pipeline: If no direct route exists (e.g. Word document to PDF), it automatically determines the intermediate steps. It first converts the Word file to an intermediate Google Doc, then exports that Google Doc as a PDF, before cleaning up temporary files.
OCR Processing: For image formats (.png, .jpg), it passes the file through Drive API’s OCR engine to extract embedded text directly into a Google Doc.
This approach removes the manual work of tracking MIME compatibility matrices and chaining multiple Drive API requests manually.
Summary
Matching your conversion method to the task keeps your Google Apps Script projects lean and reliable. For rich document generation and extraction, Drive API conversions offer structured flexibility.
Google Apps Script is easily my favorite runtime, so much so that I wrote a book about it. Yet, being a low-code environment it comes with its quirks, quotas and limitations. In a recent project I needed to process 300 spreadsheets totalling over 5 million cells in over 80,000 rows. I could eithr abandon the platform and spin up a “real” backend, or get creative. Really creative.
Dmitry Kostyuk has published a detailed guide on bypassing Google Apps Script limits to process very large JSON datasets. Rather than completely abandoning Apps Script, Dmitry shares an example zero-cost pipeline to process over 5 million spreadsheet cells without timing out. Here’s the breakdown of his architecture:
Extract and compress the master data snapshot locally using Node.js and fflate (Dmitry highlights this could be hosted in Cloud Run).
Encode to Base64 and slice it into text chunks.
Automate multipart file uploads to Google Drive using the open-source Google Workspace CLI (gws).
Use Apps Script as a thin proxy to fetch and return the text strings to the client browser.
Reassemble, decompress and parse the chunks in the browser using Alpine.js.
A key component of this workflow is the efficiency of the open-source Google Workspace CLI (gws). Rather than writing custom API upload scripts, it uses the gws +upload helper to automatically detect MIME types and execute multipart Drive uploads in a single command.
Crucially, this solution moves heavy computation to the developer machine and the user browser, keeping the Apps Script runtime focused purely on serving data. This relies on HtmlService for client-side processing and you can deploy it in standalone web apps or custom sidebars and dialogue boxes for editors like Google Docs and Sheets.
Dmitry also uses his open-source Apps Script Engine Template, which provides a pre-configured Vite, clasp and environment management setup to speed up development and allow you to standardise your build workflow.
Read Dmitry’s full post for the complete code snippets and a look at the technical trade-offs of this approach. It is a fantastic example of using platform constraints to define a more efficient architecture!
Have you ever used Google Workspace Studio? If so, you might have seen its “Google Drive Folder” trigger that instantly reacts to new files. It is an excellent feature for no-code automation, however, it has its limitations in what you can do in subsequent Studio steps.
If you are interested in this capability but would like to do more, I’ve just published an example repo which explores Google Drive events which is currently in the Developer Preview as part of the new Workspace Events API. With this you can subscribe to real-time changes including file uploads to specific folders, which opens opportunities for programmatically interacting with Google Drive and other services.
A Practical Application: Enterprise Data Pipelines
For me one of the most valuable applications for this capability lies within enterprise data pipelines. In many corporate environments, business users lack direct access to Google Cloud Storage buckets which are often used in data pipelines and instead they rely entirely on Google Drive for file sharing and collaboration.
By subscribing to folder events, developers can automatically trigger the movement or digestion of data the exact moment a user drops a new file into a designated Drive folder. This creates a seamless, automated bridge between user-facing storage interfaces and backend processing systems.
Architectural Highlights from the Demo
To demonstrate this I’ve published a complete proof of concept in the Google Drive to Google Sheets Notification Add-on Tutorial. This is a basic Google Workspace Add-on which lets users ‘subscribe’ to a Google Drive folder and get realtime updates to a Google Sheet. The example replicates what is possible already with Workspace Studio, but the idea is once you know how to set up a subscription you can do anything you want with that file. The example, unusually for me, has zero Apps Script meaning for chunkier data problems you can easily scale on Google infrastructure. If you look at the codebase, hopefully there are several design patterns that are useful for Workspace developers:
Infrastructure as Code (IaC): Rather than configuring the Google Cloud project services manually via the console, the repository includes Terraform configuration files to automatically provision the necessary Pub/Sub topics, service accounts, and IAM bindings required for secure message delivery. Using a ‘Infrastructure as Code’ makes it easy to ‘apply’ updates to your project.
Built-in Resilience: The Terraform configuration explicitly sets up a dead-letter topic. If your Event Processor fails to handle a file notification, the message routes to this secondary queue, ensuring no data events are lost during temporary outages or bugs.
Decoupled Execution: The example also separates user interaction from backend processing. An Add-on Handler manages user authorisation and API subscription creation meanwhile, a dedicated Node Cloud Function acts as a secure Event Processor, picking up messages asynchronously via Pub/Sub to prevent timeout issues.
The 3 Gotchas We Are Solving
The Drive API already has a watch endpoint but there three common obstacles that the new Workspace Events API gracefully solves:
The “Blind Ping”: When watching a folder with the legacy files.watch, the notification only tells you that the folder’s contents changed; it does not explicitly tell you which file was added. You are therefore forced to perform a secondary query to find the new file. The new Workspace Events API solves this by handing you the exact ID of the newly added file directly in the payload.
The Public URL Requirement: Standard webhooks require a public HTTPS URL and domain verification. By routing our events through Google Cloud Pub/Sub, this approach allows you to receive notifications privately and securely.
The Empty Payload: Google typically sends legacy notification data hidden within HTTP headers, which can be difficult to parse in certain environments, such as Apps Script. This is less of a concern in the shared example as we are using only Cloud Run Functions.
Summary
Hopefully, the demo is helpful showcasing what is possible using the Google Workspace Events API and Infrastructure as Code architectures. If there are any hints or tips I have missed, feel free to drop them in the comments below.
Why use a dedicated app when you can simply ask Gemini to write and run the Python code for you? A look at the power of Google Apps Script and GenAI
For many Google Workspace developers, handling complex file formats or performing advanced data analysis has traditionally meant navigating the limitations of Apps Script’s built-in services. We have previously featured solutions for these challenges on Pulse, such as merging PDFs and converting pages to images or using the PDFApp library to “cook” documents. While effective, these methods often rely on loading external JavaScript libraries like pdf-lib, which can be complex to manage and subject to the script’s memory and execution limits.
While users of Gemini for Google Workspace may already be familiar with its ability to summarise documents or analyse data in the side panel, those features are actually powered by the same “Code Execution” technology under the hood. The real opportunity for developers lies in using this same engine within Apps Script to build custom, programmatic workflows that go far beyond standard chat interactions.
A recent project by Stéphane Giron highlights this path. By leveraging the Code Execution capability of the Gemini API, it is possible to offload intricate document and data manipulation to a secure Python sandbox, returning the results directly to Google Drive.
Moving beyond static logic
The traditional approach to automation involves writing specific code for every anticipated action. The shift here is that Gemini Code Execution does not rely on a pre-defined set of functions. Instead, when provided with a file and a natural language instruction, the model generates the necessary Python logic on the fly. Because the execution environment includes robust libraries for binary file handling and data analysis, the model can perform varied tasks without the developer needing to hardcode each individual routine. Notably, the model can learn iteratively which means if the generated code fails, it can refine and retry the script up to five times until it reaches a successful output.
While basic data analysis is now a standard part of Gemini for Workspace, having direct access to the library list in the Gemini sandbox opens up additional specialised, developer-focused avenues:
Dynamic Document Generation: Using python-docx and python-pptx, you can programmatically generate high-fidelity Office documents or presentations based on data from Google Workspace, bridging the gap between ecosystems without manual copy-pasting. [Here is a container bound script based on Stéphane code for Google Docs that generates a summary PowerPoint file]
Programmatic Image Inspection: Using Gemini 3 Flash, you can build tools that inspect images at a granular level. For example, a script could process a batch of site inspection photos, using Python code to “zoom and inspect” specific equipment labels or gauges, and then log those values directly into a database.
The mechanics and constraints
The bridge between Google Drive and this dynamic execution environment follows a straightforward pattern:
File Preparation: The script retrieves the target file from Drive and converts the blob into a format compatible with the Gemini API.
Instruction & Execution: Along with the file, a prompt is sent describing the desired outcome.
The Sandbox: Gemini writes and runs the Python code required to fulfil the request.
Completion: Apps Script receives the modified file or data and saves it back to the user’s Drive.
However, there are important boundaries to consider. The execution environment is strictly Python-based and has a maximum runtime of 30 seconds. Furthermore, developers should be mindful of the billing model: you are billed for “intermediate tokens,” which include the generated code and the results of the execution, before the final summary is produced.
Get started
For those interested in the implementation details, Stéphane has shared a repository containing the Apps Script logic and the specific configuration needed to enable the code_execution tool.
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.
2. Google Apps Script (Code.js)
Update Manifest (appsscript.json): Add the https://www.googleapis.com/auth/userinfo.email scope.
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) andMethod 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!
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.
In this video we build a Chat app as a Google Workspace add-on with Apps Script and extend it to other Workspace applications (Calendar, Gmail, Drive, Docs, Sheets, and Slides).
To see this new framework in action, Pierrick Voulet from the Google Developer Relations team has released a fantastic video tutorial, ‘Build a Chat app as a Google Workspace add-on with Apps Script’.
The ‘OOO Assistant’: A Practical Demonstration
The video centres on building an ‘Out-of-Office Assistant’ that can block out the user’s Calendar for the day and set a Gmail out-of-office auto-reply. The real magic is how this Chat-based logic extends into a full Workspace Add-on, available in the sidebars of Gmail, Calendar, Drive, Docs, Sheets and Slides. This is achieved with minimal code changes, primarily by configuring the appsscript.json manifest to include:
A homepageTrigger to display a UI card in the add-on sidebar.
universalActions that reuse the exact same functions (blockDayOut(), setAutoReply()) originally built for the Chat quick commands.
The video provides a complete overview of the process, and you can get hands-on with the shared code yourself. For more technical details, check out the official Extend Google Chat documentation.
Learn how to add, update and search or query for custom Google Drive properties in file metadata in with Google Apps Script
Managing and searching for project files in Google Drive can be a challenge, especially when naming conventions are not followed. While Google’s Drive Labels API offers a solution, its use is often restricted to administrators, leaving many developers looking for a more accessible alternative.
Over on his blog, Yagisanatode, developer Scott Donald provides an in-depth tutorial on using custom file properties with Apps Script. This technique allows you to programme-matically attach your own searchable metadata—like a project ID, version number, or status—to any file in Drive.
The tutorial walks you through the entire process, including:
Enabling the Drive API v3 advanced service.
Using Drive.Files.update() to add properties to a file.
Crafting queries with Drive.Files.list() to find files based on your custom properties.
For developers working at scale, the tutorial also covers how to avoid slow, iterative updates. Scott introduces a powerful batch update method that uses UrlFetchApp to efficiently modify thousands of files in a single request.
This is an excellent, practical guide for any intermediate developer looking to build more organised and robust file management systems in Google Drive.