HomeAbout MeBook a Call

Building Stateful AI Agents with Firestore and Google Apps Script

By Vo Tu Duc
May 05, 2026
Building Stateful AI Agents with Firestore and Google Apps Script

The simple, “fire-and-forget” automations we rely on are powerful, but the very statelessness that makes them simple is also their greatest limitation.

image 0

The Challenge of Statelessness in Enterprise [Automated Job Creation in Real Time Jobber and Google Sheets Integration from Gmail](https://votuduc.com/Automated-Job-Creation-in-Jobber-from-Gmail-p115606)

In the world of enterprise Automated Quote Generation and Delivery System for Jobber, we’ve become incredibly adept at building trigger-based, fire-and-forget workflows. A new row appears in a Google Sheet, and a calendar event is created. An email with a specific subject line arrives, and a task is added to a project board. These are the foundational blocks of modern productivity—powerful, efficient, and utterly stateless. But this very statelessness, the source of their simplicity, is also their greatest limitation. When tasks become complex, conversational, and unfold over time, this model begins to fracture.

Why Traditional Automations Fall Short for Complex Tasks

Think of a standard Automated Work Order Processing for UPS script, like one written in AI Powered Cover Letter Automation Engine, as having severe short-term memory loss. It executes a task based on a single, immediate trigger and then completely forgets it ever happened. It has no memory of past interactions, no context for the current request, and no awareness of a larger, ongoing process.

image 1

This “fire-and-forget” paradigm is perfectly fine for discrete, atomic tasks, but it fails spectacularly when faced with real-world business processes that are inherently stateful:

  • Multi-Step Onboarding: A new employee onboarding process isn’t a single event. It’s a sequence of actions—send a welcome email, provision accounts, schedule orientation, check in after one week—that can take days to complete. A stateless automation can’t track progress, know that Step 1 is done, and intelligently decide to initiate Step 2.

  • Interactive Support Tickets: Imagine a support bot that needs to ask clarifying questions. “Have you tried restarting the device?” A stateless bot can’t wait for the user’s response, which might come hours later, and then proceed based on that new information. It has no memory of the original question it asked.

  • Long-Running Approval Chains: A document needs approval from three different managers in sequence. The automation needs to notify the first manager, wait for their approval, then notify the second, wait again, and so on. It must maintain the state of the document (pending_manager_A, pending_manager_B, etc.) throughout the entire lifecycle.

Traditional automations can’t “pause” and “resume.” They can’t remember conversations. They can’t adapt their behavior based on the cumulative history of a workflow. To overcome this, we don’t just need a better script; we need a fundamentally different approach.

Introducing the Stateful Agent: A Paradigm Shift for Workflows

A stateful agent is the antithesis of a fire-and-forget script. It is an autonomous system designed with a persistent memory, allowing it to manage complex, long-running tasks with intelligence and context.

Unlike its stateless counterpart, a stateful agent:

  1. Maintains State: At its core, the agent has access to a persistent data store where it records everything relevant to its task—conversation history, user details, progress through a workflow, collected data, and internal “thoughts.” This is its memory.

  2. Operates Over Time: It is not bound by a single execution. It can initiate a task, pause indefinitely while waiting for an external trigger (like a human response or an API callback), and then resume exactly where it left off, with full memory of the prior context.

  3. Reasons and Decides: By combining its persistent state with the intelligence of a Large Language Model (LLM), the agent can do more than just follow a rigid set of if/then rules. It can analyze the history of an interaction, understand user intent, and make reasoned decisions about the next best action to take.

This isn’t just an incremental improvement. It’s a paradigm shift from simple “automation” to true “orchestration.” We’re moving from writing scripts that react to single events to building agents that manage entire processes.

Architectural Blueprint: Gemini, Firestore, and Apps Script

To bring this concept to life within the Google ecosystem, we’ll orchestrate three powerful services, each playing a distinct and crucial role in our agent’s architecture.

  • 🧠 Gemini: The Reasoning Core

Gemini is the brain of our operation. It provides the cognitive capabilities for the agent. When the agent needs to make a decision, it will pass its current state (the conversation history, workflow status, etc.) along with the new input to the Gemini API. Gemini’s role is not to do the work, but to decide what work needs to be done. It will analyze the context and return a structured plan or the next specific action for our executor to perform.

  • 💾 Firestore: The Persistent Memory

If Gemini is the brain, Firestore is the agent’s long-term memory. It’s a highly scalable, real-time NoSQL database where we will store the “state” for every ongoing task or conversation. Each process managed by our agent will have its own document in Firestore, containing everything from chat logs to progress flags. When our agent “wakes up,” its first action is to read its memory from Firestore. Its last action is to write its updated state back. This ensures continuity and resilience.

Apps Script provides the hands and feet for our agent. It is the powerful, serverless execution environment that connects our agent’s brain (Gemini) and memory (Firestore) to the outside world. Apps Script will be responsible for:

  • Handling triggers (e.g., a new email via the Gmail API, a web-hook).

  • Reading from and writing to the Firestore state database.

  • Calling the Gemini API for decision-making.

  • Executing the actions Gemini decides upon, such as sending emails, updating [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), creating Calendar events, or calling other third-party APIs.

Together, this trio forms a robust and scalable architecture. A trigger activates an Apps Script function, which loads the state from Firestore, asks Gemini what to do next, executes the prescribed action, and saves the new state back to Firestore, ready for the next interaction.

Core Concept: The State Store Pattern Explained

To build agents that feel intelligent and can handle more than just trivial, one-shot tasks, we need to give them a memory. In software architecture, this “memory” is achieved through a concept called a state store. This is the absolute bedrock of our agent design, transforming our simple scripts into robust, multi-step assistants.

What is a State Store and Why Does it Matter for Architecting AI Agents for the Google Workspace Marketplace?

At its core, a script execution is a fleeting, ephemeral event. An Apps Script function triggered by a new email runs, performs its logic, and then vanishes, taking all its variables and in-flight information with it. This is known as being stateless. It has no memory of past executions and no awareness of a future. For many simple automations, this is perfectly fine.

An AI agent, however, cannot be stateless. Its very purpose is to maintain context, track progress, and execute tasks that unfold over time. This is where state comes in. State is the snapshot of all relevant information about a task at a specific moment. It’s the agent’s working memory.

Consider an agent designed to onboard a new client:

  1. Send a welcome email.

  2. Wait for the client to fill out a Google Form.

  3. Once the form is submitted, create a client folder in Google Drive.

  4. Draft a project proposal in Google Docs using AI.

  5. Notify the project manager in Google Chat for review.

This process can take days. A stateless script would be impossible to build. The agent needs to remember: “I’ve sent the welcome email to client XYZ, and now I am waiting for their form submission.” This information is its state.

A State Store is an external, persistent service—a database—where we save this state.

Instead of holding the agent’s status in a temporary variable, we write it to the state store at the end of each execution. When the agent runs again, its first action is to read from the state store to recall what it was doing.

Why this is a game-changer for AI agents:

  • Long-Running Processes: It allows agents to perform tasks that exceed technical limitations, like script execution timeouts. The agent can “pause” by saving its state and “resume” on the next run.

  • Contextual Awareness: For conversational agents, the state store holds the chat history, enabling the agent to understand follow-up questions and refer to previous points.

  • Resilience and Fault Tolerance: If a script fails midway through processing a batch of data, it doesn’t have to start from scratch. On the next run, it can read its last known state and pick up exactly where it left off, saving time and preventing duplicate actions.

Think of a stateless script as a chef with severe short-term memory loss who can only follow one instruction at a time from a recipe. A stateful agent with a state store is a master chef with a detailed notebook, tracking the progress of a complex, multi-day banquet preparation.

Why Firestore is the Ideal State Store for Workspace Automations

While you could theoretically use a Google Sheet or Script Properties as a rudimentary state store, these options quickly become brittle and unmanageable. For a robust solution, we turn to a proper database. Google’s Firestore (in Datastore mode) emerges as the perfect choice for several key reasons:

  1. Seamless Google Ecosystem Integration: Firestore is a first-party Google Cloud product. This means authentication from Apps Script is incredibly straightforward using the built-in ScriptApp.getOAuthToken() method. There are no complex API keys to manage or third-party credential flows to wrestle with. It’s designed to work within the Google universe.

  2. Flexible, Schema-less Data Model: Agent state is often complex and can change as you add new capabilities. Firestore is a NoSQL document database, meaning it stores data in a format similar to JSON. You can store nested objects, arrays, and varied data types without defining a rigid table structure upfront. Need to add a conversationHistory array to your state? Just add it. This flexibility is invaluable for iterative development.

  3. Built for Scale and Performance: While your initial agent might not handle massive traffic, Firestore is built on Google’s global infrastructure. It offers low latency and automatic scaling. This ensures that as your automations become more critical or numerous, your state management backbone won’t crumble under the load.

  4. Generous Free Tier: The Firestore free tier (Spark Plan) is exceptionally generous, offering ample storage, reads, and writes to run dozens of sophisticated automation agents without ever paying a dime. This makes it accessible for everyone, from hobbyists to enterprise teams prototyping new workflows.

  5. Direct and Easy Access: Using the Firestore REST API with Apps Script’s UrlFetchApp service is simple and effective. A wrapper library can make these interactions even cleaner, allowing you to get, set, and update your agent’s state with single lines of code.

Systematically Overcoming Apps Script Execution Timeouts

The single greatest barrier to complex automation in [Architecting Multi Tenant AI Workflows in Building Modular Agentic Apps Script with Gemini Function Calling](https://votuduc.com/architecting-multi-tenant-ai-workflows-in-google-apps-script-p-20260321290501) is the hard execution time limit: 6 minutes for standard Gmail accounts and 30 minutes for Automatically create new folders in Google Drive, generate templates in new folders, fill out text automatically in new files, and save info in Google Sheets accounts. Any process that takes longer is unceremoniously terminated. This makes processing hundreds of files, migrating large datasets, or running complex AI generation chains seem impossible.

This is the problem the State Store pattern was born to solve. By combining Firestore with Apps Script’s programmatic triggers, we can chain together short executions to create a single, unstoppable, long-running task.

Here’s the flow:

  1. Initiate and Save State: A trigger (e.g., a user clicking a menu item) starts the first execution. The very first thing the script does is create a “job” document in Firestore. This document contains the initial state, such as status: 'RUNNING', processedItems: 0, and a list of all items to be processed.

  2. Process a Small Batch: The script does a small, manageable chunk of work that is guaranteed to finish in under 6 minutes. For example, it processes the first 10 emails out of 500.

  3. Update State: After completing the batch, it updates the state document in Firestore. For instance, it sets processedItems: 10. This acts as a bookmark, saving its progress.

  4. Check for Completion & Re-trigger:

  • The script checks if the work is done (processedItems >= totalItems).

If* not done**, it programmatically creates a new time-based trigger to run itself again in one minute.

  • The script then finishes its execution, exiting gracefully long before the timeout limit.
  1. Resume the Work: One minute later, the new trigger fires, starting a fresh 6-minute execution. The script’s first step is to read its state from Firestore. It sees processedItems: 10 and knows to start its work on item #11.

This cycle of Read State → Do Work → Update State → Re-trigger continues, chaining executions together.


┌──────────────────────────┐     ┌──────────────────────────┐     ┌──────────────────────────┐

│       Execution 1        │     │       Execution 2        │     │       Execution N        │

│  (Triggered by User)     │     │ (Triggered by Script)    │     │ (Triggered by Script)    │

├──────────────────────────┤     ├──────────────────────────┤     ├──────────────────────────┤

│ 1. Create State          │     │ 1. Read State            │     │ 1. Read State            │

│    (index: 0)            │     │    (index: 10)           │     │    (index: N*10)         │

│ 2. Process items 0-9     │     │ 2. Process items 10-19   │     │ 2. ...                   │

│ 3. Update State          │     │ 3. Update State          │     │ 3. Update State          │

│    (index: 10)           │     │    (index: 20)           │     │    (status: 'COMPLETE')  │

│ 4. Create next trigger   │     │ 4. Create next trigger   │     │ 4. Delete trigger        │

└──────────────────────────┘     └──────────────────────────┘     └──────────────────────────┘

│                             │                             │

└──────────> Firestore <──────────┘                             │

(State)                                          │

▲                                             ▼

└─────────────────────────────────────────── FINISH

To the end-user, it appears as a single, continuous task. Behind the scenes, it’s a resilient, distributed process orchestrated by our Firestore state store. This pattern is the key that unlocks the full potential of building truly powerful, [Building Stateful AI Agents Building Long-Term Memory for Gemini with Firestore with Firestore for Gemini Long Term Memory](https://votuduc.com/building-stateful-ai-agents-with-firestore-for-gemini-long-term-memory-p-20260322693462) within the AC2F Streamline Your Google Drive Workflow ecosystem.

Implementation Guide: Architecting Your Stateful Agent

With the high-level architecture defined, it’s time to get our hands dirty. This section provides a step-by-step guide to building the core components of our stateful agent. We’ll design the data structures, implement the session management logic, and write the code that gracefully handles the infamous Apps Script execution time limit.

Step 1: Designing the Firestore Data Model for State Tracking

The foundation of any stateful system is its data model. The state must be stored externally, and for this, Firestore is an excellent choice due to its scalability, flexible data model, and seamless integration with Google Cloud services. Our goal is to create a single source of truth for each “conversation” or “task” our agent undertakes.

We’ll structure our data around a central collection, let’s call it agent_sessions. Each document in this collection will represent a single, unique session and contain all the information necessary for the agent to pause and resume its work at any point.

Here’s a breakdown of a well-structured session document:


// Firestore Collection: agent_sessions

// Document ID: <unique_session_id> (e.g., "sess_abc123xyz")

{

"sessionId": "sess_abc123xyz",

"createdAt": "2023-10-27T10:00:00Z",

"lastUpdatedAt": "2023-10-27T10:05:30Z",

"status": "IN_PROGRESS", // Other values: PENDING_RESUME, AWAITING_USER_INPUT, COMPLETED, FAILED

"currentState": "summarizing_document_2_of_10", // A human-readable string describing the current micro-task

"context": {

"userId": "[email protected]",

"driveFolderId": "1a2b3c4d5e6f...",

"filesToProcess": ["fileId1", "fileId2", "fileId3", ...],

"processedFiles": ["fileId1"],

"summaries": {

"fileId1": "This is the summary for the first document..."

}

},

"history": [

{

"role": "user",

"parts": [{ "text": "Please summarize all the documents in my 'Project Alpha' folder." }]

},

{

"role": "model",

"parts": [{ "text": "Of course. I've found 10 documents to process. I will start with the first one now." }]

}

],

"error": null // or { "message": "Failed to access fileId2", "timestamp": "..." }

}

Key Fields Explained:

  • sessionId: A unique identifier we generate to track this specific task.

  • status: The single most important field for our stop-and-start logic. It tells our script whether to start a new task, resume an old one, or ignore it.

  • currentState: Provides a more granular description of the agent’s progress. This is invaluable for both debugging and for providing context to the AI model upon resumption.

  • context: A flexible map (object) that acts as the agent’s short-term memory. It holds all the operational data—file IDs, intermediate results, user preferences, etc.—that doesn’t belong in the conversational history.

  • history: An array that mirrors the structure of the Gemini API’s contents field. By storing the conversation history, we can “rehydrate” the agent’s memory, allowing it to understand the full context of the request, even after a multi-minute pause.

Step 2: Implementing Session and Token Management in Apps Script

Google Apps Script runs in a stateless environment. Each execution is independent. To bridge this gap, we need to manage our sessionId meticulously. When a long-running process is initiated (e.g., by a user clicking a button in a Google Sheet), our script must perform two key actions:

  1. Create the Session: Generate a unique sessionId and create the initial session document in Firestore with a status of IN_PROGRESS.

  2. Persist the Token: Store this sessionId so that subsequent, automatically triggered executions know which session to resume.

The most reliable way to handle this persistence across executions is by using PropertiesService.


// A utility function to start a new session or retrieve an existing one

function getOrCreateSession(initialContext) {

const firestore = FirestoreApp.getFirestore(FIREBASE_EMAIL, FIREBASE_KEY, FIREBASE_PROJECT_ID);

const userProperties = PropertiesService.getUserProperties();

let sessionId = userProperties.getProperty('currentSessionId');

if (sessionId) {

// Attempt to retrieve the existing session

const sessionDoc = firestore.getDocument(`agent_sessions/${sessionId}`);

if (sessionDoc && sessionDoc.fields) {

console.log(`Resuming existing session: ${sessionId}`);

return sessionDoc.fields;

}

}

// If no valid session ID exists, create a new one

console.log('Creating a new session...');

sessionId = 'sess_' + Utilities.getUuid();

const newSession = {

sessionId: { stringValue: sessionId },

createdAt: { timestampValue: new Date().toISOString() },

lastUpdatedAt: { timestampValue: new Date().toISOString() },

status: { stringValue: 'IN_PROGRESS' },

currentState: { stringValue: 'session_started' },

context: { mapValue: { fields: initialContext } }, // initialContext should be Firestore-formatted

history: { arrayValue: { values: [] } }

};

firestore.createDocument(`agent_sessions/${sessionId}`, newSession);

userProperties.setProperty('currentSessionId', sessionId);

return newSession;

}

In this pattern, PropertiesService acts as a simple key-value store tied to the user or script, holding the “token” (sessionId) that points to our complex state object in Firestore. When a function finishes or is about to time out, we clear this property.

Step 3: Integrating the Gemini Agent for Intelligent Task Resumption

The magic happens when we combine our stored state with the Gemini API. A naive implementation might just restart a task, but a stateful agent uses its history to make an intelligent next move.

When our script resumes execution, the process is:

  1. Fetch State: Retrieve the full session document from Firestore using the sessionId.

  2. Rehydrate Context: Load the history array from the document. This array is now ready to be used as the contents parameter in our next call to the Gemini API.

  3. Craft a Resumption Prompt: Construct a new prompt that explicitly tells the model it is resuming a task. This is crucial for preventing confusion and repeated work.

Here’s how you might build the API call payload:


function callGeminiWithContext(session) {

// 1. Rehydrate the conversation history from Firestore

const history = session.history.arrayValue.values.map(entry => {

// (Add logic here to convert Firestore format to Gemini API format)

return {

role: entry.mapValue.fields.role.stringValue,

parts: [{ text: entry.mapValue.fields.parts.arrayValue.values[0].mapValue.fields.text.stringValue }]

};

});

// 2. Craft a prompt that acknowledges the current state

const resumptionPrompt = `

RESUMPTION CONTEXT:

You are resuming a task.

The current status is: "${session.currentState.stringValue}"

The last thing you said was: "${history[history.length - 1].parts[0].text}"

Based on the full conversation history provided, determine and execute the next logical step.

`;

// 3. Combine history with the new prompt

const contents = [

...history,

{

role: 'user', // We frame the resumption command as a user instruction

parts: [{ text: resumptionPrompt }]

}

];

// 4. Make the API call

const geminiApiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=${API_KEY}`;

const options = {

method: 'post',

contentType: 'application/json',

payload: JSON.stringify({ contents: contents })

};

const response = UrlFetchApp.fetch(geminiApiUrl, options);

const result = JSON.parse(response.getContentText());

// Remember to update the session history with this new turn!

// ...

return result;

}

By providing both the full history and a specific resumption prompt, you give the model everything it needs to seamlessly continue the conversation or task without missing a beat.

Step 4: Code Deep Dive: Handling the ‘Stop-and-Start’ Logic

This is the most critical piece of the puzzle: the mechanism that preempts the Apps Script 6-minute execution limit. The strategy is to monitor elapsed time within our long-running loops and, when the limit approaches, gracefully stop, save state, and schedule a new execution to take over.

Let’s imagine a main function processDocuments() that iterates through a list of files.


const EXECUTION_TIME_LIMIT_MS = 300000; // 5 minutes to be safe

function processDocuments() {

const startTime = new Date().getTime();

const firestore = FirestoreApp.getFirestore(FIREBASE_EMAIL, FIREBASE_KEY, FIREBASE_PROJECT_ID);

const userProperties = PropertiesService.getUserProperties();

const sessionId = userProperties.getProperty('currentSessionId');

if (!sessionId) {

console.error("No active session ID found. Exiting.");

return;

}

// 1. LOAD STATE

let session = firestore.getDocument(`agent_sessions/${sessionId}`).fields;

let context = session.context.mapValue.fields; // Simplified for clarity

let filesToProcess = context.filesToProcess.arrayValue.values.map(v => v.stringValue);

let processedFiles = context.processedFiles ? context.processedFiles.arrayValue.values.map(v => v.stringValue) : [];

// Determine where to start the loop from

const startIndex = processedFiles.length;

// 2. THE MAIN PROCESSING LOOP

for (let i = startIndex; i < filesToProcess.length; i++) {

const fileId = filesToProcess[i];

// --- Check if time is almost up ---

const elapsedTime = new Date().getTime() - startTime;

if (elapsedTime > EXECUTION_TIME_LIMIT_MS) {

console.log("Execution time limit approaching. Saving state and scheduling continuation.");

// 3. SAVE AND STOP

// Update the session document in Firestore

firestore.updateDocument(`agent_sessions/${sessionId}`, {

'status': { stringValue: 'PENDING_RESUME' },

'currentState': { stringValue: `Paused before processing document ${i + 1} of ${filesToProcess.length}` },

'lastUpdatedAt': { timestampValue: new Date().toISOString() }

}, true); // The 'true' here means it's a mask update

// 4. SCHEDULE RESUMPTION

// Create a trigger to run this same function again in ~1 minute

ScriptApp.newTrigger('processDocuments')

.timeBased()

.after(60 * 1000)

.create();

return; // Stop the current execution

}

// --- Do the actual work ---

console.log(`Processing file ${i + 1}: ${fileId}`);

// ... call Gemini, process text, etc. ...

// Update state in memory after each successful step

processedFiles.push(fileId);

// Periodically update Firestore to avoid losing too much work on an unexpected error

firestore.updateDocument(`agent_sessions/${sessionId}`, {

'context.processedFiles': { arrayValue: { values: processedFiles.map(id => ({ stringValue: id })) } },

'currentState': { stringValue: `Finished processing document ${i + 1} of ${filesToProcess.length}` }

}, true);

}

// 5. CLEANUP

console.log("All documents processed successfully.");

firestore.updateDocument(`agent_sessions/${sessionId}`, {

'status': { stringValue: 'COMPLETED' },

'lastUpdatedAt': { timestampValue: new Date().toISOString() }

});

// Clear the session ID from properties to signify completion

userProperties.deleteProperty('currentSessionId');

}

How Resumption Works:

When the trigger fires after one minute, it calls processDocuments() again.

  • The function starts.

  • It reads the sessionId from userProperties (which we didn’t delete).

  • It loads the session from Firestore.

  • It sees that processedFiles already contains some items.

  • The startIndex is calculated based on the length of processedFiles, so the for loop automatically picks up exactly where the previous execution left off.

This elegant loop of “work -> check time -> save -> trigger -> resume” is the engine that allows your Apps Script agent to perform tasks that far exceed the standard execution limits.

Advanced Architectural Considerations and Use Cases

Building a stateful agent is one thing; engineering a robust, scalable system that can handle real-world complexity is another. The combination of Firestore’s persistent, queryable state and Google Apps Script’s event-driven execution model unlocks powerful patterns that go far beyond simple request-response cycles. Let’s explore how to architect solutions for long-running tasks, concurrent users, and inevitable failures.

Managing Multi-Day Asynchronous Tasks and Human-in-the-Loop Processes

Many valuable automated workflows aren’t instantaneous. They involve waiting for external events, scheduled reports, or—most unpredictably—human intervention. A standard script execution, limited to minutes, simply can’t handle these scenarios. The key is to leverage Firestore as the agent’s “external brain” and use Apps Script triggers as a pacemaker.

This is achieved through a “Wake-Up and Check” pattern:

  1. Entering a Wait State: When an agent needs to pause, it doesn’t just sleep. It persists its intention in Firestore. It updates its state document with a status like WAITING or PENDING_APPROVAL and includes a waitUntil timestamp or a specific condition to check for.

  2. The Pacemaker Trigger: A time-driven Google Apps Script trigger is configured to run at a regular interval (e.g., every 5 minutes, every hour). This trigger executes a single, stateless function.

  3. Querying for Actionable Agents: The triggered function’s sole purpose is to query the Firestore agents collection. It looks for documents that are in a waiting state and whose conditions for resuming have been met. For example:

  • query.where('state', '==', 'WAITING').where('waitUntil', '<=', new Date())

  • query.where('state', '==', 'PENDING_APPROVAL').where('approvalStatus', '!=', 'PENDING')

  1. Resuming Execution: For each agent returned by the query, the function “wakes it up” by invoking the next logical step in its workflow. This could involve calling another function, updating its state to PROCESSING, and executing the task it was waiting to perform.

This architecture elegantly supports Human-in-the-Loop (HITL) processes. Imagine an agent that drafts an email and requires manager approval before sending.

  • The agent generates the draft and saves it to its Firestore document.

  • It updates its state to PENDING_APPROVAL.

  • It uses MailApp to send an email to the manager containing a link to a simple Apps Script web app.

  • The manager clicks the link, views the draft, and clicks an “Approve” or “Reject” button.

  • This button click triggers a function in the web app that updates the agent’s document in Firestore, setting a field like approvalStatus to APPROVED.

  • On its next run, the pacemaker trigger discovers this state change, and the agent proceeds to send the email.

The agent’s process is completely decoupled from the human’s response time, allowing workflows to span days or weeks without ever hitting an execution timeout.

Scaling for Concurrency and Multi-User Scenarios

When your system moves from a single user to many, or when a single workflow can spawn multiple concurrent tasks, you will inevitably encounter race conditions and platform limits. A naive approach where a web app trigger directly executes a 30-second task will quickly fail under load.

Firestore Transactions are your first line of defense. When multiple processes might try to modify the same agent document simultaneously, a standard update operation can lead to inconsistent state. A transaction solves this by ensuring that the read-modify-write cycle is atomic.

Consider an agent that can only perform one task at a time. Before starting a new task, you must check if its current state is IDLE.


// Inside an Apps Script function

const firestore = FirestoreApp.getFirestore(...);

const agentRef = `agents/${agentId}`;

try {

firestore.runTransaction(transaction => {

const agentDoc = transaction.get(agentRef);

if (agentDoc.get('state') === 'IDLE') {

// State is as expected, proceed with the update

transaction.update(agentRef, { state: 'PROCESSING', task: newTaskDetails });

// Return a value to signal success

return "Transaction successful";

} else {

// Agent is busy, the transaction will safely abort

// Returning undefined or throwing an error aborts the transaction

return;

}

});

// Now, trigger the actual work asynchronously

processAgentTask(agentId);

} catch (e) {

console.error("Failed to start task due to concurrent modification or other error.");

}

If two users try to assign a task to the same idle agent at the exact same moment, the transaction guarantees that only one will succeed.

For architectural scaling, implement an asynchronous task queue. Instead of processing tasks immediately upon user request, decouple the request from the execution.

  1. Ingestion: Your user-facing doPost() or doGet() function does the absolute minimum: it validates the request, creates a new document in a taskQueue collection in Firestore with a PENDING status, and immediately returns a 202 Accepted response to the user.

  2. The Worker Pool: A time-driven trigger (e.g., running every minute) acts as your worker. It queries the taskQueue for a small batch of PENDING tasks (e.g., limit(5)).

  3. Processing: The worker uses a transaction to change the task’s status from PENDING to PROCESSING. This “locks” the task, preventing another worker instance from picking it up. It then executes the task. Upon completion, it updates the task’s status to COMPLETED or FAILED.

This pattern smooths out bursts of traffic, respects Apps Script’s concurrent execution quotas, and ensures that your system remains responsive even when the background workload is heavy.

Strategies for Robust Error Handling and State Recovery

In any distributed system, failure is a certainty. APIs become unavailable, scripts encounter unexpected data, and network connections drop. A resilient agent must be designed with failure in mind. The key is to treat errors as just another type of state.

1. Explicit Error States and Logging:

When a try...catch block catches an error, don’t just log it to the console. Persist the failure information directly into the agent’s state document in Firestore.

  • Change the state field to ERROR or RETRY_PENDING.

  • Add an errorInfo map containing the error message, stack trace, and a timestamp.

  • Introduce a retryCount field, initializing it to 1.


// Inside a function performing a fallible operation

try {

const response = UrlFetchApp.fetch("https://api.example.com/data");

// ... process response ...

} catch (e) {

const currentRetries = agent.retryCount || 0;

if (currentRetries >= MAX_RETRIES) {

// Move to a terminal failure state

firestore.updateDocument(`agents/${agentId}`, {

state: 'FAILED',

errorInfo: { message: e.message, timestamp: new Date() }

});

} else {

// Schedule a retry

updateStateForRetry(agentId, e, currentRetries);

}

}

2. Automated Retries with Exponential Backoff:

Constantly retrying a failing API every minute can exacerbate the problem. A better approach is exponential backoff.

When an error occurs and you decide to retry, calculate the next attempt time and store it.

nextRetryTimestamp = new Date(Date.now() + BASE_DELAY_MS * Math.pow(2, retryCount))

Your “Wake-Up and Check” trigger can then have a secondary query to find agents ready for a retry:

query.where('state', '==', 'RETRY_PENDING').where('nextRetryTimestamp', '<=', new Date())

This prevents a failing agent from consuming excessive resources and gives transient issues time to resolve. If the retryCount exceeds a defined maximum (e.g., 5 retries), transition the agent to a terminal FAILED state and trigger an alert for manual review.

3. Manual State Recovery:

Because the agent’s entire state is transparently stored in Firestore, recovery from a terminal FAILED state becomes straightforward. A developer or administrator can:

  1. Inspect the errorInfo in the Firestore document to diagnose the problem.

  2. Manually fix the root cause (e.g., correct an invalid configuration value in another field of the same document).

  3. Manually edit the document in the Firestore console, changing the state from FAILED back to a previous state like PENDING.

  4. The agent’s automated pacemaker will pick it up on its next run and resume the workflow, effectively providing a powerful and auditable mechanism for manual intervention and disaster recovery.

Conclusion: The Future of Automation is Stateful

We’ve moved beyond the era of simple, fire-and-forget scripts. The automations we’ve constructed in this guide represent a fundamental paradigm shift—from stateless, transactional commands to stateful, conversational agents. The distinction is critical. A stateless automation is a tool; it performs a task when told. A stateful agent, powered by the persistent memory of Firestore and the integrative power of Google Apps Script, is a partner. It remembers, it understands context across time and interactions, and it can execute complex, multi-step processes that were previously the exclusive domain of human intervention. This is not just about making our scripts more powerful; it’s about fundamentally rethinking the architecture of digital work and collaboration. The future of automation isn’t just about doing things faster; it’s about building systems with memory, resilience, and a genuine understanding of the tasks at hand.

Recap: Building Resilient and Intelligent AI Automations

Throughout this journey, we’ve dissected the core components of this new architectural pattern. We began by acknowledging the ceiling of traditional, stateless Apps Script automations—powerful, yet forgetful and brittle when faced with long-running or interactive tasks.

The introduction of Firestore was our pivotal moment. By integrating this scalable, real-time NoSQL database, we gave our agent a long-term memory. We transformed it from a simple executor into an entity that could:

  • Persist State: Track the progress of a task across multiple triggers, user interactions, and even system restarts.

  • Maintain Context: Remember previous conversations and decisions, allowing for more natural and intelligent follow-up actions.

  • Enable Asynchronous Operations: Manage complex workflows that unfold over hours or days, gracefully picking up where they left off.

Google Apps Script served as the central nervous system, the indispensable glue connecting our agent’s Firestore “memory” and its AI “brain” to the Automated Client Onboarding with Google Forms and Google Drive. ecosystem. It’s the engine that translates intent into action within Docs, Sheets, Gmail, and Calendar, making our agent a true first-class citizen in our digital environment. The result is a class of automation that is not only intelligent but profoundly resilient, capable of navigating the messy, unpredictable reality of modern workflows.

Your Next Steps in Advanced Workspace Architecture

What we’ve built here is a powerful foundation, a launchpad for your own explorations into advanced workspace automation. The concepts of state management and AI integration are not limited to a single use case; they are a new set of primitives for you to build with. As you move forward, consider the following paths:

  • Expand and Experiment: Take the core principles and apply them to a problem unique to your workflow. Could you build a project management agent that coordinates updates between Google Sheets, Calendar, and Gmail? Or an intelligent onboarding assistant that guides new team members through a series of documents and tasks over their first week? Start with a small, well-defined problem and iterate.

  • Fortify Your Architecture: As you move from prototype to production, focus on robustness. Implement comprehensive error handling and logging within your Apps Script code. Dive deep into Firestore Security Rules to ensure your agent’s memory is secure and accessible only by authorized processes. Monitor your API usage and Firestore costs to build sustainably.

  • Explore the Broader Ecosystem: Google Apps Script is a fantastic starting point, but it’s part of the much larger Google Cloud Platform. For more demanding tasks, consider graduating parts of your logic to Cloud Functions for more scalable, event-driven execution. Explore [Building Self Correcting Agentic Workflows with Building Self-Correcting Agentic Workflows with Vertex AI](https://votuduc.com/building-self-correcting-agentic-workflows-with-vertex-ai-p-20260321542526) for fine-tuning models or leveraging a wider array of AI services to give your agent even more specialized skills.

The blueprint is now in your hands. The fusion of stateful databases, versatile scripting environments, and powerful AI models unlocks a new frontier of possibility. Go build the intelligent, persistent, and truly helpful digital partners of tomorrow.


Tags

AI AgentsFirestoreGoogle Apps ScriptStateful AIEnterprise AutomationServerlessGoogle Cloud

Share


Previous Article
Building a RAG Context Manager with Apps Script and Gemini Pro
Vo Tu Duc

Vo Tu Duc

A Google Developer Expert, Google Cloud Innovator

Stop Doing Manual Work. Scale with AI.

Hi, I'm Vo Tu Duc (Danny), a recognised Google Developer Expert (GDE). I architect custom AI agents and Google Workspace solutions that help businesses eliminate chaos and save thousands of hours.

Want to turn these blog concepts into production-ready reality for your team?
Book a Discovery Call

Table Of Contents

1
The Challenge of Statelessness in Enterprise [Automated Job Creation in Real Time Jobber and Google Sheets Integration from Gmail](https://votuduc.com/Automated-Job-Creation-in-Jobber-from-Gmail-p115606)
2
Core Concept: The State Store Pattern Explained
3
Implementation Guide: Architecting Your Stateful Agent
4
Advanced Architectural Considerations and Use Cases
5
Conclusion: The Future of Automation is Stateful

Portfolios

AI Agentic Workflows
Cloud Engineering
AppSheet Solutions
Change Management
Strategy Playbooks
Product Showcase
Uncategorized
Workspace Automation

Related Posts

Automate Site Defect Punch Lists with Gemini and Google Chat
May 22, 2026
© 2026, All Rights Reserved.
Powered By

Quick Links

Book a CallAbout MeVolunteer Legacy

Social Media