real tables inside the n8n instance with columns, types, rows, and CRUD via the
dataTable
node and data-table MCP tools.
Use them for local persistent state: lookup tables, recent events, per-session inventories, counters, idempotency tracking, dedup state when there's row-level logic or external visibility (plain "have I seen this value?" dedup belongs in the
Remove Duplicates
node). Small-to-moderate volume (tens of thousands of rows fine, millions belong in a real DB).
Non-negotiables
System-managed columns + external IDs.
Three columns auto-exist on every table:
id
(bigserial),
createdAt
,
updatedAt
. Don't declare them in
create_data_table
(errors or shadows the system column). Don't write them on insert. For domain identifiers from outside (arxivId, stripeCustomerId, requestId), add a separate column and key dedup/lookup on that.
Only primitives in columns, nested data uses
string
+
_object
postfix.
No JSON/object/array column types exist. For nested data (arrays, parsed objects), use a
string
column with
JSON.stringify(...)
on write and
JSON.parse(...)
on read. Mark the column with
_object
(e.g.,
keyInsights_object
). The postfix is the contract that tells readers to parse. See
references/SCHEMA_DESIGN.md
.
Strong defaults
Don't add a Set node before a Data Table node to modify fields.
The Data Table node's per-column expression slots are just as powerful as Set fields, so the Set node is doing zero work the Data Table node can't do itself. (Same Set-node antipattern called out in
n8n-expressions-official
.)
Match n8n's column casing: camelCase.
The auto-managed columns are camelCase (
createdAt
,
updatedAt
), so user columns read more cleanly when they match:
arxivId
,
paperId
,
taxRate
. Mixed casing in the same query (
createdAt >= ... AND arxiv_id eq ...
) reads as a typo. Keep the
_object
postfix on stringified-blob columns regardless (
keyInsights_object
), the underscore is a contract marker, not casing.
Verify the
columns
parameter via
get_workflow_details
after create/update.
The UI has a display quirk in manual mapping mode ("Currently no items exist" with no actual data loss). Checking the JSON confirms what's persisted.
Relational design works when the shape calls for it.
For genuine parent-child data (papers → summaries, customers → orders), reference parents by
id
, name columns explicitly (
paperId
,
customerId
), and enforce integrity in workflow logic. Don't force it on flat use cases (dedup, lookup, audit) where there's no relationship to model.
Storage format is not interface format.
Parse
_object
fields
before
returning them from a sub-workflow. Callers should never receive stringified shells they have to parse themselves. See
references/SCHEMA_DESIGN.md
"Storage format ≠ interface format".
Show more
Installs
961
Repository
n8n-io/skills
GitHub Stars
464
First Seen
Jul 8, 2026
Security Audits
Gen Agent Trust Hub
Pass
Socket
Pass
Snyk
Pass