HomeAbout MeBook a Call

Secure Agentic Workflows with a Firebase Auth Approval Gate

By Vo Tu Duc
May 05, 2026
Secure Agentic Workflows with a Firebase Auth Approval Gate

The next frontier of AI isn’t just about reasoning—it’s about taking action. While these autonomous agents promise to be the ultimate force multipliers, granting them control over our critical systems introduces significant and overlooked risks.

image 0

The Rise of Agentic AI and the Need for Human Oversight

We’re moving beyond the era of AI as a simple conversationalist or data processor. The new frontier is agentic AI—systems designed not just to reason, but to act. An AI agent can be tasked with a high-level goal, and it will autonomously formulate a plan, select tools (like APIs or code interpreters), and execute a sequence of steps to achieve that goal. Imagine an agent that can triage customer support tickets and draft responses, optimize cloud infrastructure spending by decommissioning unused resources, or even coordinate multi-step marketing campaigns.

The power is undeniable. These agents promise to be the ultimate force multipliers, automating complex workflows that were once the exclusive domain of human experts. But with this great power comes significant, non-obvious risk. Granting full autonomy to a system that can interact with production APIs, sensitive data, and external communication channels is a precarious proposition. Before we hand over the keys, we need to build a robust system of checks and balances.

Why autonomous workflows need a human safety net

Full autonomy in high-stakes environments is a liability, not a feature. The core issue stems from the probabilistic and sometimes unpredictable nature of the Large Language Models (LLMs) that power these agents. Here are the primary failure modes we must guard against:

  • Action-Oriented Hallucination: We’re all familiar with LLMs inventing facts. When an agent acts on these fabrications, the consequences escalate dramatically. It might attempt to call a non-existent API endpoint, pass malformed data to a critical function, or email a fabricated, damaging summary to a real client. The agent acts with complete confidence, unaware its entire plan is built on a faulty premise.
image 1
  • Ambiguity and Scope Creep: Agents require precise instructions. A vaguely defined goal like “clean up the project directory” could be interpreted in countless ways. Does it mean deleting .tmp files, or does it mean archiving the entire source code repository? Without a confirmation step, the agent’s reasonable—but incorrect—interpretation could lead to irreversible data loss.

  • Security and Permission Escalation: An agent is only as secure as the tools it can access. If an agent has direct access to powerful APIs (e.g., user.delete, billing.update), any vulnerability—be it a clever prompt injection attack or an unforeseen model flaw—could be exploited to cause catastrophic damage. The agent becomes a single point of failure and a massive attack surface.

Simply put, we cannot afford to let the agent be the final arbiter of its own actions. We need a safety net that separates the agent’s ability to plan from the authority to execute.

Introducing the ‘Human-in-the-Loop’ (HITL) approval pattern

The solution is a classic engineering pattern adapted for the age of AI: Human-in-the-Loop (HITL). Instead of building a fully autonomous actor, we architect a system where the AI functions as a hyper-competent assistant that proposes actions. The final decision to execute, modify, or reject that proposal rests with a trusted human operator.

Here’s how the pattern works in an agentic context:

  1. Proposal: The AI agent receives a task, breaks it down, and determines the exact action required. This could be an API call with specific parameters, the content of an email, or a script to be run.

  2. Staging: Instead of executing the action immediately, the agent serializes its plan into a structured format and places it in a staging area or queue. This plan is inert—a simple data object with no inherent permissions.

  3. **Review & Approval: A human operator is notified. They are presented with a clear, unambiguous view of the proposed action: “Do you want to send this exact email to this specific recipient?” or “Do you approve running gcloud compute instances delete vm-prod-01?”

  4. Execution: Only upon receiving explicit, authenticated approval from the human does the system proceed. A separate, trusted execution module—which the agent itself cannot directly access—carries out the command.

This HITL gate transforms the agent’s role. It does the complex cognitive work of planning and tool selection, but the final, critical step of execution is gated by human judgment and accountability.

Architectural overview: Combining Apps Script, AI-Powered Invoice Processor, and Firebase for secure execution

To implement this robust HITL pattern, we will orchestrate a trio of powerful Google Cloud and Workspace services, each playing a distinct and critical role. This architecture is designed for security, scalability, and rapid development.

Here is a high-level overview of the components and the flow of an action request:

  • Firebase (The State Manager & Security Core): Firebase acts as the central nervous system for our approval workflow.

  • Firestore: We will use a Firestore collection as the “staging area” for proposed actions. The agent writes a new document here for each action it wants to take, with a default status of pending.

  • Firebase Authentication: This is our security gate. We will leverage Firebase Auth to ensure that only designated, authenticated users can view and approve requests. This is the cryptographic link between a user’s identity and their authority to approve an action.

  • AMA Patient Referral and Anesthesia Management System (The Approval UI): AppSheetway Connect Suite provides the “human” part of the loop. We will build a simple, secure no-code application on top of our Firestore data. This app will:

  • Securely display pending action requests to authenticated users.

  • Provide simple “Approve” and “Reject” buttons.

  • Upon user interaction, update the corresponding document’s status in Firestore to approved or rejected.

  • **[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) (The Secure Executor): This is our sandboxed and privileged execution environment. The agent never gets direct access to this layer.

An* onEdit trigger in a Google Sheet connected to our Firestore data (via a simple third-party connector) or a Cloud Function Firestore trigger** will monitor for documents whose status changes to approved.

  • When an approval is detected, the trigger invokes a specific Apps Script function.

  • This Apps Script project is granted the necessary OAuth scopes to perform the actual work (e.g., send a Gmail, update a Calendar event, call an external enterprise API). It receives the parameters from the Firestore document and executes the task securely.

This separation of concerns is the key to the architecture’s security. The agent can only write to a database. The user can only change a status field via an authenticated UI. And the execution environment only runs pre-defined code in response to a trusted state change, completely isolated from the agent’s reasoning process.

Foundation: Structuring the Task Queue 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)

Before we can build a secure approval gate, we need a place for our agent’s proposed tasks to live. This is our task queue. While you could use a sophisticated database like Firestore or a dedicated queuing service, the beauty of this approach lies in its simplicity and accessibility. We’ll use a tool that’s ubiquitous, easy to inspect, and surprisingly powerful for this use case: Google Sheets.

Think of this Sheet as the central ledger for our agent’s activities. It’s where every proposed action is logged, its status is tracked, and an audit trail is naturally created. It’s the foundational layer upon which our entire secure workflow will be built.

Designing the data schema for tasks

A well-defined schema is the bedrock of any data-driven system. It ensures consistency and provides a clear contract for any service—be it our Apps Script functions or the agent itself—that interacts with the queue. We’ll keep it simple but effective.

Create a new Google Sheet and name the first tab “Tasks”. The first row should be our header, defining the columns.

| Column | Field Name | Data Type | Purpose |

| :--- | :--- | :--- | :--- |

| A | ID | String | A unique identifier for the task. This is crucial for targeting specific tasks for approval or execution. We’ll use a UUID to prevent collisions. |

| B | Description | String | A human-readable description of what the agent intends to do. E.g., “Send a follow-up email to [email protected] regarding invoice #1234.” |

| C | Status | String | The current state of the task. This is the heart of our workflow. Key values will be: Pending, Approved, Rejected, Completed, Error. |

| D | Requester | String | The email address of the user who initiated the agentic workflow. Essential for auditing and notifications. |

| E | Approver | String | The email address of the user who ultimately approved or rejected the task. This will be populated later in the workflow. |

| F | Timestamp | Timestamp | The ISO 8601 timestamp of when the task was created. Useful for sorting and tracking. |

This structure gives us everything we need: a unique way to identify a task (ID), a clear understanding of the intent (Description), a state machine to manage its lifecycle (Status), and an audit trail (Requester, Approver, Timestamp).

Creating the initial Apps Script function to populate tasks

Now, let’s bring our schema to life. We need a programmatic way to add new tasks to our Sheet. This is where Genesis Engine AI Powered Content to Video Production Pipeline comes in. It’s the serverless JavaScript-based engine that will act as the backend for our queue.

From your Google Sheet, navigate to Extensions > Apps Script. This will open a new script editor. Let’s write our first function.


// The unique ID of your Google Sheet. You can find this in the URL.

// e.g., https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit

const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';

const SHEET_NAME = 'Tasks';

/**

* Creates a new task in the Google Sheet with a 'Pending' status.

* This function acts as the entry point for our agent's proposed actions.

*

* @param {string} description - A human-readable description of the task.

* @param {string} requesterEmail - The email of the user initiating the task.

* @returns {object} - An object containing the result and the ID of the created task.

*/

function createPendingTask(description, requesterEmail) {

try {

// 1. Access the specific sheet in our spreadsheet.

const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);

if (!sheet) {

throw new Error(`Sheet with name "${SHEET_NAME}" not found.`);

}

// 2. Generate the necessary data for the new task row.

const taskId = Utilities.getUuid(); // Generate a unique identifier.

const timestamp = new Date().toISOString();

const status = 'Pending'; // The crucial initial state.

const approver = ''; // This field is intentionally left blank.

// 3. The new row array must match the order of our schema.

const newRow = [taskId, description, status, requesterEmail, approver, timestamp];

// 4. Append the new row to the bottom of the sheet.

sheet.appendRow(newRow);

console.log(`Successfully created task ${taskId} with status 'Pending'.`);

// 5. Return a success response with the new task's ID.

return {

success: true,

taskId: taskId

};

} catch (error) {

console.error(`Failed to create task: ${error.message}`);

return {

success: false,

error: error.message

};

}

}

This function is our gateway. Any time our agent wants to perform a sensitive action, it won’t execute it directly. Instead, it will call this function, providing the description and requesterEmail. The function then logs the request in our Google Sheet, assigns it a unique ID, and—most importantly—sets its Status to Pending.

Understanding the workflow pause point before execution

This is the most critical concept in our entire architecture. By writing the task to the sheet with a Pending status, we have intentionally created a workflow pause point.

In a standard, fully-automated agentic workflow, a request triggers an immediate action. This is powerful but can be risky. There’s no opportunity for oversight, no “undo” button before a potentially erroneous or harmful action is taken.

Our approach fundamentally changes this dynamic. The agent’s initial job is not to do the thing, but to propose the thing. The successful execution of the createPendingTask function represents the end of the agent’s first responsibility. The task is now “at rest” in our queue, a static entry in a spreadsheet. It is inert and harmless.

This pause is the deliberate gap we’ve engineered to insert a human-in-the-loop. The workflow cannot proceed until an external, authorized signal changes the task’s status from Pending to Approved. This single, simple state change is the key that unlocks the next phase of execution, and it’s precisely what we will secure using Firebase Authentication in the upcoming sections.

Building the Secure Identity Layer with Firebase Auth

At the heart of any secure approval system is an undeniable, verifiable identity. Before our agentic workflow can trust an approval, it must first trust the approver. This is where we lay the foundation. We need a robust, scalable, and easily integrated identity layer that can tell our [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) backend, with cryptographic certainty, who is making the request. Enter Firebase Authentication.

Why Firebase Auth is the right choice for Workspace developers

While you could roll your own authentication system, the complexities of secure password storage, session management, and multi-factor authentication are a minefield. For developers building on the Google ecosystem, Firebase Auth isn’t just a good choice; it’s the path of least resistance to enterprise-grade security.

Here’s why it’s a perfect fit for our agentic workflow:

  • Deep Google Ecosystem Integration: As a Google product, Firebase is designed to work seamlessly with Google Cloud and, by extension, 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. This means authentication is handled by the same identity provider users already trust for their Google accounts, creating a frictionless user experience.

  • Managed Security Infrastructure: Firebase handles the heavy lifting. It manages user sessions, secures credentials, and provides protection against common threats like brute-force attacks. This frees you to focus on your application’s core logic instead of becoming a security expert overnight.

  • Flexible and Powerful: While we’ll focus on “Sign in with Google,” Firebase Auth supports a vast array of providers (email/password, social logins like GitHub and X, SAML, OIDC). This future-proofs your application, allowing it to evolve without requiring a complete identity system overhaul.

  • Identity Verification via JWT: The cornerstone of our security model is the JSON Web Token (JWT). After a user signs in, Firebase provides a signed ID token. This token can be passed to our Apps Script backend, which can then independently verify its authenticity. It’s a standard, secure way to transmit identity between a client and a server.

  • Generous Free Tier: For most internal tools and many production applications, the Firebase Auth free tier (part of the Spark Plan) is more than sufficient, making it an incredibly cost-effective solution.

Step-by-step: Setting up a Firebase project and enabling authentication

Let’s get our hands dirty and set up the Firebase project that will serve as our identity provider. If you already have a Firebase project, you can skip to step 3.

  1. Create a Firebase Project:

Click on* “Add project”**.

  • Give your project a descriptive name, like agent-approval-workflow.

  • You’ll be asked if you want to enable Google Analytics. You can disable this for this project if you wish.

Click* “Create project”**. Firebase will provision your resources, which may take a minute.

  1. Navigate to the Authentication Section:
  • Once your project is ready, you’ll land on the project dashboard.

In the left-hand navigation menu, under the* “Build” dropdown, click on “Authentication”**.

  1. Enable the Google Sign-In Provider:

Click the* “Get started”** button.

You’ll be taken to the “Sign-in method” tab. Select* “Google”** from the list of providers.

A configuration panel will slide out. Click the* “Enable”** toggle.

Select a* “Project support email”** from the dropdown menu. This is the email address shown to users when they consent to the sign-in.

Click* “Save”**.

That’s it! You’ve now configured your Firebase project to act as an identity provider that can authenticate users via their Google accounts. Later, we’ll need the web app configuration details from Project Settings > General > Your apps, but for now, our backend identity service is ready to go.

Integrating Firebase with Google Apps Script for identity verification

Now for the crucial connection: making our Google Apps Script backend aware of the identity established by Firebase. Since Apps Script runs in a serverless environment without a native Firebase Admin SDK, we can’t just import a library and call admin.auth().verifyIdToken().

Instead, we’ll perform the JWT verification manually. The flow is simple:

  1. The client (e.g., a custom sidebar in a Google Sheet) gets a Firebase ID token after the user signs in.

  2. The client sends this token to a server-side Apps Script function.

  3. The Apps Script function verifies the token’s signature and claims to authenticate the user.

Here’s how to build the verification logic in Apps Script.

1. Add a JWT Library to Apps Script

Apps Script doesn’t have a built-in crypto library for JWT verification. We’ll need to add a lightweight, third-party one. A great option is a library designed for this purpose.

  • Create a new script file in your Apps Script project named JWT.gs.

  • Copy the content from a suitable library, such as the one found in this GitHub Gist for Apps Script JWT decoding. This provides the necessary functions to parse and verify the token.

2. Create the Verification Function

In a separate script file (e.g., Auth.gs), create a function that will handle the token verification. This function will be the secure gateway for any action that requires authentication.


/**

* Verifies a Firebase ID token and returns the decoded payload if valid.

* @param {string} token The Firebase ID token from the client.

* @returns {object|null} The decoded token payload or null if invalid.

*/

function getVerifiedPayload(token) {

// 1. Get your Firebase Project ID from Project Settings in the Firebase console.

const FIREBASE_PROJECT_ID = "your-firebase-project-id";

const issuer = "https://securetoken.google.com/" + FIREBASE_PROJECT_ID;

try {

// 2. Fetch Google's public keys to verify the token's signature.

// Cache them to avoid fetching on every request.

const cache = CacheService.getScriptCache();

const cachedKeys = cache.get("firebase-public-keys");

let publicKeys;

if (cachedKeys) {

publicKeys = JSON.parse(cachedKeys);

} else {

const response = UrlFetchApp.fetch("https://www.googleapis.com/robot/v1/metadata/x509/[email protected]");

publicKeys = JSON.parse(response.getContentText());

// Cache for 6 hours, as Google keys rotate.

cache.put("firebase-public-keys", JSON.stringify(publicKeys), 21600);

}

// 3. Decode the token and verify its signature and claims.

const decodedToken = jwt_decode(token, publicKeys, issuer, FIREBASE_PROJECT_ID);

// 4. If we reach here, the token is valid. Return the payload.

// The payload contains user info like uid, email, name, picture.

console.log("Token verified successfully for user: " + decodedToken.payload.email);

return decodedToken.payload;

} catch (e) {

// 5. If any part of the verification fails, the library will throw an error.

console.error("Token verification failed: " + e.toString());

return null;

}

}

/**

* A test function to demonstrate usage.

*/

function testVerification() {

// Replace with a real, valid ID token from your client application for testing.

const fakeToken = "PASTE_A_REAL_ID_TOKEN_HERE";

const payload = getVerifiedPayload(fakeToken);

if (payload) {

Logger.log("Success! Authenticated user email: " + payload.email);

Logger.log("User ID (uid): " + payload.user_id);

} else {

Logger.log("Failed to authenticate user.");

}

}

Breaking Down the Code:

  1. **Project ID & Issuer: We define constants for our Firebase Project ID and the expected token issuer. This is a critical security check to ensure the token came from our Firebase project.

  2. Fetch & Cache Public Keys: The ID token is signed with a private key. To verify it, we need the corresponding public key. Google publishes these at a public endpoint. We use UrlFetchApp to get them and CacheService to store them for a few hours. This is vital for performance, as it prevents an HTTP request on every single function call.

  3. Decode and Verify: We pass the token, the public keys, and our issuer/audience claims to the jwt_decode function from the library we added. This single call performs several critical checks:

  • Signature Verification: Is the token actually signed by Google?

  • Audience (aud) Check: Was this token intended for our project?

  • Issuer (iss) Check: Was this token issued by the correct authority?

  • Expiration (exp) Check: Has the token expired?

  1. Return Payload: If all checks pass, the function returns the token’s payload, which contains the user’s unique Firebase ID (uid), email, name, and other details. Your backend logic can now trust this information completely.

  2. Error Handling: If any check fails, the library throws an error, which we catch and handle gracefully by returning null. Any function calling getVerifiedPayload will know that the user is not authenticated.

With this getVerifiedPayload function in place, we have successfully bridged the gap between Firebase Auth and Google Apps Script. We now have a secure, reusable utility to enforce that only authenticated users can trigger our sensitive agentic workflows.

Crafting the User Approval Interface in OSD App Clinical Trial Management

With our Google Sheet primed to receive requests from the agentic workflow, our next move is to build the human-in-the-loop component. We need a simple, secure, and mobile-friendly way for an authorized user to review and act on these requests. This is where Google AppSheet shines. It allows us to transform our humble spreadsheet into a functional application with zero code, providing the perfect “pane of glass” for our approval gate.

Generating an AppSheet application from your Google Sheet data source

First things first, let’s bootstrap the application directly from our existing data. AppSheet’s intelligence in scaffolding a starter app from a Google Sheet is one of its most powerful features.

  1. Navigate to AppSheet: Head over to appsheet.com and sign in with the same Google account that has access to your Google Sheet.

  2. Start with Existing Data: On the “My Apps” page, click “Create” -> “App” -> “Start with existing data”.

  3. Configure the New App:

  • App name: Give it a clear name, like Agent Task Approver.

  • Category: Choose something appropriate, such as Operations.

  • Choose your data: Click the button and the Google Drive file picker will appear. Navigate to and select the Google Sheet you created in the previous step.

AppSheet will now analyze your spreadsheet’s structure. It will identify the columns (task_id, timestamp, status, etc.) and infer their data types. In a few moments, you’ll be dropped into the AppSheet editor with a basic, functional application that can already read the data from your sheet.

Before moving on, it’s a good practice to verify the column types. Go to the Data > Columns tab in the editor. Ensure status is set to an Enum type (AppSheet is usually smart enough to do this automatically if you have “PENDING”, “APPROVED”, “REJECTED” values already in the sheet). This will create a dropdown for the status, which is exactly what we want.

Configuring the UI for reviewing pending tasks

The default app is functional, but it’s not optimized for our specific use case. It likely shows all tasks, regardless of their status. Our goal is to create a clean, focused view that only displays tasks awaiting a decision. We’ll achieve this using an AppSheet “Slice”.

A Slice is a filtered subset of your table’s data. By creating a slice for pending tasks, we can build a UX view that exclusively targets those records.

  1. Create the Slice:

In the AppSheet editor, navigate to the* Data > Slices** tab.

Click* + New Slice**.

Select your Google Sheet as the* Source Table**.

  • Name the slice something descriptive, like Pending Tasks.

The most critical part is the* Row filter condition**. This is an expression that determines which rows belong in the slice. Enter the following expression:


[status] = "PENDING"

  • This simple formula tells AppSheet to only include rows where the value in the status column is exactly “PENDING”. Save the slice.
  1. Create the View:

Now, navigate to the* UX** tab.

AppSheet probably created a default view for you. Let’s create a new one for clarity. Click* + New View**.

For the* For this data** option, select your newly created Pending Tasks slice from the dropdown.

  • Give the view a name, like Pending Approvals.

For the* View type**, a Deck or Card view is often more visually appealing for a list of tasks than a simple table. Let’s choose Deck.

  • Configure the view’s columns to display the most relevant information at a glance. For example:

  • Primary header: task_description

  • Secondary header: requested_by

  • Summary column: timestamp

Now, when you look at the app preview, you’ll see a clean, focused list showing only the tasks that require your attention.

Creating ‘Approve’ and ‘Reject’ actions to update the sheet status

Seeing the pending tasks is only half the battle. We need to empower the user to act on them. In AppSheet, this is done by creating “Actions”. We’ll create two distinct actions: one to approve a task and one to reject it.

  1. Navigate to the Actions Tab: In the editor, go to the Behavior > Actions tab.

  2. Create the ‘Approve’ Action:

Click* + New Action**.

  • Give it a clear name: Approve Task.

  • For a record of this table: Make sure your main table (not the slice) is selected.

  • Do this: Select Data: set the values of some columns in this row. This action type allows us to modify the data in the row the action is triggered on.

  • Set these columns: Click Add and set the status column to the value "APPROVED". Make sure to include the double quotes.

  • Appearance:

  • Display name: Type Approve. This is the text the user will see on the button.

  • Action icon: Choose a suitable icon, like a checkmark (check).

  • Prominence: Set this to Display prominently to make the button easy to find.

  1. Create the ‘Reject’ Action:

Now, repeat the process for the rejection action. You can even click* Copy** on the “Approve Task” action to speed things up.

  • Action Name: Reject Task.

  • Set these columns: Change the value to be set for the status column to "REJECTED".

  • Appearance:

  • Display name: Reject.

  • Action icon: Choose a different icon, like a cross or a thumbs-down (close).

  • Prominence: You can also set this to Display prominently. AppSheet will place the buttons next to each other.

With these actions configured, the user experience is now complete. An approver can open the app, tap on a pending task from the list, view its details, and will see two prominent “Approve” and “Reject” buttons. Tapping either button will instantly update the status column for that specific row in the Google Sheet, which in turn completes the feedback loop for our agentic workflow.

Closing the Loop: Triggering and Verifying Execution

With the approval mechanism in place, the final phase is to create a secure, automated process that acts upon that approval. Simply seeing the word “Approved” in a spreadsheet cell is insufficient; we must programmatically verify the approver’s identity and then reliably execute the intended task. This is where we close the loop, transforming a manual approval into a trusted, automated action.

Using an Apps Script trigger to detect status changes to ‘Approved’

The bridge between the user’s action in the Google Sheet and our backend logic is an Apps Script trigger. The onEdit(e) simple trigger is a powerful, event-driven function that runs automatically whenever a user changes a value in the spreadsheet. We’ll use this to monitor our ‘Status’ column.

When an edit occurs, the trigger fires and passes an event object (e) to our function. This object contains crucial context, such as the range that was edited, its old and new values, and the user who made the change. Our first job is to filter these events to act only when a task’s status is changed to “Approved”.

Here’s a foundational structure for the trigger function:


// Define constants for column numbers to make the code more readable and maintainable.

const STATUS_COLUMN = 5; // Assuming Status is in column E

const JWT_COLUMN = 6;    // Assuming the JWT is stored in column F

function onEdit(e) {

const sheet = e.source.getActiveSheet();

const range = e.range;

// 1. Check if the edit happened in the correct sheet and column.

// We only care about single-cell edits in the STATUS_COLUMN.

if (sheet.getName() !== "AgentTasks" || range.getColumn() !== STATUS_COLUMN || range.getNumRows() > 1) {

return;

}

// 2. Check if the new value is "Approved".

const newStatus = e.value;

if (newStatus !== "Approved") {

return;

}

// 3. If conditions are met, proceed with verification and execution.

const row = range.getRow();

console.log(`Approval detected on row ${row}. Initiating verification...`);

processApproval(sheet, row);

}

This code acts as a gatekeeper. It ignores irrelevant edits, ensuring our more intensive verification logic only runs when absolutely necessary. For production workflows, consider upgrading to an installable onEdit trigger, as it runs with the authority of the user who installed it, providing more consistent permissions and avoiding certain simple trigger limitations.

The critical step: Verifying the approver’s identity against Firebase Auth

This is the lynchpin of the entire security model. We cannot trust the e.user.getEmail() property from the event object, as a user could manually type “Approved” into the cell. Instead, we must cryptographically verify the JSON Web Token (JWT) that our web app placed in the sheet alongside the status change.

The verification process involves several checks:

  1. Retrieve the JWT: Get the token from the corresponding hidden column for the edited row.

  2. Validate the Signature: Use Firebase’s public keys to verify that the token was signed by your Firebase project and has not been tampered with.

  3. Check the Claims: Ensure the token is not expired (exp claim) and was intended for your application (aud claim).

  4. Extract the Identity: If validation succeeds, decode the token to securely extract the user’s UID and email.

  5. Authorize the User: Compare the verified email or UID against an “authorized approvers” list for that specific task.

Implementing JWT validation in Apps Script requires a library. You can use the robust OAuth2 for Apps Script library or a dedicated JWT library. For this example, we’ll abstract the complexity into a helper function, verifyFirebaseJwt.


function processApproval(sheet, row) {

try {

const jwt = sheet.getRange(row, JWT_COLUMN).getValue();

if (!jwt) {

throw new Error("JWT not found for this approval.");

}

// This function encapsulates the complex JWT validation logic.

// In a real implementation, it would fetch Firebase public keys and use a JWT library.

const decodedToken = verifyFirebaseJwt(jwt);

// Extract the verified user's email from the decoded token payload.

const approverEmail = decodedToken.payload.email;

console.log(`JWT successfully verified for user: ${approverEmail}`);

// Optional but recommended: Check if this user is authorized for this specific task.

// const authorizedApprovers = sheet.getRange(row, AUTHORIZED_APPROVERS_COLUMN).getValue();

// if (!authorizedApprovers.includes(approverEmail)) {

//   throw new Error(`User ${approverEmail} is not authorized to approve this task.`);

// }

// If verification and authorization pass, execute the task.

executeAgentTask(sheet, row, approverEmail);

} catch (error) {

console.error(`Verification failed for row ${row}: ${error.message}`);

// Revert the status to its previous state or set it to "Verification Failed" for a clear audit trail.

sheet.getRange(row, STATUS_COLUMN).setValue("Verification Failed");

sheet.getRange(row, STATUS_COLUMN + 2).setValue(`Error: ${error.message}`); // Log error in a notes column

}

}

function verifyFirebaseJwt(token) {

// In a real-world scenario, this function would:

// 1. Fetch Google's public signing keys for Firebase Auth.

// 2. Use a JWT library to parse and verify the token's signature, expiration, and audience.

// 3. Return the decoded token payload upon success or throw an error on failure.

// For this example, we'll simulate a successful verification.

// NOTE: THIS IS A MOCK AND IS NOT SECURE. REPLACE WITH A REAL JWT LIBRARY.

if (!token.startsWith("eyJ")) {

throw new Error("Invalid token format.");

}

const mockPayload = {

email: "[email protected]",

uid: "abc123xyz",

// ... other claims like exp, aud, iss

};

console.log("Simulating successful JWT verification.");

return { payload: mockPayload };

}

By relying on the JWT, we shift from a trust-by-proxy model (trusting the user logged into Google Sheets) to a cryptographic proof of identity.

Executing the final task logic and updating the status to ‘Executed’

Once the approver’s identity has been unequivocally confirmed, the script can safely proceed with the agent’s work. This logic is highly specific to your workflow—it could involve making API calls, manipulating data in other systems, provisioning resources, or generating reports.

We’ll encapsulate this work in a dedicated function, executeAgentTask. After the task completes successfully, the final step is to update the sheet to reflect this, providing a clear and permanent audit trail.


function executeAgentTask(sheet, row, verifiedApproverEmail) {

try {

// 1. Gather task parameters from the sheet.

const taskDetails = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0];

const taskType = taskDetails[0]; // e.g., 'send-invoice'

const taskTarget = taskDetails[1]; // e.g., '[email protected]'

const taskAmount = taskDetails[2]; // e.g., 500

console.log(`Executing task '${taskType}' for target '${taskTarget}'...`);

// 2. Perform the agentic action.

// This is where you would place your specific business logic.

// For example, call an external invoicing API.

// MailApp.sendEmail(taskTarget, "Your Invoice", `Amount due: $${taskAmount}`);

console.log(`Task for row ${row} completed successfully.`);

// 3. Update the sheet to close the loop.

const executionTimestamp = new Date();

sheet.getRange(row, STATUS_COLUMN).setValue("Executed");

sheet.getRange(row, STATUS_COLUMN + 1).setValue(verifiedApproverEmail); // Log the verified approver

sheet.getRange(row, STATUS_COLUMN + 2).setValue(executionTimestamp); // Log the execution time

} catch (error) {

console.error(`Task execution failed for row ${row}: ${error.message}`);

// Update the sheet with an error status for manual review.

sheet.getRange(row, STATUS_COLUMN).setValue("Execution Failed");

sheet.getRange(row, STATUS_COLUMN + 2).setValue(`Error: ${error.message}`);

}

}

With this final piece in place, the workflow is complete. An onEdit trigger detects the approval, a robust verification function confirms the approver’s identity via a Firebase JWT, and an execution function performs the task and records the outcome. This creates a secure, auditable, and fully automated approval gate for your agentic workflows.

Conclusion: Scaling Your Secure Automated Work Order Processing for UPS Architecture

We’ve journeyed from a conceptual need—taming the power of autonomous agents—to a concrete, secure implementation using a Firebase Auth approval gate. By injecting a Human-in-the-Loop (HITL) checkpoint, you transform potentially risky automated processes into reliable, auditable, and controlled workflows. This pattern isn’t just a theoretical exercise; it’s a production-ready blueprint for building trust in your automation.

Recap of the HITL approval pattern’s benefits

Before we look ahead, let’s crystallize the value of what we’ve built. This architecture provides a multi-layered defense for your critical systems:

  • **Verifiable Identity: At its core, this pattern replaces ambiguous API keys or service account credentials with a definitive human identity. The JWT from Firebase Auth is a cryptographic signature, proving who authorized an action, not just what authorized it.

  • Intentionality and Control: Automation can run amok due to bugs or flawed logic. Requiring a human to explicitly approve a high-stakes operation—like a database schema migration or a bulk customer email—ensures that every critical action is intentional and reviewed.

  • **Decoupled and Scalable Security: The approval logic is decoupled from the agent’s core task logic. Your agent focuses on what to do, while the approval gate focuses on whether it should be done. This separation of concerns makes the system easier to maintain, test, and scale. You can apply this same security gate to dozens of different agentic workflows without reimplementing the authentication and authorization logic each time.

  • Leveraging a Managed Ecosystem: By using Firebase, we offload the immense complexity of building and maintaining a secure authentication system. We get user management, authentication providers, and secure token generation out of the box, allowing us to focus on the business logic of our application.

Advanced considerations: Audit logging, notifications, and error handling

The basic pattern is powerful, but for true business-critical systems, you’ll need to build a more robust operational wrapper around it.

  • Audit Logging: Accountability doesn’t end with approval. Every approval request and its outcome must be logged immutably. For each event, capture the approver’s UID, the timestamp, the IP address, the full payload of the proposed action, and the final status (approved, rejected, expired). Store this data in a dedicated system like Google Cloud Logging, a write-only Firestore collection with strict security rules, or a specialized logging service. This trail is invaluable for security forensics, compliance audits, and debugging.

  • Real-time Notifications: An approval request is useless if the approver doesn’t know it exists. When your agent creates a pending task, it should trigger a notification. This could be a push notification via Firebase Cloud Messaging (FCM) to a dedicated admin app, a message to a Slack channel via a webhook, or an email sent through a service like SendGrid. The notification should contain a summary of the action and a direct link to the approval interface to minimize friction.

  • **Comprehensive Error Handling: What happens if the agent fails after receiving the approved JWT? The system must be resilient. Implement a state machine for your tasks (e.g., pending_approval, approved, executing, completed, failed). If an executing task fails, you need robust retry logic (with exponential backoff) and, eventually, a dead-letter queue mechanism to flag it for manual intervention. Ensure you have alerts in place for tasks that enter a failed state so your operations team can investigate immediately.

Next steps for your business-critical workflows

You now have a powerful new pattern in your architectural toolkit. The key to unlocking its full potential is to apply it strategically and iteratively.

  1. Identify Your First Use Case: Start by identifying a single, high-impact workflow that currently requires manual intervention due to its inherent risk. Good candidates include deployment pipelines, data backfills, or customer support actions that modify user data. Implement this approval gate as a pilot project to build confidence and demonstrate its value.

  2. Enhance with Role-Based Access Control (RBAC): Elevate your security model by using Firebase Auth’s custom claims. You can assign roles like admin, developer, or support_lead to your users. Your approval endpoint can then decode the JWT, inspect these claims, and ensure that only users with the appropriate role can approve certain classes of actions. A developer might be able to approve a staging deployment, but only a senior admin can approve a production database migration.

  3. Explore Advanced Approval Scenarios: Consider workflows that require more than one signature. This pattern can be extended to support M-of-N approvals, where a task requires approval from, for example, two out of three designated managers before it can proceed. This involves managing the state of approvals in your database until the required threshold is met.

  4. Build a Centralized “Action Center”: As you integrate this pattern across more services, create a unified dashboard for your team. This “Action Center” would list all pending approval requests from all your various agents, providing a single, secure interface for managers and operators to review and act on pending tasks.

By thoughtfully integrating this HITL approval gate, you can confidently deploy autonomous agents to handle increasingly complex and critical tasks, unlocking new levels of efficiency while maintaining the highest standards of security and control.


Tags

Agentic AIFirebaseSecurityAI WorkflowsAuthenticationHuman-in-the-loop

Share


Previous Article
Secure Google Workspace Addons with Firebase Auth A GDE Blueprint
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 Rise of Agentic AI and the Need for Human Oversight
2
Foundation: Structuring the Task Queue 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)
3
Building the Secure Identity Layer with Firebase Auth
4
Crafting the User Approval Interface in OSD App Clinical Trial Management
5
Closing the Loop: Triggering and Verifying Execution
6
Conclusion: Scaling Your Secure Automated Work Order Processing for UPS Architecture

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