n8n Google Sheets Automation: Sync Data with Any Tool

Connect Google Sheets to any app with n8n. Read, write, and sync rows automatically using triggers, HTTP nodes, and real workflow examples.

Problem: Your Data Lives in Google Sheets but Your Tools Don't Talk to It

You've got CRM leads in one place, form submissions in another, and a Google Sheet that's supposed to hold it all together — but syncing it is manual copy-paste work.

n8n can automate every part of this: read rows on a schedule, append new data from any trigger, update cells when upstream records change, and push Sheet data into any API.

You'll learn:

  • How to connect n8n to Google Sheets with OAuth2
  • How to read, append, and update rows using the Google Sheets node
  • Three real workflow patterns: form-to-sheet, sheet-to-CRM, and scheduled sync

Time: 20 min | Difficulty: Intermediate


Why Google Sheets + n8n Works Well

Google Sheets is a practical data layer for small teams: human-readable, shareable, no schema migrations. n8n treats it as a first-class integration — the Google Sheets node handles auth, pagination, and cell mapping without custom code.

The catch: you need a properly configured OAuth2 credential the first time. That setup takes 5 minutes and you do it once.


Solution

Step 1: Create a Google Cloud OAuth2 Credential in n8n

You need a Google Cloud project with the Sheets API enabled before n8n can authenticate.

  1. Go to console.cloud.google.comAPIs & ServicesEnable APIs → search "Google Sheets API" → Enable it.
  2. Also enable the Google Drive API (required for listing spreadsheets by name).
  3. Go to CredentialsCreate CredentialsOAuth 2.0 Client ID.
  4. Set Application type to Web application.
  5. Under Authorized redirect URIs, add your n8n callback URL:
https://your-n8n-domain.com/rest/oauth2-credential/callback

For local n8n: http://localhost:5678/rest/oauth2-credential/callback

  1. Copy the Client ID and Client Secret.

In n8n:

  1. Go to CredentialsNew → search Google Sheets OAuth2 API.
  2. Paste the Client ID and Client Secret.
  3. Click Sign in with Google and authorize the account that owns the Sheet.

Expected: Credential shows green "Connected" status.

If it fails:

  • redirect_uri_mismatch → The URI in Google Cloud doesn't exactly match n8n's callback URL. Copy it character-for-character from n8n's credential screen.
  • Access blocked: app not verified → Click "Advanced" → "Go to [app] (unsafe)" during OAuth for personal/dev use. For production, submit the app for Google verification.

Step 2: Read Rows from a Sheet

Add a Google Sheets node to your workflow. Set the operation to Read Rows.

Resource:    Spreadsheet
Operation:   Read Rows
Document:    [Select your spreadsheet by name]
Sheet:       Sheet1

By default this returns all rows as JSON objects, using the first row as column headers.

Example output for a leads sheet:

[
  { "Name": "Alice Chen", "Email": "alice@example.com", "Status": "New" },
  { "Name": "Bob Torres", "Email": "bob@example.com", "Status": "Contacted" }
]

Each item in n8n's output maps to one row. Downstream nodes like IF, Set, or HTTP Request can reference any column by name: {{ $json.Email }}.

Tip — filter rows at the node level: Under Filters, add a column condition like Status = New to avoid pulling the whole sheet into memory for large datasets.


Step 3: Append New Rows

Use the Append Row operation to write data from any upstream node into your Sheet.

Resource:    Spreadsheet
Operation:   Append Row
Document:    [Your spreadsheet]
Sheet:       Sheet1

Under Values to Send, map the incoming data to your column headers:

Name   →  {{ $json.name }}
Email  →  {{ $json.email }}
Date   →  {{ $now.toISO() }}
Source →  Typeform

n8n appends after the last populated row automatically. You don't need to track row numbers.

Common pattern — form submission to Sheet:

Webhook (Typeform / Tally / custom) 
  → Set node (normalize field names)
    → Google Sheets (Append Row)
      → Slack (notify team)

The Webhook node receives the POST, Set maps it to your schema, Sheets writes the row, Slack sends a message. Total nodes: 4.


Step 4: Update an Existing Row

Updating requires knowing which row to target. n8n's Google Sheets node supports Update Row with a column-match lookup — you don't need the row index.

Resource:      Spreadsheet
Operation:     Update Row
Document:      [Your spreadsheet]
Sheet:         Sheet1
Lookup Column: Email
Lookup Value:  {{ $json.email }}

Under Values to Update, set only the columns you want to change:

Status      →  Closed
ClosedDate  →  {{ $now.toISO() }}

n8n finds the first row where Email matches the lookup value and updates those cells. Everything else in the row stays untouched.

If it fails:

  • No rows updated → The lookup value doesn't match exactly (check for trailing spaces or case differences). Add a Set node before this one to normalize the value with {{ $json.email.trim().toLowerCase() }}.
  • Column not found error → The column header in your Sheet has a leading/trailing space. Fix the header directly in Sheets.

Step 5: Trigger a Workflow When a Sheet Changes

For sheet-triggered workflows, use the Google Sheets Trigger node instead of a manual or webhook trigger.

Node:          Google Sheets Trigger
Document:      [Your spreadsheet]
Sheet:         Sheet1
Event:         Row Added
Poll Interval: Every 1 minute

n8n polls the Sheet on your chosen interval and fires the workflow for each new row since the last check. The trigger node outputs one item per new row.

Limitation: Google Sheets has no native push webhook. Polling every minute is the minimum interval on most n8n plans. For real-time triggers, use a Google Apps Script to POST to an n8n webhook instead — set it up in the Sheet's Extensions → Apps Script:

// Paste this in Apps Script → assign to onFormSubmit or onChange trigger
function sendToN8n(e) {
  const payload = {
    row: e.range.getValues()[0],
    sheet: e.range.getSheet().getName(),
    timestamp: new Date().toISOString()
  };
  UrlFetchApp.fetch("https://your-n8n.com/webhook/sheets-trigger", {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload)
  });
}

This fires instantly on form submit or cell edit — no polling delay.


Step 6: Complete Workflow — Sheet to CRM Sync

Here's a production-ready workflow that reads new "Status = Qualified" rows from a Sheet every 15 minutes and creates contacts in HubSpot.

Schedule Trigger (every 15 min)
  → Google Sheets: Read Rows
      Filter: Status = Qualified
  → IF: HubspotID is empty         ← skip rows already synced
      TRUE branch:
        → HTTP Request: POST to HubSpot Contacts API
            URL:    https://api.hubapi.com/crm/v3/objects/contacts
            Auth:   Header Auth (Bearer token)
            Body:   {
                      "properties": {
                        "email":     "{{ $json.Email }}",
                        "firstname": "{{ $json.Name.split(' ')[0] }}",
                        "lastname":  "{{ $json.Name.split(' ')[1] }}"
                      }
                    }
        → Google Sheets: Update Row
            Lookup: Email = {{ $json.Email }}
            Update: HubspotID = {{ $json.id }}
                    Status    = Synced

The IF node prevents re-syncing rows by checking whether HubspotID is already populated. After each sync, the Update Row node writes the HubSpot contact ID back to the Sheet — so the next run skips it.


Verification

Run the full workflow manually first:

Click "Test Workflow" in n8n → check each node's output panel

Verify in your Sheet that the HubspotID column populates after the run. Check HubSpot Contacts to confirm the records created.

For the scheduled version, activate the workflow and wait one cycle:

You should see: New Sheet rows with Status = Synced and a populated HubspotID column within 15 minutes of the trigger firing.


What You Learned

  • The Google Sheets node supports Read, Append, and Update in the same credential — one OAuth2 setup covers all three operations.
  • Use column-match lookups for updates instead of row indexes — it's more resilient when rows get inserted or reordered.
  • For real-time triggers, Google Apps Script + n8n Webhook beats the polling trigger by an order of magnitude in latency.
  • The IF node with a "already synced" check is the simplest way to make a Sheet-to-CRM sync idempotent.

When not to use this approach: If your Sheet has 10,000+ rows and you're reading all of them on every poll, move the data to PostgreSQL and use n8n's Postgres node instead. Sheets starts slowing down API calls noticeably above ~5,000 rows.

Tested on n8n 1.40.0, Google Sheets API v4, Node.js 20, Ubuntu 24.04