Auto-rebuild grouped summaries on edit
Refresh pivot-style rollups the instant data changes — no manual recompute.
Published Jan 4, 2026
Northwind tracks studio spending on an Expenses tab — one row per cost, with
a category against each. The team wants a live rollup of total spend by
category, but a pivot table has to be refreshed by hand and a SUMIF block
needs maintaining as new categories appear. Either way, the summary is only as
fresh as the last time someone remembered to update it.
This script keeps the rollup honest. Every time a cell on the Expenses tab
changes, it recomputes the per-category totals from scratch and rewrites a
Rollup tab. There is nothing to refresh and nothing to drag down — the
summary is correct the moment the data is.
What you’ll need
- A Google Sheet with an
Expensestab that has a header row including acategorycolumn and anamountcolumn. - The
amountcolumn should hold plain numbers, not text — the script adds them up directly. - The script creates the
Rolluptab itself on the first run, so you do not need to make it in advance.
The script
// Tab names — the source of expense rows and the summary destination.
const SOURCE_SHEET = 'Expenses';
const ROLLUP_SHEET = 'Rollup';
/**
* Installable onEdit trigger. Fires on every edit to the spreadsheet,
* but only rebuilds the rollup when the Expenses tab changed.
*/
function onEdit(e) {
// Guard: a manual run has no event object, so there is nothing to do.
if (!e || !e.range) return;
// Ignore edits to any tab other than Expenses.
if (e.range.getSheet().getName() !== SOURCE_SHEET) return;
rebuildCategoryRollup();
}
/**
* Reads the Expenses tab, totals the amounts by category, and
* rewrites the Rollup tab from scratch.
*/
function rebuildCategoryRollup() {
const ss = SpreadsheetApp.getActive();
const values = ss.getSheetByName(SOURCE_SHEET).getDataRange().getValues();
// Split the header off and map column names to their indexes.
const [header, ...rows] = values;
const col = Object.fromEntries(header.map((k, i) => [k, i]));
// Sum the amounts into a { category: total } map.
const totals = {};
for (const row of rows) {
const category = row[col.category];
if (!category) continue; // skip rows with no category
totals[category] = (totals[category] || 0) + row[col.amount];
}
// Rebuild the Rollup tab from scratch on every edit.
const out = ss.getSheetByName(ROLLUP_SHEET) || ss.insertSheet(ROLLUP_SHEET);
out.clear();
out.getRange(1, 1, 1, 2).setValues([['Category', 'Total']]);
// Write one row per category, if there is anything to write.
const data = Object.entries(totals);
if (data.length) {
out.getRange(2, 1, data.length, 2).setValues(data);
}
}
How it works
onEditis an installable trigger that runs on every edit to the spreadsheet. It first guards against a manual run, where there is no event object.- It checks which tab the edit happened on and returns immediately unless it
was the
Expensestab — so edits elsewhere cost nothing. - When the
Expensestab did change, it callsrebuildCategoryRollup. rebuildCategoryRollupreads the wholeExpensestab, splits off the header row, and builds acollookup so columns are referenced by name.- It loops the rows, skipping any with no category, and accumulates each
amountinto atotalsmap keyed by category. - It finds or creates the
Rolluptab, clears it, and writes a fresh header followed by one row per category — so the rollup always reflects the current data with no stale rows left behind.
Example run
The Expenses tab:
| date | category | amount |
|---|---|---|
| 2026-01-02 | Software | 120 |
| 2026-01-02 | Travel | 340 |
| 2026-01-03 | Software | 80 |
| 2026-01-03 | Catering | 55 |
The moment any of those cells is edited, the Rollup tab is rewritten:
| Category | Total |
|---|---|
| Software | 200 |
| Travel | 340 |
| Catering | 55 |
Add a new Travel row for 60 and Travel jumps to 400 instantly — no
refresh.
Trigger it
A simple onEdit function cannot reliably write to other sheets, so this needs
an installable trigger:
- In the Apps Script editor open Triggers (the clock icon).
- Add a trigger for
onEdit, choose From spreadsheet as the source and On edit as the event type. - Approve the authorisation prompt. The rollup now rebuilds on every edit to
the
Expensestab.
Watch out for
onEdittriggers have a short runtime limit. A full rebuild is fine for hundreds or low thousands of rows; for very large sheets, recompute on a timer instead so a slow rebuild does not block editing.- The script overwrites the
Rolluptab entirely on every run. Do not add notes or extra formulas there — they will be wiped. - Non-numeric values in the
amountcolumn will produce odd totals (string concatenation rather than addition). Keep the column formatted as numbers. - A rapid burst of edits triggers a rebuild for each one. That is usually harmless, but for heavy paste operations consider debouncing or moving to a scheduled rebuild.
- Installable triggers run as the user who created them, not the user editing. Make sure that account has access to every tab involved.
Related
Build an OKR tracker with progress rollups
Aggregate Northwind's key-result progress into objective-level scores automatically.
Updated Jan 14, 2026
Build a recurring-task generator
Spawn new Northwind task rows on a daily or weekly cadence from a Recurring sheet.
Updated Jan 10, 2026
Build a multi-sheet search-and-jump tool
Find a value across every tab of a workbook and click through to the cell that contains it.
Updated Jan 7, 2026
Build a data-quality scorecard
Grade any sheet on completeness, validity, and freshness — surface gaps as a single score.
Updated Dec 31, 2025
Generate a warehouse pick or packing list
Format Northwind order rows into a print-ready pick list grouped by aisle.
Updated Dec 27, 2025