n8n Google Sheets Trigger: Two-Way Sync Without Polling
Ready to automate?
Browse 5,600+ copy-paste n8n workflow templates.
Watching a Google Sheet for changes usually means polling it, checking every minute or five whether a row was added or edited, and hoping the delay does not matter. The built-in Google Sheets Trigger node in n8n works this way: it burns API quota on every check and adds latency between the edit and the workflow that reacts to it. This guide builds a different setup: a Google Apps Script installable trigger pushes edits to n8n instantly via webhook, and a matching write-back path keeps Airtable in sync in both directions.
What You'll Build
A two-way sync between a Google Sheet and an Airtable base:
- A Google Apps Script
onEdittrigger fires the moment a row changes - It posts the changed row to an n8n Webhook Trigger node
- n8n normalizes the payload and upserts the row into Airtable
- A separate path watches Airtable for external changes and writes them back to the Sheet
- A marker column prevents the two paths from triggering each other in a loop
Prerequisites
- An n8n instance (cloud or self-hosted) reachable from the internet so Google can call its webhook
- A Google Sheet you can attach an Apps Script project to (Extensions → Apps Script)
- An Airtable base and a Airtable API credential in n8n
- Comfort pasting a short Apps Script snippet into the Sheets script editor
The Workflow, Step by Step
Step 1: Add the Webhook Trigger in n8n
Create a new workflow and add a Webhook Trigger node set to POST. Copy its production URL, you will paste this into the Apps Script in the next step. This endpoint is what replaces polling: nothing calls it until a real edit happens.
Step 2: Push edits from Sheets with Apps Script
In the Sheet, open Extensions → Apps Script and add an installable onEdit trigger (the simple onEdit(e) function cannot make external network calls, so you need the installable version):
function onEditInstallable(e) {
const sheet = e.range.getSheet();
const row = e.range.getRow();
if (row === 1) return; // skip header edits
const rowData = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0];
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const payload = {};
headers.forEach((h, i) => (payload[h] = rowData[i]));
if (payload.synced_at) return; // this edit came from our own write-back, skip
UrlFetchApp.fetch("https://your-n8n-instance.com/webhook/sheet-sync", {
method: "post",
contentType: "application/json",
payload: JSON.stringify({ row, ...payload }),
});
}
In the Apps Script editor, go to Triggers (clock icon) → Add Trigger → choose onEditInstallable, event type "On edit." This is what makes the call fire in real time instead of on a poll cycle.
Step 3: Normalize the payload
Back in n8n, add a Set node after the Webhook Trigger to shape the incoming JSON into a consistent object: row, id (a stable identifier column in the sheet, not the row number, since rows can be re-sorted), and whatever business fields the sheet tracks (name, status, amount, and so on).
Step 4: Filter out loop-back edits
Add an IF node that checks whether synced_at is empty or older than a few seconds. If the Apps Script guard in Step 2 already blocks self-triggered edits, this node is a second safety net, useful if you later add other scripts or manual edits to the same column. Route edits that fail the check to a NoOp node to end that branch cleanly.
Step 5: Upsert the row into Airtable
Add an Airtable node set to the Upsert operation. Match on the id field from Step 3 so a repeated edit to the same row updates the existing Airtable record instead of creating a duplicate. Map the sheet's business fields to the corresponding Airtable fields.
Step 6: Watch Airtable for external changes
Airtable does not push webhooks on a free plan by default, so poll the other direction on a schedule. Add a second trigger branch starting with a Schedule Trigger node set to run every 5 minutes. Follow it with an Airtable node set to Search, filtered by a last_modified_time field greater than the last successful sync (store that timestamp with n8n's static data or a small Set node chain).
Step 7: Write changes back to the Sheet
Add a Google Sheets node set to the Update Row operation, matching on the same id column, to push each changed Airtable record back into the corresponding row. In the same operation, set the synced_at column to the current timestamp, this is the marker that Step 2's Apps Script checks before firing again, which is what breaks the loop.
Step 8: Handle conflicts
If a row can change in both places between sync cycles, add a Code node before the Step 7 write that compares last_modified_time from Airtable against a sheet_updated_at value you also track in the sheet. Last-write-wins is enough for most cases: only write back if the Airtable timestamp is newer than the sheet's own last edit. Log skipped conflicts to a dedicated Sheet tab so nothing silently disappears.
Adapting This Pattern
- Notion instead of Airtable: swap the Airtable node for a Notion node (Database Page: Upsert), and use Notion's
last_edited_timeproperty for the reverse-sync filter in Step 6 - Supabase instead of Airtable: replace the Airtable node with an HTTP Request node against Supabase's REST API using an
upsertheader, and use Supabase'supdated_atcolumn the same way - Multiple sheets: run one Apps Script trigger per sheet, each posting to the same webhook with a
source_sheetfield so the IF node in Step 4 can branch by origin
When Polling Is Still Fine
If the sheet only changes a few times a day, or a five-minute delay is acceptable, the built-in Google Sheets Trigger node on a Schedule Trigger is simpler to set up and needs no Apps Script deployment. Reach for the webhook pattern above when edits happen frequently, when API quota from constant polling is a real concern, or when downstream systems need the change within seconds rather than minutes.
Templates and Further Reading
Browse the n8n Resources template library for more sync and integration workflows, see webhook automation use cases for other push-based patterns, or explore Airtable templates for additional two-way sync examples.
Enjoyed this article?
Share it with others who might find it useful