Build a live dashboard of form responses
Visualise Northwind submissions as they arrive — counts, splits, sentiment.
Published Sep 19, 2025
A Google Form fills its linked sheet one row at a time, and that raw sheet is fine for the record but useless at a glance. When Northwind runs a sign-up form or a feedback survey, someone keeps opening the responses tab to count rows and tally categories by eye — a task that gets less reliable the busier the form gets.
This script reads the responses sheet and rebuilds a small dashboard tab: the total number of submissions and a breakdown by category. Pointed at a form- submit trigger, it refreshes itself every time a new response lands, so the dashboard is always current without anyone touching it.
What you’ll need
- A Google Form linked to a responses spreadsheet. The first sheet of that
spreadsheet must include a
Categorycolumn — the script splits the counts on it. - A second Google Sheet (or a second tab) for the dashboard. The script clears and rewrites its first sheet, so keep nothing else there.
- The two file IDs, pasted into the constants at the top of the script.
The script
// The spreadsheet the form writes responses into.
const RESPONSES_SHEET_ID = '1abcResponsesId';
// The spreadsheet the dashboard is rebuilt in.
const DASHBOARD_SHEET_ID = '1abcDashboardId';
/**
* Reads the form responses and rebuilds the dashboard sheet with a
* total submission count and a breakdown by category.
*/
function rebuildDashboard() {
const responses = SpreadsheetApp.openById(RESPONSES_SHEET_ID)
.getSheets()[0].getDataRange().getValues();
// Bail out early if there are no responses yet (header row only).
if (responses.length < 2) {
Logger.log('No responses yet — nothing to build.');
return;
}
// 1. Split off the header and map header names to column indexes.
const [header, ...rows] = responses;
const col = Object.fromEntries(header.map((key, i) => [key, i]));
// 2. The headline number: how many responses in total.
const total = rows.length;
// 3. Tally responses by their Category value.
const byCategory = {};
for (const r of rows) {
const category = r[col.Category] || 'unknown';
byCategory[category] = (byCategory[category] || 0) + 1;
}
// 4. Clear the dashboard and write the total and the breakdown.
const dash = SpreadsheetApp.openById(DASHBOARD_SHEET_ID).getSheets()[0];
dash.clear();
dash.getRange(1, 1, 1, 2).setValues([['Metric', 'Value']]);
dash.getRange(2, 1, 1, 2).setValues([['Total', total]]);
const categoryRows = Object.entries(byCategory);
dash.getRange(4, 1, 1, 2).setValues([['Category', 'Count']]);
dash.getRange(5, 1, categoryRows.length, 2).setValues(categoryRows);
Logger.log('Dashboard rebuilt from ' + total + ' responses.');
}
How it works
rebuildDashboardopens the responses spreadsheet and reads its first sheet withgetDataRange. If there is nothing but a header row it logs and stops.- It splits off the header and builds a
collookup from header name to index, so aCategorycolumn can sit anywhere without breaking the script. - The total is simply the number of data rows — the headline figure for the top of the dashboard.
- It walks every response and tallies the
Categorycolumn into abyCategoryobject. A blank category falls back to'unknown'so no response is silently lost from the count. - It clears the dashboard sheet and writes two small blocks: a total row near
the top, then a
Category/Counttable starting at row 5 with one row per distinct category. - Because the whole dashboard is rebuilt each run, it always reflects the current state of the responses — categories that disappear drop off, new ones appear automatically.
Example run
Say the responses sheet has collected eight submissions:
| Timestamp | Name | Category |
|---|---|---|
| … | Priya | Bug |
| … | Sam | Feature |
| … | Alex | Bug |
| … | (5 more) | … |
After a run, the dashboard sheet reads:
| Metric | Value |
|---|---|
| Total | 8 |
| Category | Count |
| Bug | 4 |
| Feature | 3 |
| unknown | 1 |
One response had no category and is counted under unknown rather than dropped.
Trigger it
Wire it to a form-submit trigger so the dashboard refreshes itself:
- In the Apps Script editor open Triggers (the clock icon).
- Add Trigger → choose
rebuildDashboard, event source From spreadsheet, event type On form submit. - Save and approve the authorisation prompt.
If you would rather not rebuild on every single submission, use a Time-driven trigger every 5 minutes instead — a small lag in exchange for fewer runs.
Watch out for
- The script keys on a column literally named
Category. If the form question is worded differently, the header will not match — rename the column or update thecol.Categorylookup to the real header text. dash.clear()wipes the dashboard’s first tab completely, including any chart or formatting. Build charts on a separate tab that reads from this one.- A form-submit trigger fires once per response. A burst of submissions means a burst of full rebuilds — fine for a normal form, but switch to a timed trigger if the form is high-volume.
- Category counts are exact string matches.
Bugandbugwith a trailing space count as two categories — trim or normalise the value if responses are free-typed rather than chosen from a list. - The dashboard is only as fresh as the last trigger run. If you disable the
trigger, the numbers freeze — run
rebuildDashboardby hand to catch up.
Related
Trigger an onboarding sequence on form submit
Kick off tasks when a new Northwind hire submits their starter form.
Updated Oct 17, 2025
Build a content-submission queue
Collect Northwind guest posts or ideas for review through a Form.
Updated Oct 9, 2025
Score sentiment in open-text feedback
Rate Northwind feedback comments without manual review — using the in-Sheet sentiment function.
Updated Oct 5, 2025
Build a peer-nomination and voting system
Collect and tally Northwind nominations for awards or initiatives — one ballot, anonymous.
Updated Oct 1, 2025
Roll a form over each cycle
Archive old responses and reset for the next Northwind cycle — quarterly OKR check-ins.
Updated Sep 27, 2025