Skip to content
Data

Tables & data

Tables are built-in spreadsheets your workflows and agents read and write, with no external database to set up. A workflow can find rows, insert and update them, delete them, or start when a row changes.

How tables are stored

A table's rows live in one shared store, with your columns packed into a single data field per row (there is no external database to connect or configure). Every row also carries four system fields you don't define yourself:

System fields on every row
System fieldMeaning
idThe row’s primary key. Matches the reserved filter name id (see filtering below).
row_numberThe row’s number in its own table (1, 2, 3... in the order rows were added), shown as # in the grid. Deleted rows leave gaps and numbers are never reused. Read-only: filter or sort on it, but address a row by id.
priorityAn integer used for the default sort order.
created_atTimestamp set when the row was inserted.

These fields come back alongside your own columns on every read. A few names are reserved and can't be used for your own columns: id, data_source_id, tenant_id, data, priority, row_index, row_number, created_at, and updated_at. A write to one of them is refused.

One exception: a table that already had its own column named row_number before the row number existed (typically a CSV import) keeps it. There, row_number is your column everywhere: reads return your value, a filter on it compares your value, and writes to it work. The row number is then not returned by reads, trigger payloads or the JSON export for that table (the grid and the CSV and Excel exports still show it under #).

The CRUD operations

There are five underlying operations. The workflow builder and the agent-facing table tool label them slightly differently, but they map onto the same behavior:

Table operations in the builder and the agent tool
Builder nodeAgent tool actionWhat it does
Find Rowsquery_rowsQuery rows by a filter and return them as an items list, meant to feed a Split step for per-row parallel processing. Find Rows does not split by itself.
Get Rowquery_rowsQuery rows by a filter and return them as a flat rows list. Find Rows and Get Row are separate builder nodes, but both resolve to the same underlying read.
Create Rowinsert_rowsAdd a new row from a map of column values.
Update Rowupdate_rowsChange columns on every row matching a filter.
Delete Rowdelete_rowsRemove every row matching a filter.
Create Columnadd_columnsAdd a column to the table, optionally backfilling existing rows with a default value.

The agent table tool also exposes table-level actions (create, get, list, update, delete, and present, which opens the table in your side panel), update_column, import, saved-view actions (create_view, update_view, delete_view), and home dashboard pins (pin, unpin), plus help. A table is not published to the marketplace on its own: it travels inside the application or the agent that uses it (see Marketplace).

Column types

A column is one of seventeen types, which control how it's validated and shown:

Column types
TypeNotes
textFree text.
numberNumeric value (stored, but compared as text: see filtering below).
dateDate value.
checkboxBoolean.
selectOne choice from a fixed list. Requires a non-empty options list at creation.
multi_selectSeveral choices from a fixed list. Also requires a non-empty options list.
ratingNumeric rating.
sentimentSentiment value.
progressProgress value.
fileA stored file, shown as a card. See file and image columns below.
imageThe same value as file, shown as a thumbnail.
emailEmail address.
phonePhone number.
urlURL.
vectorEmbedding vector for similarity search. Requires a dimension (1 to 4000). Plan-gated on the managed cloud: see below.
userA workspace member (an owner, an assignee). The cell holds the member's user id.
relationLinks rows of another table of the workspace, or of the same table. The cell holds a list of row ids. See linking tables below.

Linking tables

A relation column links each row to rows of another table: a contact to its company, a deal to its contact. It can also link rows of its own table, such as a task to its parent task. Its configuration names the linked table, the column of that table shown in the cell (a company's name), and whether a cell may link several rows. The cell stores the linked row ids, for example [12]; a write of an id that is not a row of the linked table is refused. The linked table must be in the same workspace.

Column rules

Any column can carry rules, set when the column is created or edited. They are checked on every write, from the grid, a workflow, an agent, or an import alike:

Column rules
RuleEffect
RequiredEvery row needs a value: a write that leaves it empty, or empties it, is refused. Adding a required column to a table that has rows needs a default value.
UniqueNo two rows hold the same value, compared without case or outer spaces (two spellings of one email are one contact). Available on text, number, email, phone, url, select, date, and user columns.
Only listed optionsOn select and multi_select columns: a value outside the options is refused instead of being kept as free text.

A refused write names the column and the rule, and nothing of it is stored. Switching a rule on is refused while existing rows break it, so fix those rows first.

Values are converted to the column type

Every value you write is converted to its column's type: a number column turns "3,14" or "42%" into a number, a checkbox accepts yes or 1, and so on. A value that doesn't fit its column does not fail the write: instead the write reports warnings. Most are harmless normalizations, but a value that can't be parsed leaves the cell empty, and a file reference with nothing to fetch it by is stored but unusable.

In a workflow, read them from the step output as {{table:<label>.output.warnings}}; the agent table tool adds them to its reply. A vector value is the exception: a malformed embedding is never a warning on a write that succeeds. Adding or updating rows then fails the whole write and stores no row; an import, or an agent write that matches rows on a key, refuses only that row.

File and image columns

A file column and an image column hold the same value, a file reference, and differ only in how the grid shows it (a card or a thumbnail). In the grid, a cell can take an upload, an existing file picked from Files, or an external URL. Reads return the file reference as an object (id, path, mimeType, and size appear only when known):

File or image cell value
{
  "_type": "file",
  "id": "b21f6c1e-5a9d-4c1b-9d0e-2f7a8c3e41d2",
  "url": "/api/proxy/files/by-id/b21f6c1e-5a9d-4c1b-9d0e-2f7a8c3e41d2/raw",
  "name": "report.pdf",
  "mimeType": "application/pdf",
  "size": 48213
}

Write a whole file reference (for example {{core:dl.output.file}}) into the cell, not one of its fields. Store files in a file or image column, not a text column: a text column keeps whatever text it was given and gives it back as text. A file cell can't be used as a filter value (it matches nothing).

Filtering: the rule that trips people up

A filter (where) is a bare column name, an operator, and a value: write status, not data.status (a data. prefix is stripped for you). The reserved name id matches a row's primary key, not a column you defined.

Filter operators and how they compare
OperatorHow it compares
>, <, >=, <=As numbers when the value is a number (or a text that is one): amount > 9 keeps 10 and 100, and a cell that is not a number never matches. Any other value compares as text, so write dates as YYYY-MM-DD.
=, !=As numbers when the value is a number (1.5 matches 1.50), as the exact text when it is text ("007" matches only "007").
contains, starts_with, ends_withText search, case ignored. % and _ are plain characters here.
LIKEA pattern you write yourself: % is any text, _ one character. Case-sensitive.
IN, IS NULL, IS NOT NULLIN takes a non-empty list and compares each value as text.

LIKE does not add wildcards automatically: include % or _ in the value yourself (for example %gmail.com), or use ends_with. Aliases are accepted too: ==, EQ, NE, GT, LT, GTE, LTE, ISNULL, and NOTNULL.

Reading & pagination

How many rows a read returns, and how many it can return at most, depends on where it runs from:

Row limits by caller
CallerRows per read
Workflow Get Row nodeUp to 500 rows per read. With no limit set it reads up to 500; a higher limit is lowered to 500.
Workflow Find Rows nodeKeeps 100 rows by default. You can set a higher limit, but the read behind it still returns at most 500 rows.
Agent table tool (query_rows)20 rows by default, up to 10,000. The answer says hasMore when more rows match: read on with offset.
The table grid in the app20 rows per page by default; choose 10, 20, 50, or 100 per page.

Every returned row carries the system fields id, priority, and created_at alongside your own columns. The default sort is priority DESC, id DESC (newest or highest priority first). There is no user-configurable sort on Find Rows or Get Row, so sort downstream in a Code step if you need a specific order. A limit of 0 always returns no rows and does not tell you whether any row matches: to check that a row exists, read with a limit of 1 and test the count.

Writing rows

Insert takes a map of column to value. Update takes a filter plus a non-empty set map: both are required, or the write fails fast. What an empty value stores depends on the column type: a text column keeps an empty text, a number column stores 0, and most other types store no value. In a workflow Update Row step, a column of set whose value is empty is left out, so the row keeps what it holds there.

text
Insert "Save contact":
  columns = { name: "{{trigger:form.output.name}}", email: "{{trigger:form.output.email}}" }

Update "Mark done":
  where = { column: "status", operator: "=", value: "in_progress" }
  set   = { status: "completed" }

Deleting

Delete always needs a where filter: there is no “clear table” operation. To wipe every row, match on the always-present primary key:

text
Delete "Clear table":
  where = { column: "id", operator: "IS NOT NULL" }

Don't create duplicates

The simplest guard is a Unique rule on the column that identifies an item (an email, a message id): a second insert of the same value is refused. An agent can also insert with match_column, which updates the row holding that value and inserts only the new ones. In a workflow, when a re-run must update rather than fail, guard the insert:

  1. Find by a unique key
    Look the item up first with Find Rows, for example where: { column: "message_id", operator: "=", value: "{{trigger:gmail.output.id}}" }.
  2. Decide on the count
    A Decision on {{table:check.output.item_count}} == 0 splits “new” from “already there”.
  3. Insert only on the new branch
    Insert on the new branch; end on the other. The write is now idempotent.

Creating a column

A select or multi_select column restricts values to a set of choices you provide: creation is rejected if that options list is empty. A vector column needs a declared dimension (1 to 4000) and a distance metric. When you add a column with a default value, existing rows that don't already have that key are back-filled with the default automatically.

Views and boards

A table can keep several saved views, shared by everyone in the workspace: a grid with some columns hidden, or a board that groups the rows into columns by a select or a user column (a sales pipeline by stage, tasks by owner). Dragging a card to another column updates that row, with the same rules as any other write. Which view is open is remembered in your browser. A view can also be pinned on the home dashboard.

Importing rows

In the table grid, Import fills the table from a CSV, TSV, Excel, or JSON file (up to 10 MB and 20,000 rows), or from cells pasted from a spreadsheet. The separator is detected, and each source column is matched to a table column by name, created as a new column with a suggested type, or skipped.

Import options
OptionEffect
Add all rowsEvery row of the file is inserted.
Update existing rows, add new onesPick the column under Match rows on (an email, a reference). A row whose value already exists there (case ignored) is updated, and an empty cell keeps what is stored. The others are inserted.
If a row is refusedSkip it and continue reports a row a column rule refuses, with its line number, and goes on. Stop the import stops there and keeps the rows already imported.

Values are converted like any other write ("1 234,50" into a number column becomes 1234.5), and a link column accepts the linked row's shown value, such as a company name, as well as its id. Imported rows do not start the table's row triggers.

A vector column takes a cell holding its numbers as a JSON array, such as [0.12, -0.03, 0.4], of the column's dimension. An empty cell keeps the embedding a row already has, and a cell that is not a valid vector refuses its row. A vector column cannot be the Match rows on column, and a row needs a value besides its embedding.

Exporting a table

In the table grid, Export downloads the rows as CSV, JSON, or Excel: Selected rows (when you selected some), Matching rows (when a search or a filter is on), or All rows. One file holds up to 20,000 rows; a larger export is cut there, and the app says so.

Vector similarity search

Tables store and search embeddings; they don't create them. Generate the embedding with an embeddings API (through an integration or an HTTP Request step) and insert it into the vector column. To prepare documents, an Extract from File step in text mode splits a PDF, Word, HTML, or text file into chunks you can embed one by one (see Files & storage).

A similarity search is a read with a similarity block instead of (or alongside) where: column, queryVector, an optional topK (1 to 100, default 5), and two optional bounds. The query vector must match the column's declared dimension. The distance metric is not part of the query: it is set on the vector column when you create it (cosine by default, or l2 or dot), and it is fixed from then on, like the dimension.

Each returned row carries its distance and, on a cosine or dot column, its similarity: on a cosine column similarity is 1 minus the distance (1 means identical, 0 unrelated), on a dot column it is the inner product. A table column of your own named distance or similarity keeps its value instead. minSimilarity keeps the rows at least that similar (not available on an l2 column), maxDistance the rows at most that far. The older threshold still works and is a strict maximum distance (distance < threshold), not a similarity score; prefer the two bounds above. Combine where with similarity for hybrid search: you get up to topK of the nearest rows that pass the filter.

A vector column holding fewer than 10,000 embeddings (by default) is searched exactly: every true nearest row comes back. A larger column gets an index in the background, after a write or a search, and is then searched through it: much faster, and it can miss a few of the true nearest rows.

A table can hold several vector columns, each with its own dimension and metric (for instance one per embedding model). To move to another model, add a second vector column, fill it, point your searches at it, then drop the first.

When a row changes

Every insert, update, and delete fires a row-changed event once it commits. This is what powers a datasource trigger: a workflow that starts automatically whenever a row in a chosen table is created, updated, or deleted. An update event also carries the row's previous values, so a trigger can compare before and after.

Troubleshooting

Common table problems
SymptomCauseFix
A > or < filter skips rows it should matchEither the value is not a number, so it compares as text (a date not written YYYY-MM-DD), or the cell is not a number (text such as "12 kg"), so a numeric filter skips it.Pass a number, write dates as YYYY-MM-DD, and keep numeric columns free of units.
A delete fails because it has no filterDelete always needs a where filter: there is no clear-table operation.To remove every row, filter on id with IS NOT NULL.
Re-running a workflow creates duplicate rowsThe insert runs every time, even for an item already stored.Look the item up with Find Rows first and insert only when item_count is 0.
A read returns fewer rows than the table holdsA workflow read returns at most 500 rows, and Find Rows keeps 100 by default.Narrow the filter, or page with offset on Get Row.
A stored file comes back as textIt was saved in a text column.Use a file or image column and map the whole file reference into it.
A write succeeds but a cell is emptyThe value could not be converted to the column type. The write reports it in warnings instead of failing.Read the warnings output and fix the value or the column type.

Related pages