Google Sheets Automation: Surviving the Second Week
Macros, Apps Script, and Zapier all get a sheet automation working on day one. None of the guides covers day eight, when a re-run duplicates every row it already processed and nobody finds out for a week. Here is what a Sheets automation needs to keep running.

The four ways to automate Google Sheets
There are four, in rough order of how much you have to learn: formulas that recalculate themselves, the built-in macro recorder, Apps Script with installable triggers, and an external tool calling the Sheets API. Pick by how much the automation needs to survive without you watching it.
Formulas are automation, even though nobody calls them that. ARRAYFORMULA calculates down a column as rows arrive, QUERY filters and sorts untouched, IMPORTRANGE pulls from another spreadsheet on its own. If the task is "this column should always show that calculation," stop here.
The macro recorder sits under Extensions, Macros, Record macro. It watches you format cells or sort a range and writes the Apps Script for what it saw. Google asks you to pick absolute or relative references, described in its own help page as acting on "the exact cell you record" versus "the cell you select and its nearby cells." No warning comes attached.
Apps Script is the real thing underneath: JavaScript with a Sheets binding and installable triggers that run as often as every minute or as rarely as once a month. Timing is randomized, so a 9 AM daily trigger gets a slot between 9 and 10.
External tools call the Sheets API from outside the spreadsheet: Zapier, Make, Pipedream, or your own code. This is the only approach where the logic and memory live outside the file it operates on.
| Approach | What it is good for | Runs unattended? | Survives a structure change? | Where state lives | | --- | --- | --- | --- | --- | | Formulas | Derived columns, filtered views, pulling another file's range | Yes, recalculates itself | Mostly. Inserts shift cleanly, a renamed header breaks QUERY | In the sheet | | Macro recorder | Repeating a click sequence you watch | Only with a trigger attached | No. Absolute references address coordinates, so an inserted column misdirects it | Nowhere | | Apps Script | Scheduled jobs, custom menus, event handlers | Yes, via installable trigger | Only if written to address columns by header | Nowhere by default. Add a column or use PropertiesService | | External tool on the API | Cross-system sync, validation, enrichment | Yes | Only if it keys rows instead of coordinates | In the tool's own store, if it has one |
What Sheets is and is not built for
A spreadsheet cell is addressed by coordinate. That is the whole design, and it is why Sheets is good at what it does. It also means a row has no identity. Sort the sheet and row 47 is now row 12. Insert a column and every reference to column D points at the wrong data. There is no primary key to deduplicate against unless you add one.
There are also hard ceilings: 10 million cells and 18,278 columns per spreadsheet. Teams hit the cell limit by accident, usually with a formula applied to an empty million-row range. If you are near it, consider when the data should live in a real database.
When an AI agent over Sheets actually helps
Three patterns come up repeatedly, and all share a shape. The repeatable part is mechanical, one small part needs judgment.
Inbound row validation and enrichment. New rows land from a form, an import, or a partner file. Each gets checked against required fields and formats, then enriched from a system of record: company size from HubSpot, account owner from Salesforce. Most rows are unambiguous. A handful are not, and that is where a model earns its cost. See enriching records from the CRM.
Reporting handoff. A sheet is where a team reads numbers. An agent reads the underlying systems, writes current figures into the tab, and summarizes what moved and why, which a formula cannot do. Posting the digest to Slack is what teams ask for first.
Cross-system sync. The sheet is a staging area between two systems that do not talk. Rows in, records out to Notion, Airtable, or a CRM, with a record of what moved. This is the same problem in Airtable.
What you need to build one
The Sheets API in 200 words
Authenticate with OAuth and request the narrowest scope that works. For values you want https://www.googleapis.com/auth/spreadsheets, or spreadsheets.readonly if the automation never writes. Where a Drive scope is also needed, Google's scope reference labels drive.file recommended and non-sensitive, granting access to "only the specific Google Drive files you use with this app," while plain drive is restricted and covers "all of your Google Drive files." One spreadsheet versus everything the user owns.
The surface you will actually use is small. values.get and batchGet read ranges, values.update and batchUpdate write them, values.append adds rows after a table, and spreadsheets.batchUpdate handles structural work. Writes take a required valueInputOption: RAW stores your string as typed, so =1+2 stays text, while USER_ENTERED parses it as the UI would.
Rate limits, verified 16 September 2026 against the usage limits page: 300 read and 300 write requests per minute per project, 60 per minute per user, no daily cap. Over quota returns 429, and Google recommends truncated exponential backoff capped at "typically 32 or 64 seconds."
Pros and cons of the Sheets API
It is a clean REST API with good client libraries, and a batch request counts as one call against quota. Now the parts that bite.
There is no change webhook. The only push signal is a Drive file-change notification, which says a file changed, carries no row detail, and expires unless renewed. Every "instant new row" trigger, in every tool, is snapshot diffing the sheet against stored state on a polling interval. Pipedream's Sheets triggers default to 15 minutes and warn you to narrow the monitored range above 1,000 rows. That is not a knock on those tools. It is the only mechanism the platform allows, and it is why remembering what you processed is foundational.
batchUpdate is all or nothing. The limits page states it plainly: "Invalid requests cause the entire update to fail." One malformed value fails every row in the batch, so validation has to happen before the write.
values.append overwrites by default. It scans your range for a table, then writes into the rows after it, overwriting whatever sits there. Set insertDataOption=INSERT_ROWS instead. A blank spacer row above a notes block is enough to eat data silently. Reads have a matching trap: trailing empty cells are omitted, so a row of 8 columns with 3 blank comes back with 5 entries.
A worked example: the form-response handoff agent
What it does
A form writes responses into a tab. New rows get validated, valid ones enriched against the CRM and written back, failures sent to a quarantine tab with the reason, and a scheduled Slack digest reports counts of processed, quarantined, and failed.
How the steps wire together
- Scope the credentials first.
drive.fileon the one spreadsheet, read-only on the CRM, write on a single Slack channel. - Read only the rows past your marker. Store the last completed row position outside the sheet, then read forward.
- Validate every row in code, before any write. Required fields, email format, date parsing. Deterministic, no model involved.
- Enrich the valid rows against the CRM. Batch the lookups. Here a model handles the ambiguous case, a company name matching three accounts, flagging it instead of guessing.
- Write back in one batch, then advance the marker. Only completed rows move it forward. Failures go to quarantine.
- Post the digest, and alert on the run itself. The digest reports rows. A separate alert reports whether the run happened.
A scheduled run, not an event trigger. There is no change webhook, so polling is what "instant" means.
What breaks in week two
The re-run duplicates everything. The script restarts, or someone runs it by hand, and with nothing recording what was handled it processes row 1 again. Every CRM record gets a second write. Fix: a durable marker, advanced only after a row completes.
One bad row kills the batch. A date that will not parse, and batchUpdate rejects all 200 writes. Rows 1 through 46 looked like they succeeded and did not. Fix: validate first, quarantine failures, write the rest.
The credential can read the entire Drive. Someone granted drive during setup because it was the first option that worked. Fix: drive.file, re-authorized against the single file.
It stops on a Tuesday and nobody notices. Apps Script emails a "Summary of failures for Apps Script" notice from noreply-apps-scripts-notifications@google.com to whoever created the trigger. That person may have left, or may filter it. Worse, a run that completes while processing zero rows produces no failure at all. Fix: a heartbeat in the digest, so a missing digest is the alarm.
What governance the agent needs
Two things. Each credential scoped to the narrowest access that lets its step work. And a record of which rows were touched, when and by which run, with the quarantine reason, so someone can say later why row 412 never reached the CRM.
Build this in Major
Once an automation has to remember what it processed, quarantine a bad row, retry safely, and report on itself, what you have is a small application with state. The macro recorder cannot hold that, and neither can a Zap. Apps Script and Zapier are good at day one. This is day eight.
On Major, you describe the handoff agent and it builds the app that runs it. The processed-row marker and the quarantine log live in a managed database rather than a cell somewhere. The validation rules run as deterministic code, so the model is not re-deciding what a valid email looks like 200 times an hour. It stays for the ambiguous row, the company name matching three accounts. Sheets is the data surface, the CRM read-only, Slack takes the digest, and each credential is scoped where the agent acts, with every row attributable afterwards. This is one case of agentic workflow patterns and what agentic automation actually means. The agent reasons once about this handoff, then the app runs it forever.
If your sheet automation is a task someone watches, keep the formula. If it runs unattended and you cannot say what it did last Tuesday, it needs a marker, a quarantine tab, and an alert, and those belong in an app rather than a script. Build your form-response handoff agent on Major.
Related articles
Frequently asked questions
- Is there a way to automate Google Sheets to automatically sort data?
- Yes. Select a range, then use Data, Sort range to sort on demand. For sorting that maintains itself, wrap your data in a QUERY or SORT formula on a second tab, which re-sorts as rows arrive. For scheduled sorting of the source tab, an Apps Script time-driven trigger can call the sort each morning. The same three tiers apply to most Sheets automation.
- Is there an AI that helps with Google Sheets?
- Yes, in two forms. Gemini features inside Google Workspace can draft formulas and summarize a range from a prompt. Separately, agents work on a sheet through the Sheets API, reading rows, validating them, enriching them against other systems, and writing results back. The first helps you author a spreadsheet. The second runs unattended work against one on a schedule.
- How can I pull data from one Google sheet to another automatically?
- Use IMPORTRANGE in the destination sheet, pointing at the source spreadsheet URL and range, and authorize the connection once. It refreshes on its own. IMPORTRANGE cannot validate what arrives, log what it pulled, or run on a fixed schedule. When you need any of those, read the source through the Sheets API and write the destination yourself.
- How do you stop a Google Sheets automation from processing the same row twice?
- Record what you already handled, because Sheets rows have no stable identity. A cell is addressed by coordinate, so a sort or an inserted row shifts every reference and there is no natural key to deduplicate on. Either add a processed-at column the automation writes and filters on, or store the last completed position outside the sheet and advance it only after a row finishes.
- How do you know when a Google Sheets automation stops working?
- By default you mostly do not. Apps Script emails a failure summary to whoever created the trigger, which is easy to filter or lose when that person changes roles, and a run that completes while processing zero rows reports no failure at all. A real alert path inverts this: have the job post a heartbeat on every run, and alert when the heartbeat is missing.