Introduction
Commercial workflow approval platforms often cost thousands of dollars per month in enterprise SaaS licensing fees. Yet, almost every business already possesses the exact infrastructure required to build seamless approval engines: Google Forms for data collection, Google Sheets as the relational database, Gmail for one-click manager sign-offs, and Google Apps Script for automated state management.
Workflow Logic
- An employee submits an expense reimbursement or time-off request via Google Forms.
- Form submission triggers an Apps Script
onFormSubmitevent. - The script generates a secure, unique approval token and emails the designated manager with interactive [Approve] and [Reject] HTML action buttons.
- Clicking a button hits an Apps Script Web App endpoint, updating the spreadsheet status and notifying the employee of the outcome.
Step 1: Setting Up the Linked Google Form & Sheet
Create a Google Form with the following fields: Employee Name, Employee Email, Expense Amount, Category, Manager Email, and Receipt Upload. Link the form to a destination Google Sheet. The sheet will automatically include a Timestamp column alongside your form fields.
Step 2: The Approval Web App Script
In your spreadsheet's Apps Script editor, implement both the form submission listener and the doGet() web app endpoint:
// 1. Triggered automatically on Form Submission
function onFormSubmit(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const row = e.range.getRow();
const values = e.values;
const timestamp = values[0];
const employeeName = values[1];
const employeeEmail = values[2];
const amount = values[3];
const managerEmail = values[4];
// Generate a random unique verification token
const approvalToken = Utilities.getUuid();
sheet.getRange(row, 7).setValue("Pending");
sheet.getRange(row, 8).setValue(approvalToken);
// Deploy as Web App URL
const webAppUrl = "YOUR_APPS_SCRIPT_WEB_APP_EXEC_URL";
const approveLink = webAppUrl + "?action=approve&token=" + approvalToken + "&row=" + row;
const rejectLink = webAppUrl + "?action=reject&token=" + approvalToken + "&row=" + row;
const htmlBody = `
<div style="font-family: Arial, sans-serif; padding: 20px;">
<h3>New Expense Approval Request</h3>
<p><strong>Employee:</strong> ${employeeName} (${employeeEmail})</p>
<p><strong>Amount:</strong> $${amount}</p>
<div style="margin-top: 20px;">
<a href="${approveLink}" style="background-color: #16a34a; color: white; padding: 10px 20px; text-decoration: none; border-radius: 6px; font-weight: bold; margin-right: 10px;">Approve Request</a>
<a href="${rejectLink}" style="background-color: #dc2626; color: white; padding: 10px 20px; text-decoration: none; border-radius: 6px; font-weight: bold;">Reject Request</a>
</div>
</div>
`;
GmailApp.sendEmail(managerEmail, "Approval Required: Expense Request from " + employeeName, "", {
htmlBody: htmlBody
});
}
// 2. Handles Manager Click on Approve / Reject
function doGet(e) {
const action = e.parameter.action;
const token = e.parameter.token;
const row = parseInt(e.parameter.row);
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const storedToken = sheet.getRange(row, 8).getValue();
const currentStatus = sheet.getRange(row, 7).getValue();
if (storedToken !== token) {
return HtmlService.createHtmlOutput("<h2 style='color:red;'>Security Error: Invalid or expired token.</h2>");
}
if (currentStatus !== "Pending") {
return HtmlService.createHtmlOutput("<h2 style='color:orange;'>Notice: This request has already been " + currentStatus + ".</h2>");
}
const newStatus = action === "approve" ? "Approved" : "Rejected";
sheet.getRange(row, 7).setValue(newStatus);
sheet.getRange(row, 9).setValue(new Date());
// Notify employee
const employeeEmail = sheet.getRange(row, 3).getValue();
GmailApp.sendEmail(employeeEmail, "Update: Your Expense Request was " + newStatus, "Your request has been officially " + newStatus + " by your manager.");
return HtmlService.createHtmlOutput("<h2 style='color:green;'>Success! The request has been marked as " + newStatus + ".</h2>");
}
Step 3: Deploying as a Web App
- Click Deploy > New deployment.
- Select type: Web app.
- Execute as: Me (your email).
- Who has access: Anyone (this allows managers to click links from mobile email clients without re-authenticating).
- Copy the Web App URL and paste it into the
webAppUrlvariable in your script.