Tables allow you to create and manage custom data tables directly within Agent Serve. Store, query, and manipulate structured data within your workflows without needing external database integrations.
Why Use Tables?
- No external setup: Create tables instantly without configuring external databases
- Workflow-native: Data persists across workflow executions and is accessible from any workflow in your workspace
- Flexible schema: Define columns with types (string, number, currency, boolean, date, json, select) and constraints (required, unique)
- Powerful querying: Filter, sort, and paginate data using a typed predicate grammar
- Agent-friendly: Tables can be used as tools by AI agents for dynamic data storage and retrieval
Key Features:
- Create tables with custom schemas
- Insert, update, upsert, and delete rows
- Query with filters and sorting
- Batch operations for bulk inserts
- Bulk updates and deletes by filter
Creating Tables
Tables are created from the Tables section in the sidebar. Each table requires:
- Name: Alphanumeric with underscores (e.g.,
customer_leads) - Description: Optional description of the table's purpose
- Schema: Define columns with name, type, and optional constraints
Column Types
See Table column types for the supported types and their storage behavior.
Column Constraints
- Required: Column must have a value (cannot be null)
- Unique: Values must be unique across all rows (enables upsert matching)
Usage Instructions
Create and manage custom data tables. Store, query, and manipulate structured data within workflows. Query Rows accepts a plain predicate — {"field":"wins","op":"gte","value":10} — for one condition. Use all (AND) or any (OR) groups for multiple or nested conditions. Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. Order is a sort spec [{"field":"wins","direction":"desc"}]. Query Rows returns every matching row when Limit is omitted (fails if the result exceeds 5MB — add a filter or a Limit). With a Limit, responses page: a non-null nextCursor means more rows exist — pass it back as the cursor. Columns to Return narrows each row to the selected columns (by stable id or name; one that no longer exists is skipped); leave it empty for every column.
Actions
Insert Row
Insert a new row into a table. IMPORTANT: You must use the "data" parameter (not "values", "row", "fields", or other variations) to specify the row contents.
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
data | object | Yes | Row data as JSON object |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether row was inserted |
row | json | Inserted row data |
message | string | Status message |
Batch Insert Rows
Insert multiple rows into a table at once (up to 1000 rows)
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
rows | array | Yes | Array of row data objects (max 1000 rows) |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether rows were inserted |
rows | array | Inserted rows data |
insertedCount | number | Number of rows inserted |
message | string | Status message |
Upsert Row
Insert or update a row based on unique column constraints. If a row with matching unique field exists, update it; otherwise insert a new row. IMPORTANT: You must use the "data" parameter (not "values", "row", "fields", or other variations) to specify the row contents.
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
data | object | Yes | Row data to insert or update |
conflictTarget | string | No | Unique column to match on. Required only when the table has more than one unique column. |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether row was upserted |
row | json | Upserted row data |
operation | string | Operation performed: insert or update |
message | string | Status message |
Update Row
Update an existing row in a table. Supports partial updates - only include the fields you want to change. IMPORTANT: You must use the "data" parameter (not "values", "row", "fields", or other variations) to specify the fields to update.
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
rowId | string | Yes | Row ID to update |
data | object | Yes | Updated row data |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether row was updated |
row | json | Updated row data |
message | string | Status message |
Update Rows by Filter
Update multiple rows that match filter criteria. Data is merged with existing row data.
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
filter | object | Yes | Filter criteria using operators like $eq, $ne, $gt, $lt, $contains, $ncontains, $startsWith, $endsWith, $in, $nin, $empty, etc. |
data | object | Yes | Fields to update (merged with existing data) |
limit | number | No | Maximum number of rows to update (default: no limit, max: 1000) |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether rows were updated |
updatedCount | number | Number of rows updated |
updatedRowIds | array | IDs of updated rows |
message | string | Status message |
Delete Row
Delete a row from a table
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
rowId | string | Yes | Row ID to delete |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether row was deleted |
deletedCount | number | Number of rows deleted |
message | string | Status message |
Delete Rows by Filter
Delete multiple rows that match filter criteria. Use with caution - supports optional limit for safety.
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
filter | object | Yes | Filter criteria using operators like $eq, $ne, $gt, $lt, $contains, $ncontains, $startsWith, $endsWith, $in, $nin, $empty, etc. |
limit | number | No | Maximum number of rows to delete (default: no limit, max: 1000) |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether rows were deleted |
deletedCount | number | Number of rows deleted |
deletedRowIds | array | IDs of deleted rows |
message | string | Status message |
Query Rows
Query rows with a typed predicate filter and cursor pagination. A single filter can be a plain condition: \{"field":"wins","op":"gte","value":10\}. Use all (AND) or any (OR) groups for multiple or nested conditions. Operators: eq, ne, gt, gte, lt, lte, in, nin, like, ilike, nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. Order is a sort spec, e.g. [\{"field":"wins","direction":"desc"\}]. Omit limit to return the entire result — the query fails if it exceeds the 5MB budget (narrow with a filter or set a limit). With a limit, a page can end early at the byte budget: a non-null nextCursor means more rows exist — pass it back as cursor to continue; never infer completion from page size.
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
filter | json | No | Predicate condition, e.g. \{"field":"wins","op":"gte","value":10\}. Use all or any for multiple conditions; omit to match all rows. |
columns | array | No | Stable column IDs or table column names to include in each row data object. Omit or pass an empty array to return all columns. A reference that matches no column is ignored. |
order | json | No | Sort spec, e.g. \[\{"field":"wins","direction":"desc"\}\]. |
limit | number | No | Maximum rows per page. Omit to return the entire matching result — fails if it exceeds the 5MB budget. With a limit, pages may byte-cut early and set nextCursor when more remain. |
cursor | string | No | Opaque pagination cursor returned by a prior query. Omit for the first page. |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether the query succeeded |
rows | array | Query result rows |
rowCount | number | Number of rows returned |
totalCount | number | Total rows matching the predicate (computed on the first page only) |
limit | number | Limit used in the query |
nextCursor | string | Cursor to fetch the next page, or null on the last page |
Get Row
Get a single row by ID
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
rowId | string | Yes | Row ID to retrieve |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether row was retrieved |
row | json | Row data |
message | string | Status message |
Get Schema
Get the schema configuration of a table
Input
| Parameter | Type | Required | Description |
|---|---|---|---|
tableId | string | Yes | Table ID |
Output
| Parameter | Type | Description |
|---|---|---|
success | boolean | Whether schema was retrieved |
name | string | Table name |
columns | array | Column definitions (each includes its stable id) |
columnCount | number | Number of columns |
rowCount | number | Number of rows in the table |
maxRows | number | Max rows per table for the workspace's plan |
message | string | Status message |
Filter Operators
A filter is a predicate. One condition is an object naming a column, an operator, and a value:
{"field": "status", "op": "eq", "value": "active"}| Operator | Description | Example |
|---|---|---|
eq | Equals | {"field": "status", "op": "eq", "value": "active"} |
ne | Not equals | {"field": "status", "op": "ne", "value": "deleted"} |
gt | Greater than | {"field": "age", "op": "gt", "value": 18} |
gte | Greater than or equal | {"field": "score", "op": "gte", "value": 80} |
lt | Less than | {"field": "price", "op": "lt", "value": 100} |
lte | Less than or equal | {"field": "quantity", "op": "lte", "value": 10} |
in | In array | {"field": "status", "op": "in", "value": ["active", "pending"]} |
nin | Not in array | {"field": "type", "op": "nin", "value": ["spam", "blocked"]} |
contains / ncontains | Contains, or does not contain (case-insensitive) | {"field": "email", "op": "contains", "value": "@gmail.com"} |
like / nlike | Pattern match, * wildcard (case-sensitive) | {"field": "name", "op": "like", "value": "Dr.*"} |
ilike / nilike | Pattern match, * wildcard (case-insensitive) | {"field": "name", "op": "ilike", "value": "*jo*"} |
startsWith | Starts with (case-insensitive) | {"field": "name", "op": "startsWith", "value": "Dr."} |
endsWith | Ends with (case-insensitive) | {"field": "file", "op": "endsWith", "value": ".pdf"} |
isNull / isNotNull | Cell is (not) null | {"field": "phone", "op": "isNull"} |
isEmpty / isNotEmpty | Cell is (not) empty | {"field": "phone", "op": "isEmpty"} |
Most columns are scalar (string, number, boolean, date) or opaque JSON; use ilike with *value* for substring matching on text.
Select columns accept only a subset of these operators, and a query using any other operator on one is rejected rather than returning no rows:
| Column | Allowed operators |
|---|---|
| Single-select | eq, ne, in, nin, isEmpty, isNotEmpty |
| Multi-select | contains, ncontains, isEmpty, isNotEmpty |
A multi-select cell holds a list of options, so match it with contains (by option name) rather than ilike.
Combining Filters
Wrap conditions in all for AND:
{
"all": [
{"field": "status", "op": "eq", "value": "active"},
{"field": "age", "op": "gte", "value": 18}
]
}Use any for OR:
{
"any": [
{"field": "status", "op": "eq", "value": "active"},
{"field": "status", "op": "eq", "value": "pending"}
]
}Groups nest, so mixed logic is a group inside a group:
{
"all": [
{"field": "status", "op": "eq", "value": "active"},
{"any": [
{"field": "plan", "op": "eq", "value": "pro"},
{"field": "score", "op": "gte", "value": 90}
]}
]
}Omit the filter entirely to match every row.
Sort Specification
Order is a list of column/direction pairs, applied in order:
[{"field": "createdAt", "direction": "desc"}]Multi-column sorting:
[
{"field": "priority", "direction": "desc"},
{"field": "name", "direction": "asc"}
]Pagination
Omit Limit to return every matching row in one response; the query fails if the result exceeds 5MB, so narrow with a filter rather than guessing a limit.
With a Limit, results page. A page can end at the limit or at the 5MB byte budget, whichever comes first, so a short page does not mean the end. Pass the returned nextCursor back as Cursor to fetch the next page and stop only when nextCursor is null — never infer completion from the row count.
Built-in Columns
Every row automatically includes:
| Column | Type | Description |
|---|---|---|
id | string | Unique row identifier |
createdAt | date | When the row was created |
updatedAt | date | When the row was last modified |
These can be used in filters and sorting.
Limits
| Resource | Limit |
|---|---|
| Tables per workspace | Depends on workspace plan and deployment settings |
| Rows per table | Depends on workspace plan and deployment settings |
| Columns per table | 1,000 |
| Max row size | 400KB by default; configurable on self-hosted deployments |
| String value length | 10,000 characters |
| Query limit | 1,000 rows |
| Batch insert size | 1,000 rows |
| Bulk update/delete | 1,000 rows |
Notes
- Tables are scoped to workspaces and accessible from any workflow within that workspace
- Data persists across workflow executions
- Use unique constraints to enable upsert functionality
- The visual filter/sort builder provides an easy way to construct queries without writing JSON