As AI supercharges our productivity tools, we face a critical trade-off between powerful new features and the responsive user experience we depend on.
The promise of generative AI within our daily productivity tools is immense. Imagine summarizing lengthy reports in Google Docs with a single click, generating complex formulas in Sheets from plain English, or drafting professional email responses in Gmail instantly. This is the new frontier for [Automatically create new folders in Google Drive, generate templates in new folders, fill out text automatically in new files, and save info in [Automated Web Scraping with [Multilingual Text-to-Speech Tool with SocialSheet Streamline Your Social Media Posting 123](https://votuduc.com/Multilingual-Text-to-Speech-Tool-with-Google-Workspace-p809282)](https://votuduc.com/Automated-Web-Scraping-with-Google-Sheets-p292968)](https://workspace.google.com/marketplace/app/auto_create_folder_and_files/430076014869) Add-ons.
However, this power introduces a fundamental conflict. The synchronous, immediate nature of a user interface is at odds with the asynchronous, computationally intensive reality of large language models (LLMs). We’re caught in a tug-of-war: on one side, the desire to integrate groundbreaking AI capabilities; on the other, the non-negotiable demand for a fast, responsive, and reliable user experience. Trying to serve the former without respecting the latter leads directly to user frustration, errors, and ultimately, abandonment of your add-on.
At the heart of this conflict lies a critical platform constraint: the [AI Powered Cover Letter [Automated Job Creation in Real Time Jobber and Google Sheets Integration from Gmail](https://votuduc.com/Automated-Job-Creation-in-Jobber-from-Gmail-p115606) Engine](https://votuduc.com/AI-Powered-Cover-Letter-Automated Quote Generation and Delivery System for Jobber-Engine-p111092) execution time limit. When a user interacts with your add-on’s UI—clicking a button, for instance—it triggers an Apps Script function. To protect the user from frozen interfaces and ensure the host application (Docs, Sheets, etc.) remains responsive, Google enforces a strict 30-second execution limit on these synchronous calls.
Think of it as a shot clock.
For years, this 30-second window was more than generous for common add-on tasks:
Fetching data from a CRM API.
Manipulating text and tables within a document.
Calling standard Google APIs.
These operations are typically measured in milliseconds or a few seconds at most. The 30-second limit was a safety net, not a daily hurdle.
Enter generative AI. Calling an LLM like Google’s Gemini is fundamentally different from a traditional REST API call. It’s not a simple data retrieval; it’s a request for complex computation and creation. Several factors contribute to this process being inherently slow and variable:
Model Scale: These models contain billions of parameters. The inference process—the “thinking”—requires significant computational resources.
Prompt Complexity: The length and complexity of your input directly impact processing time. Asking the AI to summarize a 20-page document is far more intensive than asking it to correct the grammar in a single sentence.
Generation Length: The time it takes to generate a response is proportional to the length of that response. A multi-paragraph summary will naturally take longer than a one-line suggestion.
Non-Deterministic Nature: Unlike a database query that returns a predictable result in a predictable time, LLM response times can fluctuate based on system load and the specific path the model takes to generate the output.
A request to the Gemini API can easily take 10, 20, or even 45+ seconds. The moment your process crosses that 30-second threshold, the Apps Script environment terminates it. Your add-on fails, the user is confused, and the “magic” of AI is replaced by the frustration of a broken tool. This makes the synchronous “call-and-wait” approach, which worked for years, completely untenable for serious AI integration.
So, how do we run a 45-second process inside a 30-second container? The answer is: we don’t. We change the architecture. This is where the Sidecar Pattern comes in.
The core principle is to decouple the long-running task from the UI’s synchronous execution path. Instead of making the Apps Script function do all the heavy lifting, we delegate it to a separate, dedicated service—our “sidecar.”
Here’s how it works conceptually:
**The UI Request (Fast): The user clicks a button in your add-on. The Apps Script function’s only job is to immediately fire off a quick, non-blocking request to an external service (our sidecar). This request simply says, “Please start this heavy AI job with these parameters.” This initial call takes milliseconds and completes well within the 30-second limit. The UI can then immediately update to show a “Processing…” or “Generating…” state.
The Heavy Lifting (Slow): The sidecar, running on a robust and scalable platform like Google Cloud Run, receives the request. It is not bound by the 30-second Apps Script limit. It can take as long as necessary to call the Gemini API, process the results, and prepare the final output.
The Result Retrieval (Asynchronous): While the sidecar works, the add-on UI periodically polls a status endpoint or waits for a notification to know the job is done. Once the result is ready, another quick Apps Script function fetches the completed data from the sidecar and updates the document.
This pattern transforms the user experience. The UI never freezes. The 30-second timeout is no longer a threat. By offloading the intensive work to a specialized sidecar service, we allow each component to do what it does best: Apps Script manages the UI, and Cloud Run handles the heavy, long-running computation.
To sidestep the rigid execution limits of the Automated Client Onboarding with Google Forms and Google Drive. environment, we’re not trying to force a long-running process where it doesn’t belong. Instead, we’ll offload it. The core of our solution is an asynchronous pattern where the Workspace Add-on acts as a lightweight client, delegating the heavy lifting to a robust, scalable backend service. We call this the “Sidecar AI Pattern” because our Cloud Run service runs alongside the main user interface, handling a specialized task—AI processing—that the primary environment isn’t suited for.
This decoupling is the magic trick. It allows the user interface to remain responsive and finish its execution in seconds, while the complex, time-consuming AI generation happens independently in the background. Let’s break down the components and the flow.
This architecture is built on three key pillars, each with a distinct and vital role.
Automated Discount Code Management System Add-on (The Initiator): This is your user-facing component, living inside Google Docs, Sheets, or Gmail. Written in Apps Script, its primary job is to provide the UI controls (buttons, sidebars, menus) and capture the user’s intent. In our pattern, it does not call the Gemini API directly. Instead, it gathers the necessary context (e.g., selected text, document content, user prompt), packages it into a payload, and makes a single, swift, non-blocking call to our sidecar service. Its execution lifecycle is brief, ensuring it never hits the dreaded timeout wall.
Cloud Run Service (The Workhorse): This is the heart of the operation—our asynchronous sidecar. It’s a stateless container that exposes a secure HTTP endpoint. When the Workspace Add-on calls this endpoint, the Cloud Run service immediately acknowledges the request and takes ownership of the task. It then handles the entire lifecycle of the AI interaction: authenticating with the Gemini API, sending the prompt, patiently waiting for the response (which could take seconds or minutes), and processing the result. Because it runs in a separate, more permissive environment, it isn’t bound by Apps Script’s short execution timeouts.
Gemini API (The Brain): This is the generative AI service doing the actual work. Whether you’re summarizing text, generating content, or analyzing data, the call to the Gemini API is the source of our latency. The complexity of the model and the length of the requested generation directly impact response time, making it fundamentally incompatible with the synchronous, quick-response nature of a UI framework.
A fourth, implicit component is a State Store, such as Firestore or Cloud Storage. Since the process is asynchronous, we need a place for the Cloud Run service to deposit the result once it’s ready, and a way for the Workspace Add-on to retrieve it later.
Understanding the sequence of events is crucial to grasping the power of this pattern. It transforms a single, long, and fragile process into a series of short, resilient steps.
[Diagram: A sequence diagram showing the interaction between the Workspace Add-on, Cloud Run, Gemini API, and Firestore would be perfect here.]
Trigger: The user clicks a button in the Workspace Add-on, like “Summarize”.
Fire-and-Forget Request: The Add-on’s Apps Script code generates a unique jobId. It then makes a quick UrlFetchApp call to the Cloud Run endpoint, passing the document content and the jobId. This is a non-blocking call. The Add-on immediately receives a 202 Accepted response and its script execution finishes, perhaps updating the UI to a “Processing…” state. The entire interaction takes less than a second.
Sidecar Processing: The Cloud Run service receives the request and begins the actual work. It calls the Gemini API with the provided content. This is the long-running step; Cloud Run will wait patiently for the API to respond, which could take 30 seconds, 90 seconds, or more.
Store Result: Once the Gemini API returns the generated summary, the Cloud Run service writes the result to a Firestore document, using the unique jobId as the document key.
Polling for Result: Back in the Workspace Add-on, a simple client-side poller (e.g., a setInterval function in the sidebar’s HTML) periodically calls a lightweight Apps Script function every few seconds.
Check and Retrieve: This Apps Script function queries Firestore, checking for the existence of a document with the corresponding jobId. If it doesn’t exist, it does nothing. If it exists, it retrieves the summary from the document.
Update UI: Once the result is retrieved, the poller updates the Add-on’s UI, replacing the “Processing…” message with the final, AI-generated summary and inserting it into the document. The loop is complete, and the user experience was never interrupted.
While you could theoretically host this sidecar logic on a VM or another serverless platform, Cloud Run offers a combination of features that make it uniquely suited for this task.
Scale to Zero (and to N): Workspace Add-on usage can be spiky. You might have dozens of users hitting your service one minute and none the next. Cloud Run automatically scales instances up to meet demand and, crucially, scales down to zero when idle, meaning you pay absolutely nothing for unused capacity.
Generous Timeouts: A standard Cloud Run service has a request timeout of up to 60 minutes. This provides an enormous buffer, easily accommodating even the most complex and time-consuming generative AI calls without breaking a sweat.
Language and Library Freedom: You are not constrained by Apps Script’s JavaScript environment. You can write your sidecar in JSON-to-Video Automated Rendering Engine, Go, Node.js, or Java, allowing you to use the official, feature-rich Google AI SDKs, advanced data processing libraries, and familiar tooling. This dramatically simplifies development and improves performance.
Serverless Simplicity: You simply provide a container image, and Google Cloud handles the underlying infrastructure, scaling, and request routing. This eliminates operational overhead, letting you focus on the application logic instead of managing servers, patches, and networks.
Integrated Security: Securing the endpoint is straightforward. You can configure the Cloud Run service to only allow authenticated invocations and grant the Apps Script’s associated service account the “Cloud Run Invoker” IAM role. This ensures that only your Add-on can trigger the AI processing, preventing unauthorized access.
With the architecture defined, let’s dive into the practical implementation. This guide breaks down the process into four distinct, manageable steps, providing the core code and configuration you need to build this pattern.
The heart of our solution is the “sidecar” service running on Cloud Run. This service needs to accept a task, immediately return a unique identifier, and provide a separate endpoint to check the task’s status and retrieve its result. We’ll use Python with the Flask framework for this example due to its simplicity and strong presence in AI/ML workloads.
1. The Application Code (main.py)
Our Flask application will manage tasks using a simple in-memory dictionary. For a production environment, you would replace this with a more robust state manager like Firestore or Memorystore (Redis) to handle scaling and state persistence across service instances.
import os
import uuid
import time
import threading
from flask import Flask, request, jsonify
app = Flask(__name__)
# WARNING: In-memory storage is for demonstration purposes only.
# In production, use a persistent, scalable solution like Firestore or Redis.
tasks = {}
def long_running_ai_task(task_id, input_data):
"""
Simulates a time-consuming AI process.
Updates the task status in the shared dictionary.
"""
print(f"Starting task {task_id} with data: {input_data}")
tasks[task_id]['status'] = 'PROCESSING'
# Simulate a 45-second AI process
time.sleep(45)
# In a real application, this would be the result from your AI model
result_data = {
"summary": f"This is the processed summary for input: {input_data.get('text', '')[:30]}...",
"entities": ["Cloud Run", "Apps Script", "AI Pattern"]
}
tasks[task_id]['status'] = 'COMPLETED'
tasks[task_id]['result'] = result_data
print(f"Completed task {task_id}")
@app.route('/process', methods=['POST'])
def start_processing():
"""
Endpoint to start a new processing task.
Immediately returns a task ID.
"""
if not request.json or 'data' not in request.json:
return jsonify({"error": "Missing data payload"}), 400
task_id = str(uuid.uuid4())
input_data = request.json['data']
tasks[task_id] = {'status': 'PENDING'}
# Run the long-running task in a separate thread to avoid blocking the request.
# In production, this should be offloaded to a task queue like Cloud Tasks.
thread = threading.Thread(target=long_running_ai_task, args=(task_id, input_data))
thread.start()
return jsonify({"task_id": task_id}), 202
@app.route('/status/<task_id>', methods=['GET'])
def get_status(task_id):
"""
Endpoint to check the status and result of a task.
"""
task = tasks.get(task_id)
if not task:
return jsonify({"error": "Task not found"}), 404
return jsonify(task)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
2. The Dockerfile
Next, we need to containerize the application.
# Use the official lightweight Python image.
FROM python:3.9-slim
# Set the working directory
WORKDIR /app
# Copy local code to the container image.
COPY . .
# Install production dependencies.
RUN pip install --no-cache-dir Flask gunicorn
# Run the web server on container startup.
# Gunicorn is a production-ready WSGI server.
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
3. Deployment to Cloud Run
With the code and Dockerfile ready, deploy the service using the gcloud CLI. The crucial flag here is --no-allow-unauthenticated, which secures our service so that only authenticated requests from our Apps Script project (or other authorized principals) can invoke it.
# Set your project ID
export PROJECT_ID="your-gcp-project-id"
gcloud config set project $PROJECT_ID
# Build the container image using Cloud Build
gcloud builds submit --tag gcr.io/$PROJECT_ID/workspace-sidecar
# Deploy the container to Cloud Run
gcloud run deploy workspace-sidecar-service \
--image gcr.io/$PROJECT_ID/workspace-sidecar \
--platform managed \
--region us-central1 \
--no-allow-unauthenticated
After deployment, take note of the Service URL provided in the output. You will need it in Step 3.
The user interface within the Automated Email Journey with Google Sheets and Google Analytics Add-on (e.g., a sidebar in Google Sheets) must provide clear feedback to the user. It cannot simply freeze while waiting for the long-running task.
Our UI will consist of a simple HTML file (Sidebar.html) with client-side JavaScript to manage the different states: idle, processing, and complete.
Sidebar.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
--- Using a simple CSS framework for styling -->
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
</head>
<body>
<div class="w3-container w3-padding">
<h4>AI Text Analyzer</h4>
<p>Enter text below and click analyze. The process may take up to a minute.</p>
<textarea id="inputText" class="w3-input w3-border" style="resize:vertical" rows="6">
The Cloud Run Sidecar AI Pattern is an effective method for eliminating UI timeouts in [Automated Google Slides Generation with Text Replacement](https://votuduc.com/Automated-Google-Slides-Generation-with-Text-Replacement-p850694) Add-ons by offloading long-running tasks.
</textarea>
<button id="analyzeButton" class="w3-button w3-blue w3-margin-top" onclick="startAnalysis()">Analyze Text</button>
--- Status Display Area -->
<div id="statusContainer" class="w3-panel w3-pale-blue w3-border w3-padding w3-margin-top" style="display:none;">
<p id="statusText">Processing...</p>
<div class="w3-light-grey">
<div id="progressBar" class="w3-container w3-blue w3-center" style="width:25%">...</div>
</div>
</div>
--- Result Display Area -->
<div id="resultContainer" class="w3-panel w3-border w3-padding w3-margin-top" style="display:none;">
<h5>Analysis Complete:</h5>
<pre id="resultText" style="white-space: pre-wrap; word-wrap: break-word;"></pre>
</div>
</div>
<script>
// Global variable to hold the polling interval ID
let pollIntervalId = null;
function startAnalysis() {
// 1. Update UI to "processing" state
document.getElementById('analyzeButton').disabled = true;
document.getElementById('statusContainer').style.display = 'block';
document.getElementById('statusText').innerText = 'Starting analysis...';
document.getElementById('resultContainer').style.display = 'none';
const textToAnalyze = document.getElementById('inputText').value;
// 2. Call the server-side Apps Script function
google.script.run
.withSuccessHandler(onTaskStarted)
.withFailureHandler(onFailure)
.startProcessingTask({ text: textToAnalyze });
}
function onTaskStarted(response) {
// 3. The server returned a task ID. Start polling for results.
console.log("Task started with ID: " + response.task_id);
document.getElementById('statusText').innerText = 'Processing request... Please wait.';
pollForResults(response.task_id);
}
function pollForResults(taskId) {
// This function will be defined in Step 4
}
function displayResults(result) {
// 5. Display the final results from the sidecar
document.getElementById('statusContainer').style.display = 'none';
document.getElementById('resultContainer').style.display = 'block';
document.getElementById('resultText').innerText = JSON.stringify(result, null, 2);
document.getElementById('analyzeButton').disabled = false; // Re-enable button
}
function onFailure(error) {
console.error("An error occurred: " + error.message);
document.getElementById('statusText').innerText = 'Error: ' + error.message;
document.getElementById('statusContainer').style.background = '#ffdddd'; // Indicate error
document.getElementById('analyzeButton').disabled = false; // Re-enable button
}
</script>
</body>
</html>
Now we connect the UI to our Cloud Run service. This is handled by a server-side Apps Script function (.gs file) that the client-side JavaScript calls via google.script.run. The key to security here is using ScriptApp.getIdentityToken() to make an authenticated call to our private Cloud Run service.
Code.gs
// Replace with the URL from your Cloud Run deployment
const CLOUD_RUN_URL = "https://workspace-sidecar-service-xxxxxxxxxx-uc.a.run.app";
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('AI Tools')
.addItem('Analyze Text', 'showSidebar')
.addToUi();
}
function showSidebar() {
const html = HtmlService.createHtmlOutputFromFile('Sidebar')
.setTitle('AI Text Analyzer');
SpreadsheetApp.getUi().showSidebar(html);
}
/**
* Called from the client-side JS to initiate the long-running task.
* @param {object} payload The data to be processed.
* @return {object} The response from the Cloud Run service, containing the task_id.
*/
function startProcessingTask(payload) {
try {
// 1. Get an OIDC token that identifies this Apps Script project.
const identityToken = ScriptApp.getIdentityToken();
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ data: payload }),
headers: {
// 2. Add the token to the Authorization header.
// Cloud Run will automatically validate this token.
'Authorization': 'Bearer ' + identityToken
},
muteHttpExceptions: true // Prevents throwing an exception on non-2xx responses
};
// 3. Make the authenticated call to the Cloud Run /process endpoint.
const response = UrlFetchApp.fetch(`${CLOUD_RUN_URL}/process`, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode === 202) {
return JSON.parse(responseBody); // Should contain { "task_id": "..." }
} else {
console.error(`Error from sidecar: ${responseCode} - ${responseBody}`);
throw new Error(`Backend service returned status ${responseCode}.`);
}
} catch (e) {
console.error('Failed to trigger processing task: ' + e.toString());
throw new Error('Could not connect to the processing service. Please try again.');
}
}
With the task running on Cloud Run, our final step is to periodically ask the service, “Are you done yet?“. This is done by implementing the polling logic on the client-side, which repeatedly calls another server-side Apps Script function to check the task status.
1. Add the Polling Logic to Sidebar.html
Complete the JavaScript in your HTML file by adding the pollForResults function. We’ll use setInterval to make the call every 3 seconds.
--- Inside the <script> tag of Sidebar.html -->
<script>
// ... (previous functions from Step 2) ...
function pollForResults(taskId) {
let attempts = 0;
const maxAttempts = 20; // Poll for a maximum of 60 seconds (20 * 3s)
// Clear any previous polling intervals
if (pollIntervalId) {
clearInterval(pollIntervalId);
}
pollIntervalId = setInterval(() => {
console.log(`Polling attempt #${attempts + 1} for task ${taskId}`);
// Call the server-side status checker
google.script.run
.withSuccessHandler(onStatusUpdate)
.withFailureHandler(onFailure)
.getTaskStatus(taskId);
attempts++;
if (attempts >= maxAttempts) {
clearInterval(pollIntervalId);
onFailure({ message: "Processing timed out. Please try again." });
}
}, 3000); // Poll every 3 seconds
}
function onStatusUpdate(response) {
console.log("Received status update:", response);
if (response.status === 'COMPLETED') {
clearInterval(pollIntervalId); // Stop polling
displayResults(response.result);
} else if (response.status === 'FAILED') {
clearInterval(pollIntervalId); // Stop polling
onFailure({ message: "Task failed during processing." });
}
// If status is 'PENDING' or 'PROCESSING', do nothing and let the interval continue.
}
</script>
2. Add the Status-Checking Function to Code.gs
Finally, create the corresponding server-side function in Apps Script that calls the /status endpoint on our Cloud Run service.
// Inside Code.gs
/**
* Called from the client-side JS to poll for the task status.
* @param {string} taskId The ID of the task to check.
* @return {object} The status and result (if available) from the Cloud Run service.
*/
function getTaskStatus(taskId) {
try {
const identityToken = ScriptApp.getIdentityToken();
const options = {
method: 'get',
headers: {
'Authorization': 'Bearer ' + identityToken
},
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(`${CLOUD_RUN_URL}/status/${taskId}`, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode === 200) {
return JSON.parse(responseBody);
} else {
console.error(`Error checking status: ${responseCode} - ${responseBody}`);
// Stop polling on the client by throwing an error
throw new Error(`Could not retrieve task status (Code: ${responseCode}).`);
}
} catch (e) {
console.error('Failed to get task status: ' + e.toString());
throw new Error('Connection to the processing service was lost.');
}
}
With these four steps complete, you have a fully functional, non-blocking UI in your Automated Order Processing Wordpress to Gmail to Google Sheets to Jobber Add-on that successfully offloads intensive work to a secure, scalable backend service.
Let’s dissect the three core components of this pattern: the Apps Script function that triggers the process, the Cloud Run endpoint that performs the heavy lifting, and the client-side JavaScript that manages the user experience.
This is the bridge between your Automated Payment Transaction Ledger with Google Sheets and PayPal add-on and your Cloud Run service. Instead of making a slow, direct call to the Gemini API from Apps Script (which is prone to timeouts), we make a fast, authenticated HTTP request to our own service.
The magic here lies in UrlFetchApp. We’re not waiting for the entire AI generation process within the Apps Script execution context. We’re simply firing a request to a highly optimized, dedicated service and waiting for its HTTP response.
Here’s what the server-side Apps Script function (Code.gs) looks like:
// Code.gs
/**
* Calls the Cloud Run service to generate content using Gemini.
* This function is called from the client-side JavaScript.
*
* @param {string} prompt The user's prompt for the AI.
* @return {string} The generated text from the Gemini API.
*/
function generateContentWithSidecar(prompt) {
// 1. Get the URL of your deployed Cloud Run service.
// It's best practice to store this in Script Properties.
const cloudRunUrl = PropertiesService.getScriptProperties().getProperty('CLOUD_RUN_URL');
if (!cloudRunUrl) {
throw new Error('Cloud Run URL is not configured in Script Properties.');
}
// 2. Get an identity token for the service account running the script.
// This token authenticates the request to your Cloud Run service.
// Your Cloud Run service must be configured to allow invocations
// from this specific service account.
const identityToken = ScriptApp.getIdentityToken();
// 3. Prepare the request payload and options.
const payload = JSON.stringify({
prompt: prompt
});
const options = {
method: 'post',
contentType: 'application/json',
headers: {
// The token is passed in the Authorization header.
'Authorization': 'Bearer ' + identityToken
},
payload: payload,
// This is crucial for debugging. It prevents Apps Script from
// throwing a generic exception on non-2xx responses, allowing you
// to parse the actual error message from your service.
muteHttpExceptions: true
};
// 4. Make the call.
const response = UrlFetchApp.fetch(cloudRunUrl, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
// 5. Handle the response.
if (responseCode === 200) {
const data = JSON.parse(responseBody);
return data.generated_text; // Return the successful result.
} else {
// Log the error for debugging and throw a user-friendly message.
console.error(`Error from Cloud Run: ${responseCode} - ${responseBody}`);
throw new Error(`The AI service failed to generate a response. Please try again. (Status: ${responseCode})`);
}
}
Key Takeaways:
Authentication: ScriptApp.getIdentityToken() is the cornerstone of security. It generates a short-lived OIDC token that proves the request is coming from your specific Apps Script project. Your Cloud Run service must be configured with IAM to accept requests authenticated with this token.
Payload: We send the user’s prompt in a simple JSON object.
Error Handling: Setting muteHttpExceptions to true gives you full control. You can inspect the HTTP status code and the response body to provide much better error feedback to the user and for your own logs.
Now for the workhorse: the Cloud Run service. This is a standard web application that exposes an HTTP endpoint. We’ll use Python with the Flask framework for this example, as it’s lightweight and perfect for this kind of task.
This service receives the request from Apps Script, calls the Gemini API, and returns the result. Because it’s running in a fully-featured server environment, it has access to more powerful libraries, better networking, and longer execution times than the Apps Script sandbox.
Here’s a sample main.py for your Cloud Run service:
# main.py
import os
import google.generativeai as genai
from flask import Flask, request, jsonify
# Initialize Flask app
app = Flask(__name__)
# Configure the Gemini API client
# In Cloud Run, set the GEMINI_API_KEY as an environment variable.
# The library will automatically pick it up.
try:
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
model = genai.GenerativeModel('gemini-pro')
except KeyError:
# This helps in debugging if the env var is not set.
raise RuntimeError("GEMINI_API_KEY environment variable not set.")
@app.route("/", methods=["POST"])
def generate():
"""
Endpoint to receive a prompt and return a Gemini-generated response.
This is the endpoint our Apps Script will call.
"""
# 1. Verify the request is JSON
if not request.is_json:
return jsonify({"error": "Request must be JSON"}), 400
# 2. Extract the prompt from the request body
data = request.get_json()
prompt = data.get("prompt")
if not prompt:
return jsonify({"error": "Missing 'prompt' in request body"}), 400
try:
# 3. Call the Gemini API
# This is the potentially long-running operation.
response = model.generate_content(prompt)
# 4. Extract the text and return it in a JSON response
generated_text = response.text
return jsonify({"generated_text": generated_text}), 200
except Exception as e:
# 5. Handle potential errors from the Gemini API
print(f"An error occurred: {e}")
return jsonify({"error": "Failed to generate content from Gemini API"}), 500
if __name__ == "__main__":
# This is used for local development.
# Gunicorn is recommended for production in Cloud Run.
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
Key Takeaways:
Standard Web Framework: This is just a simple Flask application. You could achieve the same with Express on Node.js, FastAPI, or any other web framework.
Environment Variables: Storing secrets like your GEMINI_API_KEY in environment variables is a security best practice. Cloud Run makes this easy to configure.
Clear I/O Contract: The endpoint expects a JSON object with a prompt key and returns a JSON object with a generated_text key. This simple, clear contract makes integration straightforward.
Finally, let’s look at the user-facing code. This HTML and JavaScript runs in the add-on’s sidebar or dialog in the user’s browser. Its job is to capture user input, call our server-side Apps Script function, and update the UI with the result, all without freezing the interface.
This is achieved using google.script.run, which is the asynchronous bridge between your client-side code and your server-side Code.gs functions.
Here’s a sample Sidebar.html:
--- Sidebar.html -->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
--- Add your CSS for styling here -->
<style>
.loader {
border: 4px solid #f3f3f3;
border-radius: 50%;
border-top: 4px solid #3498db;
width: 20px;
height: 20px;
animation: spin 2s linear infinite;
display: none; / *Hidden by default* /
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<h3>AI Content Generator</h3>
<p>Enter a prompt below:</p>
<textarea id="prompt-input" rows="4" style="width: 95%;"></textarea>
<br /><br />
<button id="generate-btn">Generate</button>
<div id="loader" class="loader"></div>
<h4>Result:</h4>
<div id="output-display" style="white-space: pre-wrap; background-color: #f0f0f0; padding: 10px; border-radius: 4px;">
--- AI output will appear here -->
</div>
<script>
document.getElementById('generate-btn').addEventListener('click', onGenerateClick);
function onGenerateClick() {
const prompt = document.getElementById('prompt-input').value;
if (!prompt) {
alert('Please enter a prompt.');
return;
}
// 1. Update UI to show loading state
document.getElementById('generate-btn').disabled = true;
document.getElementById('loader').style.display = 'block';
document.getElementById('output-display').innerText = 'Generating, please wait...';
// 2. Call the server-side Apps Script function
google.script.run
.withSuccessHandler(onGenerationSuccess)
.withFailureHandler(onGenerationFailure)
.generateContentWithSidecar(prompt);
}
function onGenerationSuccess(generatedText) {
// 3. Update UI with the successful result
document.getElementById('output-display').innerText = generatedText;
resetUi();
}
function onGenerationFailure(error) {
// 4. Update UI with the error message
document.getElementById('output-display').innerText = 'Error: ' + error.message;
resetUi();
}
function resetUi() {
// Helper function to re-enable the button and hide the loader
document.getElementById('generate-btn').disabled = false;
document.getElementById('loader').style.display = 'none';
}
</script>
</body>
</html>
Key Takeaways:
Asynchronous Execution: google.script.run does not block the UI. The user can still interact with the Google Doc/Sheet while the request is in flight.
State Management: The most important part of this code is managing the UI state. We disable the button and show a loader to give the user clear feedback that something is happening.
Success and Failure Handlers: Using .withSuccessHandler() and .withFailureHandler() is the correct way to process the asynchronous response. This allows you to cleanly separate the logic for handling a successful result versus an error, leading to a much more robust and user-friendly add-on.
Adopting the Cloud Run sidecar pattern is more than just a clever workaround for UI timeouts; it’s a strategic architectural shift. While solving the immediate problem is a huge win, the long-term benefits related to scalability, maintainability, security, and cost-efficiency are where this approach truly shines. Let’s break down these critical aspects.
The primary motivation might be to bypass the 30-second execution limit, but you’re also fundamentally decoupling your application logic from the UI host environment. This separation unlocks significant advantages.
Unlocking True Scalability:
Genesis Engine AI Powered Content to Video Production Pipeline runs in a shared, multi-tenant environment with strict quotas and concurrency limits. It’s designed for lightweight scripting and integration, not for serving as a heavy-duty computational backend. By offloading the intensive work to Cloud Run, you tap into a platform built for hyperscale.
Independent Scaling: Your Cloud Run service scales based on incoming requests, completely independent of the Apps Script environment. It can scale from zero instances (costing you nothing) up to thousands of container instances to handle massive, concurrent request spikes from your users, and then scale back down just as quickly.
Resource Allocation: You have granular control over the CPU and memory allocated to your service. If your AI task needs 4 vCPUs and 8GB of RAM, you can provision that—something utterly impossible within the confines of Apps Script.
A Revolution in Code Maintainability:
This pattern enforces a clean separation of concerns, which is a cornerstone of modern software development.
The Right Tool for the Job: You are no longer constrained to Apps Script’s JavaScript-based environment. You can write your backend service in the language best suited for the task. Need to leverage powerful machine learning libraries? Use Python with TensorFlow, PyTorch, or LangChain. Need raw, concurrent performance? Use Go. Prefer a vast package ecosystem? Use Node.js. This polyglot approach allows your team to use their existing skills and the best available tools.
Robust Development Practices: A Cloud Run service is a standard microservice. This means you can integrate it into a mature CI/CD pipeline, implement comprehensive unit and integration testing frameworks, and apply established DevOps practices. Managing, testing, and deploying your complex business logic becomes exponentially simpler and more reliable compared to working with a monolithic Apps Script project. Your Apps Script code slims down to its core purpose: managing the user interface and orchestrating API calls.
Once you expose a Cloud Run service, you’ve created a public-facing endpoint. Leaving it open is not an option. You must ensure that only your Google Docs to Web Add-on can invoke it. Fortunately, Google Cloud provides a best-in-class, secure, and straightforward way to handle this using IAM and identity tokens.
The goal is to establish a verifiable trust relationship: Cloud Run must be able to cryptographically verify that a request genuinely originated from your specific Apps Script project.
The IAM and OIDC Token Flow:
Instead of fumbling with fragile API keys stored as script properties, you leverage Google’s built-in identity infrastructure.
Configure Cloud Run for Authentication: Set your Cloud Run service’s ingress settings to “Allow internal traffic and traffic from Cloud Load Balancing” and ensure that “Authentication” is set to “Require authentication.” This locks down the endpoint to only accept requests with valid Google-signed identity tokens.
Grant Invoker Permissions: Create a dedicated IAM Service Account for your backend logic. Grant this service account the Cloud Run Invoker (roles/run.invoker) role on your Cloud Run service. This IAM binding is the policy that says, “Identities with this role are permitted to call this service.”
**Associate the Service Account: Link this Service Account to your Apps Script project in its Google Cloud Platform settings. This gives the script the identity of the service account.
Generate a Token in Apps Script: Within your Apps Script code, before making the call to Cloud Run, you generate an OIDC identity token by calling ScriptApp.getIdentityToken(). This token is a short-lived JSON Web Token (JWT) that contains claims about the script’s identity and is signed by Google’s authority.
Make the Authenticated Request: You include this token in the Authorization header of your UrlFetchApp call.
// In your Apps Script code
const token = ScriptApp.getIdentityToken();
const options = {
'method': 'post',
'headers': {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
'payload': JSON.stringify({ prompt: 'Summarize this document...' })
};
const response = UrlFetchApp.fetch('YOUR_CLOUD_RUN_URL', options);
Google Cloud’s infrastructure automatically validates the incoming token’s signature and checks if the identity within it has the required run.invoker permission. This method is vastly superior to static keys because the tokens are automatically generated, short-lived, and tied to a specific IAM identity, virtually eliminating the risk of leaked credentials.
While this architecture is powerful, it’s not free. However, Cloud Run’s pricing model is exceptionally well-suited for the often-bursty traffic patterns of UI-driven applications, making it highly cost-effective.
Understanding the Cost Model:
Cloud Run operates on a pure pay-per-use model. You are billed only for the vCPU and memory your container consumes while it is actively processing a request, metered to the nearest 100 milliseconds.
Scale to Zero is Key: The most powerful cost-saving feature is its ability to scale to zero. If no users are interacting with your add-on’s advanced features, your Cloud Run service has zero running instances, and your cost is zero. This is a perfect match for internal tools or add-ons with sporadic usage.
Generous Free Tier: Google Cloud’s free tier for Cloud Run is substantial, often covering the entire monthly cost for add-ons with low-to-moderate traffic.
Performance Optimization and the “Cold Start”:
The primary performance consideration with any scale-to-zero platform is the “cold start.” This is the latency incurred on the first request after a period of inactivity, as Cloud Run needs to provision a new container instance, download the image, and start your application. This can add a few seconds to the response time for that initial user.
For most add-on use cases, a slight delay on the very first invocation is an acceptable trade-off for the immense cost savings. However, if your application is highly latency-sensitive, you can mitigate this:
Configure Minimum Instances: You can set min-instances to 1 for your Cloud Run service. This forces one container to always be running and “warm,” ready to serve requests instantly. This eliminates cold starts entirely but incurs a cost for keeping that instance idle 24/7. This presents a classic cost-versus-performance decision that you can tailor to your specific needs.
Optimize Your Container: You can significantly reduce cold start times by using smaller base container images (e.g., alpine or distroless), optimizing your application’s startup code, and choosing compiled languages like Go that have a faster boot time.
We began with a common, frustrating problem: a powerful workspace application held hostage by its own intelligence. Long-running AI tasks were creating UI timeouts, degrading the user experience and undermining the very value the application was meant to provide. By implementing the Cloud Run sidecar pattern, we’ve transformed this architectural bottleneck into a highly scalable, resilient, and user-friendly solution.
The core lesson is the profound impact of decoupling the user-facing request-response cycle from intensive backend processing. The traditional monolithic approach forces the user to wait, creating a direct link between processing time and perceived performance.
The sidecar pattern shatters this link. By offloading the heavy AI computation to a dedicated sidecar container, our primary application is liberated. It can instantly acknowledge the user’s request, update the UI to a “processing” state, and move on. This asynchronous, non-blocking architecture is the key to eliminating timeouts. The result isn’t just a technical improvement; it’s a fundamental shift in the user experience, from one of frustration and uncertainty to one of speed and reliability.
This solution is more than just a clever trick; it’s an embodiment of modern cloud-native design principles. The sidecar pattern is a practical application of the Single Responsibility Principle at the container level. Your primary container is an expert at handling web requests and managing user state. Your sidecar is an expert at running a specific AI model.
This modularity brings a cascade of benefits:
Independent Development: Teams can update, patch, or even completely replace the AI model in the sidecar without ever touching the primary application’s codebase.
Targeted Resource Allocation: You can allocate more CPU or memory specifically to the sidecar container that needs it, without over-provisioning the web-facing container.
Polyglot Architecture: Use the best tool for the job. Your web backend might be written in Go or Node.js for its concurrency, while your AI sidecar is in Python, leveraging its rich ecosystem of data science libraries.
Cloud Run makes this sophisticated pattern remarkably accessible, managing the deployment, networking, and co-location of the containers for you. It allows you to focus on the logic, not the operational overhead.
You’ve now seen how to solve a specific problem with a powerful pattern. The journey doesn’t end here. We encourage you to look at your own systems through this new lens.
Identify Your Bottlenecks: What other long-running tasks exist in your applications? Think beyond AI: video transcoding, PDF generation, complex data analysis, or batch report processing are all prime candidates for the sidecar pattern.
Experiment and Adapt: The principles of asynchronous offloading are universal. Start with a small, non-critical task and build a proof-of-concept. How can you adapt this pattern to your specific stack and requirements?
Explore the Ecosystem: For even more complex workflows, consider how this pattern integrates with other services. Could you have the sidecar publish a completion message to a Pub/Sub topic? Could the initial request be placed on a Cloud Tasks queue for deferred execution?
By embracing patterns like the Cloud Run sidecar, you move from simply writing code to architecting systems that are resilient, scalable, and built to delight your users. The tools are at your fingertips; it’s time to build better.
Quick Links
Legal Stuff
