
An agency client of ours had a Google Sheet doing a job it was never designed for. Every inbound lead hit an n8n workflow, the workflow looked the lead up in a sheet to see if it had already been processed, and then wrote a row back. It worked for about eight months. Then their volume tripled, Google's API started rate-limiting the reads, and the dedupe check began silently failing — which meant the same lead got a welcome email, a Slack ping, and a CRM record three times in a row.
If your n8n workflow only needs to remember something for itself — a dedupe marker, a run counter, a small lookup — a Data Table is the right home for it. If a human, a dashboard, or another application ever needs to read that same data, you want a real external database. That's the whole decision, and most of the confusion around n8n Data Tables comes from people trying to push them across that line.
We've now migrated a dozen client workflows off Sheets and onto Data Tables. Here's what actually holds up in production, what breaks, and the facts that most of the tutorials published this year got wrong.
What n8n Data Tables Actually Are
Data Tables are structured tabular storage built into n8n and scoped to a project. You create one from the Data tables tab in a project, either from scratch or by importing a CSV, and then read and write it from the Data Table node inside any workflow in that project.
Columns come in four types: string, number, boolean, and date (the create-table API also accepts json). Every row automatically gets an id, a createdAt, and an updatedAt. That's it. There's no schema migration story, no indexes you control, no foreign keys.
The row operations available in the node are worth memorizing, because they define the ceiling:
- Get — return rows matching any or all conditions, with an optional limit and order-by.
- Insert — add rows.
- Update — modify rows matching conditions.
- Upsert — insert or update in one step. This is the operation that removes the most glue nodes in practice.
- Delete — remove matching rows, with a genuinely useful Dry Run option that shows you what would be deleted without touching anything.
- If Row Exists / If Row Does Not Exist — pass the input item through only if a match does or doesn't exist. This is the dedupe primitive, and it replaces an entire Get → IF → NoOp chain with one node.
Conditions are limited to Equals, Not Equals, Greater Than, Greater Than or Equal, Less Than, Less Than or Equal, Is Empty, and Is Not Empty. No LIKE. No contains. No regex. Keep that list in your head — it's the single best predictor of whether a Data Table will work for your use case.
Two Things Most Articles Get Wrong
I've read a lot of Data Tables posts in the last few months and two errors keep repeating.
Wrong claim #1: "The storage limit is 50 MB." It's 200 MiB by default, instance-wide across all Data Tables. n8n warns you at 80% and again at the ceiling. Past the ceiling, manual additions are blocked and workflows error on insert and update. On self-hosted you can raise it with N8N_DATA_TABLES_MAX_SIZE_BYTES; on Cloud you can't. The instance-wide part is the trap — one runaway logging table will take down the dedupe table in a completely unrelated project.
Wrong claim #2: "Data cannot be accessed via API." The n8n public API has a full /data-tables surface: list, create, get, update, and delete tables, plus GET, POST, PATCH, and DELETE on /data-tables/{id}/rows with filtering, sorting, a search parameter across string columns, and cursor pagination. You authenticate with the same X-N8N-API-KEY header as everything else. We use it to seed lookup tables during deploys.
What genuinely doesn't work: you cannot touch a Data Table from a Code node. There are no built-in methods or variables for it. If your workflow's logic lives inside a big Code node, you'll need to restructure around the Data Table node or call the public API over HTTP. Find that out at design time, not after you've written 200 lines of JavaScript.
When a Data Table Is the Right Call
These are the five patterns where we reach for a Data Table without thinking twice:
- Deduplication markers. One
stringcolumn holding an external ID, anIf Row Does Not Existnode at the top of the workflow, anInsertat the bottom. This is the highest-value use by a mile and it's what killed our client's Sheets problem. - Run state and idempotency. "Have I already synced this order?" "What was the last cursor I processed?" Small, workflow-owned, boring.
- Small lookup tables. Region-to-owner mapping, SKU-to-category, campaign-to-client. A few hundred stable rows that a non-technical teammate can edit in the UI without touching the workflow.
- Reusable prompts and message templates. Instead of hard-coding a prompt into six workflows, store it once and pull it by key. Editing a prompt stops being a workflow deploy.
- AI evaluation datasets. Test inputs and expected outputs for an agent, read at eval time. This pairs well with the way we set up memory for n8n AI agents and with testing agents before they hit production.
The performance gap versus Google Sheets isn't subtle. A Sheets lookup is an authenticated HTTP round-trip subject to quota — call it 400 to 900 milliseconds per item on a good day, plus retry logic when Google throttles you. A Data Table lookup is local storage inside the instance. On the client build I opened with, the same per-item check dropped to single-digit milliseconds, and an entire error-handling branch became dead code.
When You Need an External Database Instead
Reach for Postgres (or Supabase, or whatever you already run) the moment any of these are true:
- A human or another app reads the data. Data Tables have no SQL surface, no BI connector, and no way for your reporting stack to join them to anything. If it ends up on a client dashboard, it doesn't live here. We covered what that looks like in dashboards clients actually read.
- You need joins or aggregation. There is no
JOIN, noGROUP BY, noSUM. Pulling every row into n8n and aggregating it in a Code node works right up until the row count makes it not work. - You need partial text matching. The condition list has no LIKE and no contains. The API's
searchparameter searches across string columns, but it isn't a query language and you can't use it from the node's conditions. - The data grows unbounded. Anything that appends a row per execution forever will eat the instance-wide budget. If you use a Data Table for anything log-shaped, pair it with a scheduled cleanup workflow from day one.
- You need cross-project access. Data Tables are scoped to their project. If two projects need the same table, you either duplicate it or you move it out.
Worth saying plainly: Data Tables are not a substitute for running n8n on Postgres. If you're self-hosting past a handful of workflows, your n8n execution database should already be Postgres, because SQLite locks the whole file on write and concurrent executions collide. That's a separate concern from Data Tables, and it's the same reason we're opinionated about how many workers to run in queue mode and about self-hosted versus cloud for agencies.
The Migration We Actually Run
When we move a client off Sheets, the sequence is boring on purpose:
- Export the sheet to CSV and import it into a new Data Table from the Data tables tab. The importer infers the column structure, so check the types before you trust them — numeric IDs coming in as
stringwill silently break anEqualscondition later. - Replace the read path first, leave the write path alone. Point the lookup at the Data Table while still writing to both. Run it for a few days. If the counts diverge, you find out before anything is irreversible.
- Swap Get → IF → NoOp for
If Row Does Not Exist. This is usually where three or four nodes disappear per workflow. - Cut the write path over, then delete the Sheets credential from the workflow so nobody re-adds a node out of habit.
- Add the retention workflow. A scheduled trigger, a
Deletewith acreatedAtless-than condition, and a run with Dry Run enabled first so you can see exactly what it'll remove. Skipping this step is how a Data Table becomes an incident six months later.
One caveat we hit on two migrations: Data Tables live in a project, and if the workflow you're migrating sits in someone's personal project, the table goes there too. Move the workflow into the team project before you create the table, or you'll be recreating it and re-pointing every node.
For anything more involved than this — especially if you're coming from Zapier or Make, where state was hidden inside the platform — the mapping exercise is its own project. We wrote up that path in migrating from Zapier and Make to n8n, and the error handling and monitoring setup matters more once your state has a hard storage ceiling attached to it.
The Short Version
Data Tables are a genuinely good addition. They removed real fragility from workflows we maintain, and the If Row Does Not Exist node alone justifies learning them. But they're a workflow-owned scratchpad, not a database. Four column types, eight condition operators, no joins, no Code node access, and a shared 200 MiB ceiling.
Use them for the state your automation keeps for itself. Use Postgres for the data your business keeps. The teams that get burned are the ones that started with the first and slowly, one column at a time, turned it into the second.
You can read the official details in the n8n Data Tables documentation and the Data Table node row operations reference.
Want Someone to Look at Your Workflows?
If you're running n8n in production and you're not sure which of your state is quietly sitting in a Google Sheet, we'll tell you. Get a free automation audit — we go through your workflows, flag the fragile state, the missing error handling, and the places you're paying latency for no reason, and give you the list. No pitch attached.