Enterprise spreadsheet automation and web application framework
A collection of production-grade Google Apps Script templates demonstrating enterprise workflow automation, custom UI development, and Google Workspace integration. These templates showcase how to build powerful business applications using Google Sheets as a database with custom web interfaces.
Real-world pattern: Automated workflows processing dozens of requests daily, custom UIs for data entry, triggered email notifications, and real-time dashboard updates.
- β HtmlService Dialogs - Modal and modeless custom interfaces
- β Web Apps - Standalone web applications
- β Custom Sidebars - Integrated side panels
- β Custom Menus - Toolbar extensions
- β Time-based Triggers - Scheduled automation
- β Event-based Triggers - OnEdit, OnFormSubmit, etc.
- β Email Automation - Templated email sending
- β Data Processing - Automated data transformation
- β CRUD Operations - Create, Read, Update, Delete records
- β Data Validation - Input validation and constraints
- β Search & Filter - Advanced data querying
- β Export Capabilities - Multiple format exports
- β Google Sheets API - Programmatic sheet manipulation
- β Gmail Integration - Email sending and templates
- β Calendar Integration - Event creation and management
- β External APIs - UrlFetchApp for REST APIs
- Google Apps Script - Server-side JavaScript
- HTML5 - Custom UI templates
- CSS3 - Styling for web apps
- JavaScript - Client-side interactivity
- Google Services - Sheets, Gmail, Calendar, Drive
- Google Account
- Google Sheets access
- Basic JavaScript knowledge
- Go to script.google.com
- Create new project
- Copy template code
- Save and authorize
- Open your Google Sheet
- Extensions β Apps Script
- Copy template code
- Save and authorize
Interactive data entry form with validation
// Code.gs
function showCustomForm() {
const html = HtmlService.createHtmlOutputFromFile('FormDialog')
.setWidth(400)
.setHeight(500)
.setTitle('Data Entry Form');
SpreadsheetApp.getUi().showModalDialog(html, 'Submit Request');
}
function submitFormData(formData) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Data');
// Validate data
if (!validateFormData(formData)) {
return { success: false, error: 'Validation failed' };
}
// Append to sheet
sheet.appendRow([
new Date(),
formData.name,
formData.email,
formData.category,
formData.description,
'Pending'
]);
// Send confirmation email
sendConfirmationEmail(formData.email, formData);
return { success: true };
}
function validateFormData(data) {
if (!data.name || data.name.trim() === '') return false;
if (!data.email || !isValidEmail(data.email)) return false;
if (!data.category) return false;
return true;
}
function isValidEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}<!-- FormDialog.html -->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body {
font-family: 'Google Sans', Arial, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
input, select, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
textarea {
resize: vertical;
min-height: 100px;
}
.btn-submit {
background-color: #1a73e8;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
width: 100%;
font-size: 14px;
}
.btn-submit:hover {
background-color: #1557b0;
}
.error {
color: #d93025;
font-size: 12px;
margin-top: 5px;
}
.success {
color: #1e8e3e;
font-size: 14px;
padding: 10px;
background-color: #e6f4ea;
border-radius: 4px;
margin-bottom: 15px;
}
</style>
</head>
<body>
<div id="message"></div>
<form id="dataForm">
<div class="form-group">
<label for="name">Name *</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email *</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="category">Category *</label>
<select id="category" name="category" required>
<option value="">Select...</option>
<option value="Type A">Type A</option>
<option value="Type B">Type B</option>
<option value="Type C">Type C</option>
</select>
</div>
<div class="form-group">
<label for="description">Description *</label>
<textarea id="description" name="description" required></textarea>
</div>
<button type="submit" class="btn-submit">Submit</button>
</form>
<script>
document.getElementById('dataForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = {
name: document.getElementById('name').value,
email: document.getElementById('email').value,
category: document.getElementById('category').value,
description: document.getElementById('description').value
};
// Show loading state
const submitBtn = document.querySelector('.btn-submit');
submitBtn.textContent = 'Submitting...';
submitBtn.disabled = true;
// Submit to server
google.script.run
.withSuccessHandler(onSuccess)
.withFailureHandler(onFailure)
.submitFormData(formData);
});
function onSuccess(response) {
if (response.success) {
document.getElementById('message').innerHTML =
'<div class="success">β Submitted successfully!</div>';
document.getElementById('dataForm').reset();
} else {
document.getElementById('message').innerHTML =
'<div class="error">Error: ' + response.error + '</div>';
}
resetButton();
}
function onFailure(error) {
document.getElementById('message').innerHTML =
'<div class="error">Error: ' + error.message + '</div>';
resetButton();
}
function resetButton() {
const submitBtn = document.querySelector('.btn-submit');
submitBtn.textContent = 'Submit';
submitBtn.disabled = false;
}
</script>
</body>
</html>// Code.gs
function sendConfirmationEmail(recipientEmail, data) {
const subject = `Confirmation: ${data.category} Request`;
const htmlBody = `
<div style="font-family: Arial, sans-serif; max-width: 600px;">
<h2 style="color: #1a73e8;">Request Confirmation</h2>
<p>Hello ${data.name},</p>
<p>We've received your request with the following details:</p>
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0;">
<p><strong>Category:</strong> ${data.category}</p>
<p><strong>Description:</strong> ${data.description}</p>
<p><strong>Status:</strong> Pending Review</p>
<p><strong>Submitted:</strong> ${new Date().toLocaleString()}</p>
</div>
<p>We'll review your request and get back to you soon.</p>
<p style="color: #666; font-size: 12px; margin-top: 30px;">
This is an automated message. Please do not reply to this email.
</p>
</div>
`;
GmailApp.sendEmail(recipientEmail, subject, '', {
htmlBody: htmlBody,
name: 'Automated System'
});
}
function sendBulkNotifications() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Data');
const data = sheet.getDataRange().getValues();
const headers = data[0];
// Find columns
const emailCol = headers.indexOf('Email');
const statusCol = headers.indexOf('Status');
const notifiedCol = headers.indexOf('Notified');
// Process each row
for (let i = 1; i < data.length; i++) {
const row = data[i];
// Skip if already notified or not relevant status
if (row[notifiedCol] === 'Yes' || row[statusCol] !== 'Completed') {
continue;
}
// Send notification
try {
const email = row[emailCol];
sendStatusUpdateEmail(email, row, headers);
// Mark as notified
sheet.getRange(i + 1, notifiedCol + 1).setValue('Yes');
// Add delay to avoid rate limiting
Utilities.sleep(1000);
} catch (error) {
Logger.log(`Error sending to row ${i}: ${error}`);
}
}
}// Code.gs
function createDashboard() {
const html = HtmlService.createHtmlOutputFromFile('Dashboard')
.setWidth(800)
.setHeight(600)
.setTitle('Analytics Dashboard');
SpreadsheetApp.getUi().showModalDialog(html, 'Dashboard');
}
function getDashboardData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Data');
const data = sheet.getDataRange().getValues();
const headers = data[0];
// Calculate metrics
const metrics = {
total: data.length - 1,
byCategory: {},
byStatus: {},
recentItems: []
};
for (let i = 1; i < data.length; i++) {
const row = data[i];
const category = row[headers.indexOf('Category')];
const status = row[headers.indexOf('Status')];
// Count by category
metrics.byCategory[category] = (metrics.byCategory[category] || 0) + 1;
// Count by status
metrics.byStatus[status] = (metrics.byStatus[status] || 0) + 1;
// Recent items (last 10)
if (i <= 10) {
metrics.recentItems.push({
date: row[0],
name: row[1],
category: category,
status: status
});
}
}
return metrics;
}// Code.gs
function setupTriggers() {
// Remove existing triggers
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => ScriptApp.deleteTrigger(trigger));
// Daily report at 9 AM
ScriptApp.newTrigger('generateDailyReport')
.timeBased()
.atHour(9)
.everyDays(1)
.create();
// Weekly summary on Monday at 10 AM
ScriptApp.newTrigger('generateWeeklyReport')
.timeBased()
.onWeekDay(ScriptApp.WeekDay.MONDAY)
.atHour(10)
.create();
// OnEdit trigger
ScriptApp.newTrigger('onEditHandler')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
}
function onEditHandler(e) {
const sheet = e.source.getActiveSheet();
const range = e.range;
// Example: Auto-timestamp on status change
if (sheet.getName() === 'Data' && range.getColumn() === 6) { // Status column
const timestampCol = 7; // Adjacent column
sheet.getRange(range.getRow(), timestampCol)
.setValue(new Date())
.setNumberFormat('yyyy-mm-dd hh:mm:ss');
// Send notification if status is "Completed"
if (range.getValue() === 'Completed') {
const row = sheet.getRange(range.getRow(), 1, 1, sheet.getLastColumn()).getValues()[0];
const email = row[2]; // Email column
sendStatusUpdateEmail(email, 'Your request has been completed!');
}
}
}// Code.gs
function fetchExternalData() {
const apiUrl = 'https://api.example.com/data';
const apiKey = PropertiesService.getScriptProperties().getProperty('API_KEY');
const options = {
method: 'get',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(apiUrl, options);
const responseCode = response.getResponseCode();
if (responseCode === 200) {
const data = JSON.parse(response.getContentText());
processExternalData(data);
return { success: true, data: data };
} else {
throw new Error(`API returned status: ${responseCode}`);
}
} catch (error) {
Logger.log(`Error fetching data: ${error}`);
return { success: false, error: error.message };
}
}
function processExternalData(data) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('API Data');
sheet.clear();
// Write headers
const headers = Object.keys(data[0]);
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
// Write data
const rows = data.map(item => headers.map(header => item[header]));
sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);
}Google Sheets (Database)
β
Apps Script (Backend)
βββ Custom Functions
βββ Event Handlers
βββ API Clients
βββ Automation Logic
β
HtmlService (Frontend)
βββ Custom Dialogs
βββ Web Apps
βββ Sidebars
β
External Integrations
βββ Gmail
βββ Calendar
βββ Drive
βββ External APIs
- Workflow Automation - Request tracking and approval workflows
- Data Entry Forms - Custom interfaces for structured data collection
- Automated Reporting - Scheduled report generation and distribution
- Email Campaigns - Bulk email sending with templates
- Dashboard Creation - Real-time analytics and visualization
- API Integration - Connect external services to Sheets
// Use Script Properties for sensitive data
function setApiKey() {
PropertiesService.getScriptProperties().setProperty('API_KEY', 'your_key_here');
}
function getApiKey() {
return PropertiesService.getScriptProperties().getProperty('API_KEY');
}// Check user permissions
function isAuthorizedUser() {
const authorizedEmails = ['[email protected]', '[email protected]'];
const userEmail = Session.getActiveUser().getEmail();
return authorizedEmails.includes(userEmail);
}MIT License
These templates demonstrate:
- Enterprise workflow automation with Google Workspace
- Custom UI development with HtmlService
- Event-driven programming with triggers
- API integration patterns
- Production-grade error handling
Author: Toussaint Rwego
GitHub: @Rweg