Overview
This project implements a rule-based tagging engine using Google Apps Script that processes spreadsheet records by scanning multiple text columns, detecting prioritized keywords, mapping them to predefined process conditions, and automatically writing the results to target columns.
The goal was to replace manual, error-prone visual checks with a data-driven and reproducible workflow, where business rules are maintained in spreadsheets while execution is fully automated.
Problem
- Decision-relevant information was scattered across multiple columns (D/E/F/I/J/O)
- Business rules kept growing and became:
- Error-prone when handled manually
- Slow to execute
- Difficult for new team members to understand
- A strict rule priority was required:
once the highest-priority keyword matches, no further checks should be performed
Goal
- Scan multiple columns in the
homepagesheet for keywords - Evaluate keywords in priority order defined in
keyList - Resolve matched keywords into process conditions using
ProcessList - Automatically write output values to Q and R columns
- Stop processing a row immediately after the first valid match
Design (Data-Driven Rules)
Sheets
- homepage
Source data. Relevant text values are distributed across several columns. - keyList
Mapskeyword → processName- Priority is implicitly controlled by row order
- ProcessList
MapsprocessCondition → output values (B/C)- Determines what values should be written once a process is identified
Implementation Highlights
- Multi-column scanning
Ensures that relevant signals are detected regardless of where they appear - Priority-based matching
Keywords are evaluated top-down, stopping at the first match - Rule externalization
Business logic is managed in sheets instead of hardcoded values - Fail-safe checks
Script exits gracefully if required sheets are missing
Code (Google Apps Script)
/**
* Rule-based tagging:
* - Scan multiple columns in `homepage` for prioritized keywords (top-down in `keyList`)
* - Resolve keyword -> processName -> processCondition -> (Q, R output values)
* - Write outputs to homepage columns Q/R in batch
*/
function processConditionsOptimized() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const wsHome = ss.getSheetByName("homepage");
const wsKey = ss.getSheetByName("keyList");
const wsProcess = ss.getSheetByName("ProcessList");
if (!wsHome || !wsKey || !wsProcess) {
Logger.log("Required sheet not found.");
return;
}
const homeRange = wsHome.getDataRange();
const homeData = homeRange.getValues(); // includes header row
const keyData = wsKey.getDataRange().getValues();
const processData = wsProcess.getDataRange().getValues();
// Columns to scan in homepage (0-based): D,E,F,I,J,O
const scanColIdx = [3, 4, 5, 8, 9, 14];
// Output columns: Q and R (1-based for Range)
const OUT_Q_COL = 17;
const OUT_R_COL = 18;
// Build a lookup: processCondition -> [valueForQ, valueForR]
// Assumption: ProcessList columns: A=condition, B=Q value, C=R value
const processLookup = new Map();
for (let i = 1; i < processData.length; i++) {
const cond = normalize(processData[i][0]);
if (!cond) continue;
processLookup.set(cond, [processData[i][1], processData[i][2]]);
}
// Normalize key rules (priority is row order in the sheet)
// Assumption: keyList columns: A=keyword, B=processName
const keyRules = [];
for (let i = 1; i < keyData.length; i++) {
const keyword = normalize(keyData[i][0]);
const processName = normalize(keyData[i][1]);
if (!keyword || !processName) continue;
keyRules.push({ keyword, processName });
}
// Prepare batched outputs for Q/R (exclude header row)
const outQR = Array.from({ length: Math.max(0, homeData.length - 1) }, () => ["", ""]);
// Main loop over homepage rows (skip header)
for (let row = 1; row < homeData.length; row++) {
const match = findFirstMatchForRow_(homeData[row], scanColIdx, keyRules, processLookup);
if (match) {
outQR[row - 1][0] = match[0]; // Q
outQR[row - 1][1] = match[1]; // R
}
}
// Batch write Q/R results in a single call
if (outQR.length > 0) {
wsHome.getRange(2, OUT_Q_COL, outQR.length, 2).setValues(outQR);
}
SpreadsheetApp.getUi().alert("Processing completed.");
}
/**
* Returns [valueForQ, valueForR] for the first match in the row, or null.
* Priority:
* 1) scan columns in order (D,E,F,I,J,O)
* 2) within a cell, apply keyRules top-down (keyList order)
* 3) resolve processName by checking which processCondition it includes (ProcessList)
*/
function findFirstMatchForRow_(rowValues, scanColIdx, keyRules, processLookup) {
for (const col of scanColIdx) {
const cellText = normalize(rowValues[col]);
if (!cellText) continue;
for (const rule of keyRules) {
if (!cellText.includes(rule.keyword)) continue;
// Resolve processName -> processCondition -> output values
for (const [cond, out] of processLookup.entries()) {
if (rule.processName.includes(cond)) return out;
}
// If keyword matched but no processCondition matched, continue searching
}
}
return null;
}
/** Normalize values to safe comparable strings. */
function normalize(v) {
if (v === null || v === undefined) return "";
return String(v).trim();
}
Impact
- Replaced manual tagging with a fully automated, repeatable process
- Reduced operational errors caused by subjective judgment and rule misinterpretation
- Enabled non-engineers to maintain business rules directly in spreadsheets
- Encoded priority logic explicitly, ensuring consistent decision-making
Next Improvements
- Batch write results using setValues() to reduce execution time
- Support regular expressions for more flexible keyword matching
- Add execution logs (matched rules, processed rows) for auditing and monitoring