When your team isn't ready to stand up PostgreSQL, but you can't exactly keep the data on paper either. Google Sheets turns out to be a surprisingly capable 'lightweight DB' — as long as you stay under 5,000 rows and 3 writes per second.
This tutorial uses an inquiry form as an example and builds a mini-CRM that supports Append / Lookup / Update — all with three operations of n8n's Google Sheets node. 30 minutes, start to finish.
1. Sheet design — the header row IS the schema
To use a sheet like a DB, treat the first row as your 'column definitions'. Skip friendly Korean headers and use English snake_case instead. You can then reference them directly in n8n expressions like {{$json.customer_email}}.
// Sheet A1:H1 header
id | created_at | customer_name | customer_email | subject | body | status | assigned_to
// Example row A2
INQ-20260710-001 | 2026-07-10T10:23:00Z | Minsu Kim | kim@ex.co | Quote request | ... | new | (empty)id column must be unique. There's no auto-increment, but an expression like INQ-{{$now.toFormat('yyyyMMdd')}}-{{$itemIndex}} produces a unique enough key.2. Append — take in a new inquiry
A flow that appends one row to the bottom of the sheet for every inquiry coming in from the website form. Two nodes: n8n Webhook → Google Sheets (Append). Done.
- Add a
Webhooknode in n8n, POST method, path/inquiry - Use a
Setnode to createid,created_at,status: 'new'fields Google Sheetsnode → Operation:Append, Range:Inquiries!A:H- Match the Column Mapping 1:1 with the sheet header
- Send a confirmation email at the end with
GmailorSend Email
3. Lookup — build a status-check API
You need to be able to answer 'what happened with my inquiry?' when a customer asks. The Google Sheets node's Lookup operation exists for exactly this.
// Google Sheets node config (Lookup)
Operation: Lookup Row(s)
Range: Inquiries!A:H
Lookup Column: id
Lookup Value: {{ $json.query.id }}
Return All Matches: falsePut a Webhook (GET /inquiry/status) in front of it and you've got a lookup API in five minutes. Return the response as raw JSON via a Respond to Webhook node.
4. Update — assign an owner, change status
This is the trickiest part. Google Sheets has no concept of a 'primary key', so n8n uses a 'Matching Column' value to find and update the row. This is exactly why we made the id column unique.
// Update config
Operation: Update Row
Range: Inquiries!A:H
Matching Column: id
Values to Send:
status: 'assigned'
assigned_to: 'hans@synact.ai'
// Don't send id — it's only used for matchingIf node after Update that checks {{$json.updatedRows}} > 0 and fires a Slack alert on failure.5. Concurrency — where the sheet falls apart
The Google Sheets API has a per-project limit of 300 requests per minute. When webhook traffic spikes you get a flood of 429s, and in the worst case, Appends can overwrite each other on the same row. The three practices below cover most situations.
- Set
Retry On Failon theGoogle Sheetsnode — 3 tries, 2-second interval - For high-traffic workflows, run n8n in
Queue Modeto buffer executions - If write spikes are frequent, put a
Redisqueue in front and cap throughput at 5/sec
When to actually move to a real DB
If you see two or more of these signals, it's moving day. Swapping to the PostgreSQL node takes about 30 minutes, and the workflow structure stays largely the same.
Start with a sheet. Move when it hurts. Delaying the start by three months to 'do it right the first time' is the biggest loss of all.— SynAct.ai Consulting Team
Wrap-up
Using Google Sheets as a DB isn't a 'stopgap' — it's a legitimate architecture choice. The team can edit it comfortably, backups are automatic, and it's free. Don't undervalue those three. Just know the limits above precisely, and move on when you cross them.