Skip to main content
Lifetime license included with every purchase
n8n workflowsGoogle SheetsETL pipelinedata validation

Build a Google Sheets ETL Workflow in n8n

Build an n8n Google Sheets ETL workflow with a preflight validation step, dedupe, and upsert so bad rows never reach your database. Full node-by-node guide.

Nn8n Marketplace Team·July 11, 2026·Updated July 11, 2026·6 min read

A Spreadsheet Is a Database Everyone Can Break

Sheets are where business data is born and where it goes to rot. Someone fat-fingers a date, pastes a blank row, types "N/A" into a number column. Then a sync job loads all of it into your warehouse and the dashboards downstream go quietly wrong.

An n8n Google Sheets ETL workflow stops that at the gate. It extracts the rows, validates them against rules you define, transforms the survivors into the shape your database expects, and upserts only the clean ones. Bad rows get quarantined with a reason, not loaded.

Most tutorials show a one-way sync and call it done. The validation step is the part that actually saves you, and it's the part they skip.

What You Can Automate

  • Sheet to warehouse load: read a Google Sheet on a schedule, validate, and upsert into Postgres, MySQL, or BigQuery
  • Preflight rejection: split rows into clean and rejected branches before anything touches the database
  • Dedupe on a business key: upsert on email, order ID, or SKU so edits update instead of duplicate
  • Quarantine sheet: failed rows written back to a second tab with a human-readable reason
  • Type coercion: strings to numbers, mixed date formats to ISO, blanks to nulls, in one Code node
  • Incremental loads: only process rows added since the last run, tracked by a timestamp column

The ETL Pipeline

A proper extract-validate-transform-load run is five stages, and the order matters:

Schedule Trigger (hourly)
  → Google Sheets (read source range)
  → Code (validate → split clean / rejected)
  → [clean]    → Code (transform: coerce types, map columns)
               → Postgres (upsert ON CONFLICT)
  → [rejected] → Google Sheets (append to Quarantine tab + reason)
  → Slack (summary: 142 loaded, 3 rejected)

The branch after validation is the whole design. Clean rows go forward; rejected rows go sideways with an explanation. Nothing fails silently into the destination table.

1. Extract

The Google Sheets node, Read Rows operation, pulls your source range. Read the whole sheet for a full reload, or filter by a "processed" flag column for incremental runs. One gotcha worth knowing: the node returns empty trailing cells as undefined, not empty string, so your validation has to treat both as missing.

2. Validate

This is the preflight gate. A Code node loops every row and applies your rules, pushing each into a clean or rejected bucket with a reason on failure:

const clean = [], rejected = [];
for (const item of $input.all()) {
  const r = item.json;
  const errs = [];
  if (!r.email || !/^[^@]+@[^@]+\.[^@]+$/.test(r.email)) errs.push('bad email');
  if (r.amount == null || isNaN(Number(r.amount))) errs.push('amount not numeric');
  if (Number(r.amount) < 0) errs.push('negative amount');
  if (errs.length) rejected.push({ json: { ...r, _reason: errs.join('; ') } });
  else clean.push({ json: r });
}
return [clean, rejected];   // output 0 = clean, output 1 = rejected

Returning two arrays gives the Code node two outputs. Wire output 0 to the load branch and output 1 to the quarantine branch.

3. Transform

Now coerce. Strings to numbers, the three date formats your sales team uses into one ISO string, column names mapped to your schema. Do it on the clean branch only, because there's no point transforming rows you're about to reject.

4. Load

Upsert, never blind insert. In Postgres that's INSERT ... ON CONFLICT (email) DO UPDATE. The dedupe lives in the key, so re-running the workflow on the same sheet updates existing records instead of doubling them. That idempotency is what lets you re-run a failed batch without fear.

5. Report

Append rejected rows to a Quarantine tab and post a one-line summary to Slack: loaded count, rejected count, link to the tab. Someone owns those rejects. Make them visible.

Implementation Patterns

Pattern 1 — Idempotent upsert. The single most important property of an ETL job is that running it twice does no harm. Upsert on a business key and the workflow becomes safe to retry. Self-hosted n8n charges you nothing per execution, so a defensive hourly re-run costs an API call, not a metered task fee.

Pattern 2 — Quarantine, don't drop. Rejected rows are data, not noise. A row that failed validation usually means a process upstream is broken. Writing rejects to a tab with a reason turns your ETL job into a data-quality monitor for free.

Pattern 3 — Incremental by timestamp. For big sheets, full reloads get slow. Add a "last synced" timestamp to the workflow's static data and filter the read to rows newer than it. The IF node that does the filtering also protects you from reprocessing the same 10,000 rows every hour.

n8n Nodes You'll Use Most

NodePurpose
Schedule TriggerRuns the ETL pass on a cron
Google SheetsReads the source range, writes the quarantine tab
CodeValidates rows, splits clean from rejected, coerces types
IFFilters incremental rows by timestamp
Postgres / MySQLUpserts clean rows on a business key
MergeRecombines branches for the summary count
SlackPosts the loaded-versus-rejected summary

Getting Started

  1. Connect the Google Sheets node and confirm it reads your range in the execution log.
  2. Write the validation Code node with your real rules; test it against a sheet you've deliberately broken.
  3. Wire the clean output to a transform Code node, the rejected output to a quarantine append.
  4. Add the database node with an upsert keyed on your business column.
  5. Run on a small range manually, then check both the table and the quarantine tab.
  6. Add the Slack summary and enable the schedule.
  7. Break a row on purpose one more time to confirm it lands in quarantine, not the table.

For a head start, the Data Entry Automation Hub wires the read-validate-upsert path with the clean-versus-rejected split already built, so you swap in your sheet and your schema instead of designing the branch from scratch.

See the Data Entry Automation Hub
Skip the build

The Data Entry Automation Hub ships this end-to-end: the Google Sheets read, the validation Code node that splits clean rows from rejected, the keyed upsert, and the quarantine-tab write-back already wired together. It's part of The Complete n8n Templates Bundle, a one-time lifetime license to the whole catalog (plus every template added later) if you run more than one of these data pipelines.

Get the Data Entry Automation Hub

ETL into a sheet is one half of the story; loading into a warehouse is the other. If your destination is Google's analytics stack, the n8n BigQuery automation walkthrough covers the scheduled load and the auth gotchas, and the broader n8n data pipeline automation guide maps where validation fits across a multi-stage flow. When the validation gate itself needs to be the product, the Data Gatekeeper template formalizes the preflight-and-quarantine pattern.

Browse the full template catalog
FAQ

Common questions

Can n8n move Google Sheets data into a database?
Yes, and it's one of the cleaner ETL jobs n8n handles. A Google Sheets node reads the rows, a Code or IF node validates them, and a Postgres or MySQL node upserts the clean ones. The key is the validation step in the middle: without it you load whatever someone typed into the sheet, errors and all.
How do I avoid loading duplicate rows from a sheet?
Pick a stable business key (an email, an order ID, a SKU) and upsert on it instead of inserting blindly. In Postgres that's an INSERT with an ON CONFLICT clause; in the n8n Google Sheets node it's the appendOrUpdate operation keyed on a column. Dedupe on the key, not on the whole row, because one edited field shouldn't create a second record.
What is a preflight validation step in an ETL workflow?
It's a gate that runs before the load and stops the whole batch if the data fails a rule: a missing required column, a malformed email, a negative amount. n8n implements it as a Code node that splits rows into a clean branch and a rejected branch. The clean rows load; the rejected ones go to a quarantine sheet with a reason, so nobody silently corrupts the destination table.
Stop reading. Start running.

Get the workflow templates this guide is built on

Import-ready n8n JSON, step-by-step setup, and tested end-to-end. One-time payment, own it forever.

Free — $40 value

Get 3 tested n8n templates, free

The full customer package for three real catalog templates — workflow JSON, step-by-step setup guide, credential checklist. Built through the same live-instance release process as everything we sell. Plus new templates and automation guides in your inbox. No spam, unsubscribe anytime.

  • 01Smart To-Do List ManagerPre-built n8n workflow template that automates productivity with OpenAI. Live in about 10 minutes.$14
  • 02Email Follow-Up AutomatorPre-built n8n workflow template that automates crm with OpenAI. Live in about 15 minutes.$12
  • 03Market Trend AnalyzerPre-built n8n workflow template that automates data processing with OpenAI. Live in about 10 minutes.$14