Integration4 min read2026-07-31

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.

Step 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:

Code.gs — poll runs, append rows
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.

FAQ

Direct answers to the questions this page raises.

A live fetch can hold the connection up to 180 seconds while six engines answer, and long-held requests are exactly where Apps Script executions get fragile. The schedule-plus-poll pattern gives you the same records through reads that finish in moments, every time the trigger fires.

Column A holds the run ID, the script loads that column into a set first, and any run already present is skipped. Overlapping triggers or manual re-runs are harmless — each engine answer lands exactly once.

No. GET /v1/runs and GET /v1/runs/{id} are free reads. Credits meter only the schedule's delivered records — one per engine answer, zero for failures — billed by consumption under a spend cap.

Yes — loop over a.sources in the inner loop and append one row per source with its title, URL and position. That shape makes citation-level pivots (which domains does each engine trust for your category?) a one-click report.

Keep reading

Where this page leads next.

Run these checks on your own brand

Two ways in. Send a URL and a person runs the fetches for you, free — or connect your agent over MCP, on a plan, and run them yourself. Either way you get the raw answers and their citations, never a score.