Tables API
Create, manage, and read rows in user-managed data tables
Tables API
Data tables are tenant-scoped, schema-flexible tables you can use to persist workflow state, build small lookups, or maintain queues that humans resolve out-of-band. The same tables that show up under Data in the Studio (route /data) and that the Data Table block operates on are exposed via this REST API.
Every endpoint requires the standard x-api-key header described in Authentication. Reads require the workflows:read scope; writes require workflows:write.
Every table is owned by exactly one company. The API never returns rows belonging to a different company than the one the API key is scoped to.
Resource Shape
A table:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Processed Invoices",
"description": "Deduplication table for Stripe invoice events",
"columns": [
{ "id": "stripeInvoiceId", "name": "stripeInvoiceId", "type": "text", "required": true },
{ "id": "amountCents", "name": "amountCents", "type": "number" }
],
"schema": {
"columns": [
{ "id": "stripeInvoiceId", "name": "stripeInvoiceId", "type": "text", "required": true },
{ "id": "amountCents", "name": "amountCents", "type": "number" }
]
},
"metadata": {},
"maxRows": 10000,
"rowCount": 47,
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-01-20T08:12:00.000Z"
}A row:
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"data": {
"stripeInvoiceId": "in_1234567890",
"amountCents": 9900
},
"position": null,
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}Column Types
| Type | Description |
|---|---|
text | Free-form string |
number | Numeric value |
boolean | True or false |
date | ISO-8601 date or datetime string |
select | One value from an enumerated set (provide options: string[]) |
url | URL (string) |
email | Email address (string) |
json | Arbitrary nested JSON value |
Columns are advisory — the underlying storage is JSONB and any keys you write in data are preserved, even if they're not declared in columns. The Studio uses columns to render forms and CSV importers; workflows that write rows via the API can include extra fields without redefining the schema.
List Tables
GET /api/v1/tablesReturns every table the API key's company owns, ordered by createdAt descending.
Example
curl -X GET "http://localhost:3000/api/v1/tables" \
-H "x-api-key: YOUR_API_KEY"Response
{
"success": true,
"data": [
{ "id": "...", "name": "Processed Invoices", "...": "..." },
{ "id": "...", "name": "Pending Approvals", "...": "..." }
]
}Create Table
POST /api/v1/tablesRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name (1-255 chars). |
description | string | No | Free-form description. |
columns | object[] | No | Column definitions (see Column Types). |
schema | object | No | Free-form schema metadata. When columns is provided it is merged into schema.columns. |
metadata | object | No | Free-form metadata (used by the Studio for view preferences). |
maxRows | number | No | Row cap (1 - 100,000, default 10,000). |
Example
curl -X POST "http://localhost:3000/api/v1/tables" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Processed Invoices",
"description": "Deduplicate Stripe events",
"columns": [
{ "name": "stripeInvoiceId", "type": "text", "required": true },
{ "name": "amountCents", "type": "number" }
],
"maxRows": 25000
}'Response (201 Created)
{
"success": true,
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Processed Invoices",
"columns": [
{ "id": "stripeInvoiceId", "name": "stripeInvoiceId", "type": "text", "required": true },
{ "id": "amountCents", "name": "amountCents", "type": "number" }
],
"maxRows": 25000,
"rowCount": 0,
"createdAt": "2024-01-15T10:00:00.000Z",
"updatedAt": "2024-01-15T10:00:00.000Z"
}
}Each column without an explicit id is assigned one matching its name so it can be referenced stably even if the display name later changes.
Get Table
GET /api/v1/tables/:idReturns a single table including its current rowCount and columns.
Returns 404 NOT_FOUND if the table does not exist or belongs to another company.
Update Table
PATCH /api/v1/tables/:idPatch one or more table fields. Any field omitted from the body is left unchanged.
Request Body
| Field | Type | Description |
|---|---|---|
name | string | Rename the table. |
description | string | Replace description. |
columns | object[] | Replace schema.columns entirely. |
schema | object | Replace the full schema JSONB. |
metadata | object | Replace metadata JSONB. |
maxRows | number | Adjust the row cap (1 - 100,000). |
columns is a full replacement of schema.columns. To add a column, send the existing columns plus the new one — partial updates are not supported.
Delete Table
DELETE /api/v1/tables/:idDeletes the table and cascades to every row it contains.
{ "success": true, "data": { "deleted": true } }List Rows
GET /api/v1/tables/:id/rowsQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | 1 - 100 rows per page. |
offset | number | 0 | Offset within the result set. |
Rows are returned in position order (when set) then createdAt ascending.
Response
{
"success": true,
"data": [
{
"id": "660e8400-e29b-41d4-a716-446655440001",
"data": { "stripeInvoiceId": "in_1234567890", "amountCents": 9900 },
"position": null,
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
]
}Create Row
POST /api/v1/tables/:id/rowsRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
data | object | Yes | Row payload. Keys beyond declared columns are preserved. |
Returns 201 Created with the new row, or 400 BAD_REQUEST with "Table has reached its maximum row limit of N" when the table is full.
Example
curl -X POST "http://localhost:3000/api/v1/tables/$TABLE_ID/rows" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "data": { "stripeInvoiceId": "in_1234567890", "amountCents": 9900 } }'Update Row
PATCH /api/v1/tables/:id/rows/:rowIdReplaces the entire data field with the value in the request body. To merge fields, fetch the row first and POST the merged object back.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
data | object | Yes | New row payload. Replaces the existing data JSONB. |
Returns 404 NOT_FOUND if the row does not belong to the given table or company.
Delete Row
DELETE /api/v1/tables/:id/rows/:rowIdDeletes one row and decrements rowCount on the table.
{ "success": true, "data": { "deleted": true } }Error Responses
| HTTP | Code | Trigger |
|---|---|---|
400 | BAD_REQUEST | Validation failure or row-limit reached |
401 | UNAUTHORIZED | Missing or invalid x-api-key |
403 | FORBIDDEN | API key lacks workflows:read / workflows:write |
404 | NOT_FOUND | Table or row does not exist for this company |
429 | RATE_LIMITED | Over the read (1000/min) or write (100/min) bucket |
Audit Logging
POST, PATCH, and DELETE on /api/v1/tables/:id emit audit events with actions table.created, table.updated, and table.deleted. Row writes do not emit per-row audit events — query /api/v1/audit-logs for table-level events only.
Use From Workflows
Workflows shouldn't usually call this API directly — use the Data Table block instead. The block dispatches to the same storage through the workflow runtime, which handles tenant scoping, validation, and row-count bookkeeping inside a single transaction.