Loopfour
Blocks

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

OperationBehaviour
insertAppend a new row. Fails if the table is at maxRows.
updateMerge data into every row whose matchKey equals matchValue.
upsertUpdate matching rows, otherwise insert a row.
upsertManyBulk upsert rows in one transaction. Duplicate match values collapse to the last occurrence.
deleteDelete every row matching matchKey and matchValue.
findReturn rows whose data contains the provided filter, ordered newest first with stable offset pagination.
getByIdReturn one row by rowId; throws if it is not found.
createTableGet or create a company table by name and return its tableId. No tableId input is required.
listTablesList all company tables, only root tables, or tables in one folder according to folderId.
listFoldersList all flat table folders and the table IDs currently assigned to each one.
ensureFolderIdempotently get or create a folder by its trimmed, exact name.
ensureTableIdempotently get or create a table by exact company, folder, and table name.
createTableInFolderGet or create a named table in a specific existing folder.
assignTableToFolderMove a table to a folder, or move it to root with an explicit null.

Folder semantics

folderId has operation-specific meaning:

OperationOmittednullString or template
listTablesList all company tablesList only root tablesList only tables in that folder
ensureTableInvalidEnsure a root tableEnsure a table in that folder
createTableInFolderInvalidInvalidCreate or reuse the named table in that folder
assignTableToFolderInvalidMove the table to rootMove 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

FieldTypeRequired forDescription
operationstringallOne of the 14 operations above.
tableIdstringrow operations, assignTableToFolderTarget table ID. A literal ID or a template such as {{steps.create_table.tableId}} is accepted.
folderIdstring or nullensureTable, createTableInFolder, assignTableToFolderFolder filter, identity scope, or destination. It is optional only for listTables; the visual text field accepts null for root.
folderNamestringensureFolderTrimmed folder name, 1–255 characters.
namestringcreateTable, ensureTable, createTableInFolderTable name, 1–255 characters. Templates are supported.
descriptionstringoptional create inputDescription stored when a new table is created.
columnsobject[]createTable, ensureTable, createTableInFolderOne or more unique, case-insensitive column names. IDs are minted server-side.
maxRowsnumberoptional create inputRow ceiling for a newly created table (default 10,000, max 100,000).
dataobjectinsert, upsert; optional for updateRow payload. Updates merge it into the stored JSON data.
matchKeystringupdate, upsert, upsertMany, deleteColumn name used to find matching rows.
matchValueanyupdate, upsert, deleteValue compared as text. Runtime templates are supported.
rowsobject[]upsertManyUp to 2,000 row objects; each must carry a non-empty matchKey value.
filterobjectoptional for findJSON containment predicate. Omit it to match every row.
limitnumberoptional for findRows per page (default 50, max 1,000).
offsetnumberoptional for findNon-negative number of matching rows to skip (default 0).
rowIdstringgetByIdRow 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}}.

OperationOutput
insert, getByIdThe row fields.
update{ updated } count.
upsertAn 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.
  • createTableInFolder finds the same table name outside the requested folder.
  • getById cannot find the row.
  • The config fails schema validation, such as operation: "merge" or more than 1,000 requested find rows.

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

LimitValue
Rows per table (default)10,000
Rows per table (max)100,000
Rows per find page1,000
Rows per upsertMany call2,000
Columns per workflow create operation100
listTables / listFolders paginationNone; each call returns all matching company resources
Folder nestingNot supported; folders are flat

Frequently Asked Questions

No. Use ensureFolder followed by ensureTable. ensureTable is idempotent by company, folder, and table name and returns tableId, folderId, columns, and created for later steps.

Use listTables with folderId set to an explicit JSON null. Omitting folderId lists every company table instead.

Use assignTableToFolder with the tableId and folderId set to an explicit JSON null. Repeating the same assignment is safe and returns changed: false.

Every query is constrained by the workflow run's companyId. IDs belonging to another company are treated as unavailable.

Data Table operations are not transactional across steps. Existing writes remain. Prefer ensureFolder, ensureTable, upsert, and assignTableToFolder when a restart may repeat the step.

On this page