Data Table Block
Create, organize, and read or write user-managed data tables from a workflow
The Data Table block creates and organizes user-managed tables and performs row operations against them. Use it to persist workflow state across runs, build lookup tables, deduplicate records, or create a folder and its tables entirely inside a workflow.
Data tables and table folders are company-scoped. Folders are flat: a table can belong to one folder or to the Data Store root, but folders cannot be nested.
Every operation is tenant-isolated. A workflow running in company A cannot list, create, move, read, or write resources owned by company B.
When to Use
- Workflow state. Track records that have already been processed so later runs can skip them.
- Dynamic tables. Get or create a table whose name comes from a previous step, such as one table per accounting period.
- Workflow-managed organization. Ensure a folder exists, then create or move tables into it without a manual Studio setup step.
- Lookups. Map external IDs to internal IDs without round-tripping to another service.
- Approval queues. Insert a row representing pending work that a human resolves out of band.
Each table is capped at maxRows rows (default 10,000, max 100,000). For larger volumes, frequent analytical reads, or relational joins, use a dedicated database integration.
Configuration
The block is dispatched internally through data_table.execute. In workflow JSON, keep type: "data_table" and select one of the 14 native operations with config.operation; do not invent a data_table.<operation> action name.
Operations
| Operation | Behaviour |
|---|---|
insert | Append a new row. Fails if the table is at maxRows. |
update | Merge data into every row whose matchKey equals matchValue. |
upsert | Update matching rows, otherwise insert a row. |
upsertMany | Bulk upsert rows in one transaction. Duplicate match values collapse to the last occurrence. |
delete | Delete every row matching matchKey and matchValue. |
find | Return rows whose data contains the provided filter, ordered newest first with stable offset pagination. |
getById | Return one row by rowId; throws if it is not found. |
createTable | Get or create a company table by name and return its tableId. No tableId input is required. |
listTables | List all company tables, only root tables, or tables in one folder according to folderId. |
listFolders | List all flat table folders and the table IDs currently assigned to each one. |
ensureFolder | Idempotently get or create a folder by its trimmed, exact name. |
ensureTable | Idempotently get or create a table by exact company, folder, and table name. |
createTableInFolder | Get or create a named table in a specific existing folder. |
assignTableToFolder | Move a table to a folder, or move it to root with an explicit null. |
Folder semantics
folderId has operation-specific meaning:
| Operation | Omitted | null | String or template |
|---|---|---|---|
listTables | List all company tables | List only root tables | List only tables in that folder |
ensureTable | Invalid | Ensure a root table | Ensure a table in that folder |
createTableInFolder | Invalid | Invalid | Create or reuse the named table in that folder |
assignTableToFolder | Invalid | Move the table to root | Move the table to that folder |
In workflow JSON, root is the JSON value null. In the visual builder's Folder ID text field, enter null without quotes; runtime validation normalizes that text to the same root sentinel.
ensureFolder trims folderName, requires 1–255 characters, and returns an existing exact-name folder rather than creating a duplicate. Folder names are case-sensitive, so Finance and finance are different folders.
All table-creation operations use the same name, description, columns, and maxRows rules. ensureTable requires an explicit folder scope and matches the exact (company, folder, table name) tuple, so Invoices in the root is distinct from Invoices in a folder. Its folderId can be a folder ID or null for root. The legacy createTable operation retains company-wide name matching for compatibility and fails closed whenever multiple company-wide matches exist, including duplicate rows in one folder. createTableInFolder first reuses an exact-folder match, but otherwise rejects a same-named table elsewhere instead of creating another. Use ensureTable when the same name must be valid in multiple folder scopes.
Inputs
| Field | Type | Required for | Description |
|---|---|---|---|
operation | string | all | One of the 14 operations above. |
tableId | string | row operations, assignTableToFolder | Target table ID. A literal ID or a template such as {{steps.create_table.tableId}} is accepted. |
folderId | string or null | ensureTable, createTableInFolder, assignTableToFolder | Folder filter, identity scope, or destination. It is optional only for listTables; the visual text field accepts null for root. |
folderName | string | ensureFolder | Trimmed folder name, 1–255 characters. |
name | string | createTable, ensureTable, createTableInFolder | Table name, 1–255 characters. Templates are supported. |
description | string | optional create input | Description stored when a new table is created. |
columns | object[] | createTable, ensureTable, createTableInFolder | One or more unique, case-insensitive column names. IDs are minted server-side. |
maxRows | number | optional create input | Row ceiling for a newly created table (default 10,000, max 100,000). |
data | object | insert, upsert; optional for update | Row payload. Updates merge it into the stored JSON data. |
matchKey | string | update, upsert, upsertMany, delete | Column name used to find matching rows. |
matchValue | any | update, upsert, delete | Value compared as text. Runtime templates are supported. |
rows | object[] | upsertMany | Up to 2,000 row objects; each must carry a non-empty matchKey value. |
filter | object | optional for find | JSON containment predicate. Omit it to match every row. |
limit | number | optional for find | Rows per page (default 50, max 1,000). |
offset | number | optional for find | Non-negative number of matching rows to skip (default 0). |
rowId | string | getById | Row ID to retrieve. |
Supported column types for create operations are text, number, boolean, date, select, url, email, and json. Omitted types default to text.
Outputs
Operation results are exposed directly on the step output. For example, read {{steps.find_queue.rows}} after find, not {{steps.find_queue.result.rows}}.
| Operation | Output |
|---|---|
insert, getById | The row fields. |
update | { updated } count. |
upsert | An inserted row, or { updated } count. |
upsertMany | { total, updated, inserted } counts. |
delete | { deleted } count. |
find | { rows, pagination }, where pagination includes limit, offset, returned, nextOffset, and hasMore. |
createTable, ensureTable, createTableInFolder | { tableId, name, folderId, created, columns }. |
listTables | { tables, count }. |
listFolders | { folders, count }; each folder includes tableIds. |
ensureFolder | { folderId, folder, created }. |
assignTableToFolder | { tableId, folderId, previousFolderId, changed }. |
Examples
Ensure a folder, then ensure a table in it
The second step templates the folder ID returned by the first step. Re-running these steps reuses both resources and reports created: false for each existing resource.
[
{
"id": "ensure_reports_folder",
"name": "Ensure reports folder",
"type": "data_table",
"config": {
"operation": "ensureFolder",
"folderName": "Monthly reports"
}
},
{
"id": "ensure_invoice_table",
"name": "Ensure invoice table in folder",
"type": "data_table",
"config": {
"operation": "ensureTable",
"folderId": "{{steps.ensure_reports_folder.folder.id}}",
"name": "Invoices - {{input.period}}",
"description": "Invoices collected for the accounting period",
"columns": [
{ "name": "invoiceId", "type": "text", "required": true },
{ "name": "amount", "type": "number" },
{ "name": "processed", "type": "boolean" }
],
"maxRows": 25000
}
}
]Use {{steps.ensure_invoice_table.tableId}} in later row operations.
List root tables and move a table back to root
Root is represented by an explicit JSON null, not by an empty string.
[
{
"id": "list_root_tables",
"name": "List root tables",
"type": "data_table",
"config": {
"operation": "listTables",
"folderId": null
}
},
{
"id": "move_table_to_root",
"name": "Move table to root",
"type": "data_table",
"config": {
"operation": "assignTableToFolder",
"tableId": "{{steps.ensure_invoice_table.tableId}}",
"folderId": null
}
}
]Omit folderId from listTables to list tables across root and every folder.
Find rows with offset pagination
{
"id": "find_queue",
"name": "Find pending queue rows",
"type": "data_table",
"config": {
"operation": "find",
"tableId": "{{steps.create_invoice_table.tableId}}",
"filter": { "status": "pending" },
"limit": 250,
"offset": 0
}
}Use pagination.nextOffset for the next page when it is non-null. Ordering uses createdAt DESC with the row ID as a stable tie-breaker.
Error Handling
The block throws and the step fails when:
- A referenced table or folder does not exist for the current company.
- A required
tableId,folderId,folderName, name, column list, match field, or row ID is missing. - An insert would exceed
maxRows. createTableInFolderfinds the same table name outside the requested folder.getByIdcannot find the row.- The config fails schema validation, such as
operation: "merge"or more than 1,000 requestedfindrows.
An unchanged folder assignment succeeds with changed: false. update, upsert, and delete against zero matching rows also succeed and report zero rather than throwing.
matchKey and matchValue use text comparison against the stored JSON value. Numeric IDs compare as their string form; pass boolean values deliberately and avoid using null as a match key value.
Limits
| Limit | Value |
|---|---|
| Rows per table (default) | 10,000 |
| Rows per table (max) | 100,000 |
Rows per find page | 1,000 |
Rows per upsertMany call | 2,000 |
| Columns per workflow create operation | 100 |
listTables / listFolders pagination | None; each call returns all matching company resources |
| Folder nesting | Not supported; folders are flat |
Tables API
REST endpoints to create tables, manage folders and columns, and read or write rows from your own systems.
Code Block
Transform upstream values before writing a row.
Transform Block
Reshape upstream output into the row payload.
Loopfour