AI visibility in Google Sheets
The most durable GEO monitoring artifact might be a spreadsheet: one row per engine answer per week, forever. It's diffable, chartable, shareable, and it outlives every tool decision you'll make. Google Sheets plus a time-driven Apps Script gets you exactly that — with AgentGEO supplying the rows.
The wiring keeps the slow part out of Apps Script. A server-side AgentGEO schedule scrapes ChatGPT, Perplexity, Gemini, Google AI Overviews, Google AI Mode and Copilot on cadence; your script polls the fast read endpoint and appends whatever's new. No add-on to install, no long-held requests to worry about — two GETs and appendRow.
Read this page with an AI
Each appended row carries the run ID, query, engine, the verbatim answer, and every cited URL — enough to compute mention rates, share of voice and citation churn with plain spreadsheet formulas.
Get a free AI-visibility audit → · Read the docs → — No card, no account.
Get my free auditStep 1: A schedule on the AgentGEO side
Create it once — in the console's Schedules page or with a single POST /v1/schedules call carrying your query, surfaces and cadence (hourly, daily or weekly). The engine answers then accumulate in run history server-side, no matter what your script does.
Step 2: The Apps Script
In your sheet: Extensions → Apps Script, paste the function, and add a time-driven trigger (say, hourly). The script is idempotent — it skips runs already filed by checking the Run ID column:
const KEY = "ag_live_..."; // Script properties are the better home for this
const API = "https://api.agentgeo.org";
function pollAgentGeoRuns() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("AI answers");
const filed = new Set(
sheet.getRange(2, 1, Math.max(sheet.getLastRow() - 1, 1), 1)
.getValues().flat().filter(String),
);
const headers = { Authorization: "Bearer " + KEY };
const list = JSON.parse(
UrlFetchApp.fetch(API + "/v1/runs?limit=10", { headers }).getContentText(),
);
for (const summary of list.runs) {
if (summary.status !== "completed" || filed.has(summary.id)) continue;
const run = JSON.parse(
UrlFetchApp.fetch(API + "/v1/runs/" + summary.id, { headers }).getContentText(),
);
for (const a of run.answers) {
if (a.status !== "delivered") continue;
sheet.appendRow([
run.id,
run.query,
a.surfaceKey,
a.fetchedAt,
a.answerText,
a.sources.map((s) => s.url).join("\n"),
]);
}
}
}Both calls are quick reads that return in moments regardless of how slow the underlying scrape was — which is the point. A live multi-surface fetch can hold up to 180 seconds, and that's a wait you want on AgentGEO's side of the wire, not inside UrlFetchApp. Reads cost no credits; the schedule's delivered records cost one credit each (failed fetches are free).
Wire the sheet on an ag_test_ key first — labelled demo records at zero credits let you shape columns and formulas before the first paid run. Live ag_live_ keys are minted in the console and come with a plan.
Formulas worth adding
- Cited? column.
=IF(ISNUMBER(SEARCH("ourdomain.com", F2)), "yes", "no")against the cited-URLs cell — your citation record, one glance per row. - Share-of-voice pivot. Pivot engine × week, counting rows where each competitor's domain appears; the math behind it is in the share-of-voice use case.
- Mention-rate chart. A line chart over the weekly yes-rate per engine — the one image that makes GEO progress legible to people who'll never read JSON.
- Answer drift flag. Compare this week's answer text length and cited set to last week's; big deltas usually mean the engine re-sourced the answer and are worth reading by hand.
More integrations
Prefer records over rows? Airtable runs the same poll-and-file pattern with views on top. Prefer no code at all? Zapier can append the rows for you. See all integrations.
See what the first rows would say: Get a free AI-visibility audit → · Docs →
Get my free audit