From 6b876af072fb673fe8a88486e47c26160d4568d9 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Mon, 31 Aug 2026 16:45:27 -0700 Subject: [PATCH 1/7] feat(tables): GA table_v2 and mark the v1 Table block legacy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the gmail_v2 / github_v2 / confluence_v2 / cursor_v2 cutovers: v1 is renamed "(Legacy)", hidden from discovery, and points at its successor; v2 drops the preview gate. Both edits must land together — the registry check fails a sunset block whose replacedBy is still preview, verified by splitting them locally. v1's `triggers.enabled` deliberately stays true. Webhook execution gates on it at runtime rather than on discovery, so flipping it would break every deployed v1 table-trigger workflow. Both versions host the same `table_new_row` trigger and dispatch is provider-keyed, so hiding v1 changes nothing for triggers. No BLOCK_META_REGISTRY entry is needed — meta coverage is required only for `category: 'tools'` blocks and both Table blocks are `category: 'blocks'`. Docs regenerate from v2 now that v1 is skipped as a source, so the generated page moves to the predicate grammar and cursor pagination. The hand-written regions did not, and are updated here: the operator reference and combining examples, the workflow guide's field walkthrough, and the pagination advice — which previously told readers to advance an offset while looping on nextCursor, mixing both versions. Verified in the browser: the toolbar yields a `table_v2` block whose Query Rows shows Cursor/Order; the Agent tool picker stores `type: table_v2, toolId: table_query_rows_v2` and groups it under built-ins; and a pre-existing v1 block still renders its own Offset/Sort fields behind an amber legacy badge. --- apps/docs/components/ui/icon-mapping.ts | 1 + apps/docs/content/docs/integrations/table.mdx | 114 +++++++++++------- .../docs/tables/using-in-workflows.mdx | 21 +++- .../content/docs/workflows/triggers/table.mdx | 2 +- apps/sim/blocks/blocks.test.ts | 31 +++++ apps/sim/blocks/blocks/table.ts | 8 +- apps/sim/blocks/blocks/table_v2.ts | 5 - apps/sim/lib/integrations/icon-mapping.ts | 1 + 8 files changed, 128 insertions(+), 55 deletions(-) diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index c7feb4ab052..14abfde510d 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -546,6 +546,7 @@ export const blockTypeToIconMap: Record = { stt_v2: STTIcon, supabase: SupabaseIcon, table: Table, + table_v2: Table, tailscale: TailscaleIcon, tavily: TavilyIcon, telegram: TelegramIcon, diff --git a/apps/docs/content/docs/integrations/table.mdx b/apps/docs/content/docs/integrations/table.mdx index 744cbe6aa71..b9e9f537d45 100644 --- a/apps/docs/content/docs/integrations/table.mdx +++ b/apps/docs/content/docs/integrations/table.mdx @@ -6,7 +6,7 @@ description: User-defined data tables import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -17,7 +17,7 @@ Tables allow you to create and manage custom data tables directly within Sim. St - **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 MongoDB-style operators +- **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:** @@ -54,7 +54,7 @@ Tables are created from the **Tables** section in the sidebar. Each table requir ## Usage Instructions -Create and manage custom data tables. Store, query, and manipulate structured data within workflows. Query Rows returns every matching row when Limit is omitted and fails if the result exceeds 5MB. +Create and manage custom data tables. Store, query, and manipulate structured data within workflows. @@ -204,29 +204,29 @@ Delete multiple rows that match filter criteria. Use with caution - supports opt ### Query Rows -Query rows from a table with filtering, sorting, and pagination +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, 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` | object | No | Filter conditions \(MongoDB-style operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $ncontains, $startsWith, $endsWith, $empty\) | -| `sort` | object | No | Sort order as \{field: "asc"\|"desc"\} | -| `limit` | number | No | Maximum rows to return. Omit to return every matching row; the query fails if the result exceeds the 5MB response budget. | -| `offset` | number | No | Number of rows to skip \(default: 0\) | +| `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 query succeeded | +| `success` | boolean | Whether the query succeeded | | `rows` | array | Query result rows | | `rowCount` | number | Number of rows returned | -| `totalCount` | number | Total rows matching filter | -| `limit` | number | Limit used in query | -| `offset` | number | Offset used in query | -| `nextCursor` | string | Non-null when more rows match past this page. A page can end early at the byte budget, so this — not a short rowCount — is what says whether more remain. To page, advance offset by rowCount and stop when this is null. | +| `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 @@ -272,65 +272,95 @@ Get the schema configuration of a table {/* MANUAL-CONTENT-START:notes */} ## Filter Operators -Filters use MongoDB-style operators for flexible querying: +A filter is a predicate. One condition is an object naming a column, an operator, and a value: + +```json +{"field": "status", "op": "eq", "value": "active"} +``` | Operator | Description | Example | |----------|-------------|---------| -| `$eq` | Equals | `{"status": {"$eq": "active"}}` or `{"status": "active"}` | -| `$ne` | Not equals | `{"status": {"$ne": "deleted"}}` | -| `$gt` | Greater than | `{"age": {"$gt": 18}}` | -| `$gte` | Greater than or equal | `{"score": {"$gte": 80}}` | -| `$lt` | Less than | `{"price": {"$lt": 100}}` | -| `$lte` | Less than or equal | `{"quantity": {"$lte": 10}}` | -| `$in` | In array | `{"status": {"$in": ["active", "pending"]}}` | -| `$nin` | Not in array | `{"type": {"$nin": ["spam", "blocked"]}}` | -| `$contains` | String contains (case-insensitive) | `{"email": {"$contains": "@gmail.com"}}` | -| `$ncontains` | Does not contain (case-insensitive; matches empty cells) | `{"email": {"$ncontains": "@spam.com"}}` | -| `$startsWith` | Starts with (case-insensitive) | `{"name": {"$startsWith": "Dr."}}` | -| `$endsWith` | Ends with (case-insensitive) | `{"file": {"$endsWith": ".pdf"}}` | -| `$empty` | Cell is empty (`true`) or non-empty (`false`) | `{"phone": {"$empty": true}}` | +| `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` | Contains (case-sensitive) | `{"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 | `{"field": "name", "op": "startsWith", "value": "Dr."}` | +| `endsWith` | Ends with | `{"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"}` | + +Columns are scalar (string, number, boolean, date) or opaque JSON. There are no array columns, so use `ilike` with `*value*` for substring matching. ### Combining Filters -Multiple field conditions are combined with AND logic: +Wrap conditions in `all` for AND: ```json { - "status": "active", - "age": {"$gte": 18} + "all": [ + {"field": "status", "op": "eq", "value": "active"}, + {"field": "age", "op": "gte", "value": 18} + ] } ``` -Use `$or` for OR logic: +Use `any` for OR: ```json { - "$or": [ - {"status": "active"}, - {"status": "pending"} + "any": [ + {"field": "status", "op": "eq", "value": "active"}, + {"field": "status", "op": "eq", "value": "pending"} ] } ``` -## Sort Specification - -Specify sort order with column names and direction: +Groups nest, so mixed logic is a group inside a group: ```json { - "createdAt": "desc" + "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: + +```json +[{"field": "createdAt", "direction": "desc"}] +``` + Multi-column sorting: ```json -{ - "priority": "desc", - "name": "asc" -} +[ + {"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: diff --git a/apps/docs/content/docs/tables/using-in-workflows.mdx b/apps/docs/content/docs/tables/using-in-workflows.mdx index b690f3a6a70..1db0f2b47e3 100644 --- a/apps/docs/content/docs/tables/using-in-workflows.mdx +++ b/apps/docs/content/docs/tables/using-in-workflows.mdx @@ -19,7 +19,7 @@ Throughout this page the running example is a `leads` table with columns `compan A **Table block** performs one operation against one table. The **Operation** dropdown picks the action; the **Table** selector picks the target. The fields below those two change based on the operation you choose. -{/* VISUAL: Table block UI showing the Operation dropdown open, plus the conditional fields that appear for Query Rows (Filter Conditions, Sort Order, Limit, Offset). */} +{/* VISUAL: Table block UI showing the Operation dropdown open, plus the conditional fields that appear for Query Rows (Filter, Order, Columns to Return, Limit, Cursor). */} The operations fall into three groups: @@ -65,15 +65,24 @@ Later blocks read these by name: `` is the array, ` -**Filter Conditions** narrow the result. In the default **Builder** input mode you add rules visually: pick a column, an operator, and a value. Switch the **Input Mode** to **Editor** to write the filter as an object instead, using operators like `$eq`, `$gt`, `$contains`, and `$in`: +**Filter** narrows the result. You can build rules visually - pick a column, an operator, and a value - or write the filter directly as a predicate. One condition names a field, an operator, and a value: ``` -{ status: "unprocessed", createdAt: { $gte: "2026-06-01" } } +{"field": "status", "op": "eq", "value": "unprocessed"} ``` -**Sort Order** orders the result, again visually in Builder mode or as an object in Editor mode, for example `{ createdAt: "desc" }`. **Limit** caps how many rows come back (default 100, max 1000) and **Offset** skips rows for pagination. +Combine conditions with `all` (AND) or `any` (OR), and nest the groups for mixed logic: -{/* VISUAL: Filter Conditions and Sort Order builders, showing a status = unprocessed rule and a createdAt descending sort, with the equivalent Editor-mode object beside them. */} +``` +{"all": [ + {"field": "status", "op": "eq", "value": "unprocessed"}, + {"field": "createdAt", "op": "gte", "value": "2026-06-01"} +]} +``` + +**Order** sorts the result as a list of column/direction pairs, for example `[{"field": "createdAt", "direction": "desc"}]`. **Columns to Return** narrows each row to the fields a downstream step actually needs. **Limit** caps how many rows come back per page, and **Cursor** continues a previous page - see [Paginate large reads](#tips) below. + +{/* VISUAL: Filter and Order builders, showing a status = unprocessed rule and a createdAt descending sort, with the equivalent predicate JSON beside them. */} For a one-off point lookup, use **Get Row by ID** with a single `Row ID`. **Get Schema** returns the table's column definitions, useful when a workflow needs to inspect structure before writing. The full operator list lives in the [Table block reference](/integrations/table). @@ -125,7 +134,7 @@ After the run, the table holds the enriched rows. The next run queries them agai **Iterate row by row.** Wrap a Query → process → update cycle in a [Loop block](/workflows/blocks/loop) to handle one row at a time. This runs sequentially, slower than a batch update but useful when each row needs its own multi-step logic. Inside the loop the Agent reads the current row and an Update Row by ID writes its result. -**Paginate large reads.** Query Rows returns at most 1000 rows, and a page can also end early once its rows reach the response size budget — so a page may come back shorter than your **Limit** even when more rows match. Advance **Offset** by the `rowCount` you actually received, not by the Limit you asked for, and keep going while `nextCursor` is set. Stop when `nextCursor` is null. Stepping by the Limit instead skips whatever a short page left behind. +**Paginate large reads.** Omit **Limit** to get 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**, a page can end at the limit *or* early once its rows reach the 5MB budget — so a short page does not mean the end. Pass the returned `nextCursor` back as **Cursor** and keep going while it is non-null. Stop only when `nextCursor` is null; never infer completion from the row count. ## Inspecting reads and writes diff --git a/apps/docs/content/docs/workflows/triggers/table.mdx b/apps/docs/content/docs/workflows/triggers/table.mdx index 6217056cfc2..92612212dcd 100644 --- a/apps/docs/content/docs/workflows/triggers/table.mdx +++ b/apps/docs/content/docs/workflows/triggers/table.mdx @@ -7,7 +7,7 @@ import { BlockPreview } from '@/components/workflow-preview' The **Table trigger** runs a workflow when a row is inserted or updated in a [Sim table](/tables). Use it to react to data changes — enrich a row when it's added, or send a follow-up when a status column flips. - + ## Configuration diff --git a/apps/sim/blocks/blocks.test.ts b/apps/sim/blocks/blocks.test.ts index 935f70365ea..7df003530d7 100644 --- a/apps/sim/blocks/blocks.test.ts +++ b/apps/sim/blocks/blocks.test.ts @@ -853,6 +853,37 @@ describe.concurrent('Blocks Module', () => { expect(replacement?.hideFromToolbar).not.toBe(true) }) + it('should keep the legacy table block registered but out of discovery', () => { + const legacy = getBlock('table') + const replacement = getBlock('table_v2') + + // Placed instances must keep resolving and executing. + expect(legacy).toBeDefined() + expect(legacy?.tools.access).toContain('table_query_rows') + // ...while the block itself is gone from the toolbar, search, and mentions. + expect(legacy?.hideFromToolbar).toBe(true) + expect(legacy?.sunset).toEqual({ status: 'legacy', replacedBy: 'table_v2' }) + expect(replacement).toBeDefined() + expect(replacement?.hideFromToolbar).not.toBe(true) + // GA: the reveal gate is gone, so it no longer depends on block-visibility. + expect(replacement?.preview).toBeUndefined() + expect(replacement?.tools.access).toContain('table_query_rows_v2') + }) + + /** + * Webhook execution gates on `triggers.enabled` at runtime, not on + * discovery, so hiding v1 must not disable the trigger it hosts — every + * deployed v1 table-trigger workflow depends on it staying live. Both + * versions host the same trigger id. + */ + it("should keep the legacy table block's trigger enabled", () => { + expect(getBlock('table')?.triggers).toEqual({ + enabled: true, + available: ['table_new_row'], + }) + expect(getBlock('table_v2')?.triggers?.available).toContain('table_new_row') + }) + /** * `openai_embeddings` is an alias of `embeddings_openai`, so the legacy * block's runtime payload gained `provider` and `dimensions`. Undeclared, diff --git a/apps/sim/blocks/blocks/table.ts b/apps/sim/blocks/blocks/table.ts index 6f493a322c7..a528b27c2b2 100644 --- a/apps/sim/blocks/blocks/table.ts +++ b/apps/sim/blocks/blocks/table.ts @@ -206,12 +206,18 @@ const SORT_FIELD = ['sortBuilder', 'sort'] as const export const TableBlock: BlockConfig = { type: 'table', - name: 'Table', + name: 'Table (Legacy)', description: 'User-defined data tables', longDescription: 'Create and manage custom data tables. Store, query, and manipulate structured data within workflows. Query Rows returns every matching row when Limit is omitted and fails if the result exceeds 5MB.', docsLink: 'https://docs.sim.ai/integrations/table', category: 'blocks', + // Superseded by table_v2 (GA): hidden from discovery like other legacy _vN + // blocks; existing workflows keep executing it. `triggers.enabled` below + // deliberately stays true — webhook execution gates on it at runtime, so + // flipping it would break every deployed v1 table-trigger workflow. + hideFromToolbar: true, + sunset: { status: 'legacy', replacedBy: 'table_v2' }, bgColor: '#10B981', icon: Table, canvasPresentation: { diff --git a/apps/sim/blocks/blocks/table_v2.ts b/apps/sim/blocks/blocks/table_v2.ts index 127e5e7767e..6bd84fe9a75 100644 --- a/apps/sim/blocks/blocks/table_v2.ts +++ b/apps/sim/blocks/blocks/table_v2.ts @@ -246,11 +246,6 @@ export const TableV2Block: BlockConfig = { - Use Columns to Return to keep only the fields a downstream step needs (e.g. ["col_email","name"]) — the 5MB budget counts only the returned columns, so narrowing columns is another way to fit a large table; leave it empty for every column.`, docsLink: 'https://docs.sim.ai/integrations/table', category: 'blocks', - // Unreleased: hidden from every discovery surface until revealed via the hosted - // `block-visibility` AppConfig document or the `PREVIEW_BLOCKS` env allowlist. - // Placed instances always execute. At GA: drop this, add the BlockMeta + docs, - // and mark v1 `table` superseded. - preview: true, bgColor: '#10B981', icon: Table, canvasPresentation: { diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index aa5a326a0b9..f63362576eb 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -525,6 +525,7 @@ export const blockTypeToIconMap: Record = { stt_v2: STTIcon, supabase: SupabaseIcon, table: Table, + table_v2: Table, tailscale: TailscaleIcon, tavily: TavilyIcon, telegram: TelegramIcon, From a1b79c13f77125c653bad928e4c56ab5d96114a0 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Mon, 31 Aug 2026 17:34:22 -0700 Subject: [PATCH 2/7] fix(tables): restore the docs hero and correct the operator reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings, all consequences of v2 becoming the docs source. `BlockPreview` looks the block type up in a hand-maintained map and renders nothing when it misses, so pointing the trigger page at `table_v2` silently deleted its hero. Move the entry to the new type; the trigger config it displays is identical across both versions. The docs generator extracts `longDescription` with a single-literal regex, so v2's `+`-concatenated string published as its first fragment alone — the page lost every mention of the predicate grammar and cursor pagination. Join it into one literal. v1's was a single literal, which is why this only surfaced now. The hand-written operator table called `contains` case-sensitive. It compiles to ILIKE, as do `startsWith` and `endsWith`; the pre-cutover page had this right, so the rewrite regressed it. Restore the qualifiers and document `ncontains` alongside `contains`. Drop `'table'` from BUILT_IN_TOOL_TYPES: it now only ever reaches blocks that already passed `isAgentToolBlock`, which excludes hidden ones, so the entry is dead. Matches the `file`/`file_v5` precedent the sibling test already asserts. --- .../workflow-preview/block-display-workflows.ts | 8 ++++---- apps/docs/content/docs/integrations/table.mdx | 8 ++++---- apps/sim/blocks/blocks/table_v2.ts | 9 +-------- apps/sim/blocks/utils.test.ts | 5 +++++ apps/sim/blocks/utils.ts | 1 - 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/docs/components/workflow-preview/block-display-workflows.ts b/apps/docs/components/workflow-preview/block-display-workflows.ts index eb456340eac..0685ef2c4b0 100644 --- a/apps/docs/components/workflow-preview/block-display-workflows.ts +++ b/apps/docs/components/workflow-preview/block-display-workflows.ts @@ -349,14 +349,14 @@ export const BLOCK_DISPLAY_WORKFLOWS: Record = { ], edges: [], }, - table: { - id: 'table', + table_v2: { + id: 'table_v2', name: 'Table', blocks: [ { - id: 'table', + id: 'table_v2', name: 'Table', - type: 'table', + type: 'table_v2', bgColor: '#10B981', position: { x: 0, y: 0 }, hideTargetHandle: true, diff --git a/apps/docs/content/docs/integrations/table.mdx b/apps/docs/content/docs/integrations/table.mdx index b9e9f537d45..141b8a9ee0b 100644 --- a/apps/docs/content/docs/integrations/table.mdx +++ b/apps/docs/content/docs/integrations/table.mdx @@ -54,7 +54,7 @@ Tables are created from the **Tables** section in the sidebar. Each table requir ## Usage Instructions -Create and manage custom data tables. Store, query, and manipulate structured data within workflows. +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. @@ -288,11 +288,11 @@ A filter is a predicate. One condition is an object naming a column, an operator | `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` | Contains (case-sensitive) | `{"field": "email", "op": "contains", "value": "@gmail.com"}` | +| `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 | `{"field": "name", "op": "startsWith", "value": "Dr."}` | -| `endsWith` | Ends with | `{"field": "file", "op": "endsWith", "value": ".pdf"}` | +| `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"}` | diff --git a/apps/sim/blocks/blocks/table_v2.ts b/apps/sim/blocks/blocks/table_v2.ts index 6bd84fe9a75..6f0890a3ca8 100644 --- a/apps/sim/blocks/blocks/table_v2.ts +++ b/apps/sim/blocks/blocks/table_v2.ts @@ -226,14 +226,7 @@ export const TableV2Block: BlockConfig = { name: 'Table', description: 'User-defined data tables', longDescription: - '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, 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.', + '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.', bestPractices: ` - To fetch specific rows, use Query Rows with a predicate filter (e.g. {"field":"slack_user_id","op":"in","value":["U1","U2"]}) — do NOT read every row and filter downstream with a Condition block. - Use "Get Row by ID" only when you have the row's id; otherwise filter with a predicate. diff --git a/apps/sim/blocks/utils.test.ts b/apps/sim/blocks/utils.test.ts index 1cf444caa07..1cd89fa4229 100644 --- a/apps/sim/blocks/utils.test.ts +++ b/apps/sim/blocks/utils.test.ts @@ -82,6 +82,11 @@ describe('BUILT_IN_TOOL_TYPES', () => { expect(BUILT_IN_TOOL_TYPES.has('file_v5')).toBe(true) expect(BUILT_IN_TOOL_TYPES.has('file')).toBe(false) }) + + it('classifies the current Table block instead of the legacy Table block', () => { + expect(BUILT_IN_TOOL_TYPES.has('table_v2')).toBe(true) + expect(BUILT_IN_TOOL_TYPES.has('table')).toBe(false) + }) }) const BASE_CLOUD_MODELS: Record = { diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 79ba9185fdf..4351c4cc396 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -661,7 +661,6 @@ export const BUILT_IN_TOOL_TYPES = new Set([ 'tts', 'stt', 'memory', - 'table', 'table_v2', 'webhook_request', 'workflow', From 5797f3ab578a61c578794875c1e3b60e7209877d Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Mon, 31 Aug 2026 17:49:38 -0700 Subject: [PATCH 3/7] docs(tables): finish the v2 cutover's documentation trail Regenerate the two artifacts the `ncontains` commit left stale: the tool metadata bundle and the generated Table integration page both still published the operator list without it, so `bun run tool-metadata:check` failed at HEAD. Point the remaining docs previews at `table_v2`. `BLOCK_DISPLAY_WORKFLOWS` already moved, but the two hand-written table example workflows and the OutputBundle on the workflows guide still named the v1 type, so every table example in the docs described the block the toolbar no longer yields. They render identically today only because both types share an icon-map entry. The workflows guide told readers the log shows "the filter and sort it sent"; v2 sends `order`. Document the `sunset` step in the three skills that describe the v1 -> v2 cutover. All three stopped at `(Legacy)` + `hideFromToolbar`, but `check-block-registry` fails a legacy block with no `replacedBy`, and the amber badge and its click-to-upgrade action read from that field - following the procedure verbatim produced a build failure. Also record the ordering constraint this cutover hit: the v1 `sunset` edit and the v2 `preview` removal must land together, since the check rejects a `replacedBy` that is still preview. Left alone: the academy video previews still show v1 labels. They mirror recorded footage, so correcting the label without a re-record would only make the still disagree with the video it claims to depict. --- .agents/skills/add-block-preview/SKILL.md | 2 +- .agents/skills/add-block/SKILL.md | 4 ++++ .agents/skills/add-integration/SKILL.md | 5 ++++- apps/docs/components/workflow-preview/examples.ts | 8 ++++---- apps/docs/content/docs/integrations/table.mdx | 2 +- apps/docs/content/docs/tables/using-in-workflows.mdx | 4 ++-- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.agents/skills/add-block-preview/SKILL.md b/.agents/skills/add-block-preview/SKILL.md index 158922058e5..c583a54c2cf 100644 --- a/.agents/skills/add-block-preview/SKILL.md +++ b/.agents/skills/add-block-preview/SKILL.md @@ -41,7 +41,7 @@ A revealed block that is not globally GA (`enabled !== true`, or env-revealed) r - GA via config (code cleanup pending): `{ "enabled": true }` — suffix disappears everywhere within ~30s (AppConfig TTL) + client refetch. Same runbook as `feature-flags`: edit the hosted document, `aws appconfig start-deployment` with the `sim--fast` strategy (see the infra README). -5. **GA cleanup:** delete `preview: true` from the block (now visible to self-hosters on their next upgrade), add its `BlockMeta` + regen docs, and drop the AppConfig entry. For a v2 upgrade, this is also when v1 gets `hideFromToolbar: true` (the superseded-version paradigm). +5. **GA cleanup:** delete `preview: true` from the block (now visible to self-hosters on their next upgrade), add its `BlockMeta` + regen docs, and drop the AppConfig entry. For a v2 upgrade, this is also when v1 gets `hideFromToolbar: true` **and** `sunset: { status: 'legacy', replacedBy: '' }` (the superseded-version paradigm). Both edits must land in the **same commit** as the `preview: true` removal — `check-block-registry` fails a sunset block whose `replacedBy` is still `preview`, so splitting them breaks the build in between. Also move the block's `BLOCK_DISPLAY_WORKFLOWS` entry (`apps/docs/components/workflow-preview/block-display-workflows.ts`) to the new type, or `BlockPreview` silently renders nothing on the docs page. ## Kill switch (shipped blocks) diff --git a/.agents/skills/add-block/SKILL.md b/.agents/skills/add-block/SKILL.md index f6a1b854877..7de2fa0e872 100644 --- a/.agents/skills/add-block/SKILL.md +++ b/.agents/skills/add-block/SKILL.md @@ -695,6 +695,10 @@ export const ServiceBlock: BlockConfig = { type: 'service', name: 'Service (Legacy)', hideFromToolbar: true, // Hide from toolbar + // Required: drives the amber legacy badge and its click-to-upgrade action. + // `check-block-registry` fails a legacy block with no `replacedBy`, one whose + // target does not exist, or one whose target is itself sunset or still `preview`. + sunset: { status: 'legacy', replacedBy: 'service_v2' }, // ... rest of config } diff --git a/.agents/skills/add-integration/SKILL.md b/.agents/skills/add-integration/SKILL.md index 9d442edd645..ed19b60487d 100644 --- a/.agents/skills/add-integration/SKILL.md +++ b/.agents/skills/add-integration/SKILL.md @@ -598,7 +598,10 @@ If creating V2 versions (API-aligned outputs): 1. **V2 Tools** - Add `_v2` suffix, version `2.0.0`, flat outputs 2. **V2 Block** - Add `_v2` type, use `createVersionedToolSelector` -3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true` +3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true`, and add + `sunset: { status: 'legacy', replacedBy: '{service}_v2' }` — `check-block-registry` + fails a legacy block with no `replacedBy`, and the amber legacy badge plus its + click-to-upgrade action read from that field 4. **Registry** - Register both versions ```typescript diff --git a/apps/docs/components/workflow-preview/examples.ts b/apps/docs/components/workflow-preview/examples.ts index 639de68608e..dcf10ca36d1 100644 --- a/apps/docs/components/workflow-preview/examples.ts +++ b/apps/docs/components/workflow-preview/examples.ts @@ -139,7 +139,7 @@ export const TABLE_ENRICH_WORKFLOW: PreviewWorkflow = { { id: 'table1', name: 'Table 1', - type: 'table', + type: 'table_v2', bgColor: '#10B981', position: { x: 0, y: 0 }, hideTargetHandle: true, @@ -162,7 +162,7 @@ export const TABLE_ENRICH_WORKFLOW: PreviewWorkflow = { { id: 'table2', name: 'Table 2', - type: 'table', + type: 'table_v2', bgColor: '#10B981', position: { x: 660, y: 0 }, rows: [ @@ -1644,7 +1644,7 @@ export const TABLE_ROUNDTRIP_WORKFLOW: PreviewWorkflow = { { id: 'query', name: 'Table', - type: 'table', + type: 'table_v2', bgColor: '#10B981', position: { x: 0, y: 0 }, hideTargetHandle: true, @@ -1664,7 +1664,7 @@ export const TABLE_ROUNDTRIP_WORKFLOW: PreviewWorkflow = { { id: 'update', name: 'Table', - type: 'table', + type: 'table_v2', bgColor: '#10B981', position: { x: 680, y: 0 }, rows: [ diff --git a/apps/docs/content/docs/integrations/table.mdx b/apps/docs/content/docs/integrations/table.mdx index 141b8a9ee0b..28c9953f4ac 100644 --- a/apps/docs/content/docs/integrations/table.mdx +++ b/apps/docs/content/docs/integrations/table.mdx @@ -204,7 +204,7 @@ Delete multiple rows that match filter criteria. Use with caution - supports opt ### 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, 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. +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 diff --git a/apps/docs/content/docs/tables/using-in-workflows.mdx b/apps/docs/content/docs/tables/using-in-workflows.mdx index 1db0f2b47e3..24d130c3da5 100644 --- a/apps/docs/content/docs/tables/using-in-workflows.mdx +++ b/apps/docs/content/docs/tables/using-in-workflows.mdx @@ -43,7 +43,7 @@ Later blocks read these by name: `` is the array, ` Date: Mon, 31 Aug 2026 18:12:04 -0700 Subject: [PATCH 4/7] docs(tables): restore the select-operator reference lost in the rebase The regenerated page reverted to the "there are no array columns" claim that the select-operator fix corrected. Restore the allowed-operator table. --- apps/docs/content/docs/integrations/table.mdx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/integrations/table.mdx b/apps/docs/content/docs/integrations/table.mdx index 28c9953f4ac..096574ad428 100644 --- a/apps/docs/content/docs/integrations/table.mdx +++ b/apps/docs/content/docs/integrations/table.mdx @@ -296,7 +296,16 @@ A filter is a predicate. One condition is an object naming a column, an operator | `isNull` / `isNotNull` | Cell is (not) null | `{"field": "phone", "op": "isNull"}` | | `isEmpty` / `isNotEmpty` | Cell is (not) empty | `{"field": "phone", "op": "isEmpty"}` | -Columns are scalar (string, number, boolean, date) or opaque JSON. There are no array columns, so use `ilike` with `*value*` for substring matching. +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 From 966d3ac9b917434bfb26c5f7eb5c3f49ff158005 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Mon, 31 Aug 2026 18:20:58 -0700 Subject: [PATCH 5/7] fix(tables): address review on the cutover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile: convert the v1 lifecycle comment to TSDoc. The repo rule is TSDoc-only, and none of the four reference cutover blocks comment here at all, so the `//` form was not following a local convention either. The note is worth keeping as declaration documentation — it records why `triggers.enabled` must stay true. Cubic: fix a link to `#tips`, an anchor that does not exist on the page. The pagination guidance lives under Variations. --- apps/docs/content/docs/tables/using-in-workflows.mdx | 2 +- apps/sim/blocks/blocks/table.ts | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/docs/content/docs/tables/using-in-workflows.mdx b/apps/docs/content/docs/tables/using-in-workflows.mdx index 24d130c3da5..1c9fe27fbb1 100644 --- a/apps/docs/content/docs/tables/using-in-workflows.mdx +++ b/apps/docs/content/docs/tables/using-in-workflows.mdx @@ -80,7 +80,7 @@ Combine conditions with `all` (AND) or `any` (OR), and nest the groups for mixed ]} ``` -**Order** sorts the result as a list of column/direction pairs, for example `[{"field": "createdAt", "direction": "desc"}]`. **Columns to Return** narrows each row to the fields a downstream step actually needs. **Limit** caps how many rows come back per page, and **Cursor** continues a previous page - see [Paginate large reads](#tips) below. +**Order** sorts the result as a list of column/direction pairs, for example `[{"field": "createdAt", "direction": "desc"}]`. **Columns to Return** narrows each row to the fields a downstream step actually needs. **Limit** caps how many rows come back per page, and **Cursor** continues a previous page - see [Paginate large reads](#variations) below. {/* VISUAL: Filter and Order builders, showing a status = unprocessed rule and a createdAt descending sort, with the equivalent predicate JSON beside them. */} diff --git a/apps/sim/blocks/blocks/table.ts b/apps/sim/blocks/blocks/table.ts index a528b27c2b2..bdfd00e91cd 100644 --- a/apps/sim/blocks/blocks/table.ts +++ b/apps/sim/blocks/blocks/table.ts @@ -212,10 +212,14 @@ export const TableBlock: BlockConfig = { 'Create and manage custom data tables. Store, query, and manipulate structured data within workflows. Query Rows returns every matching row when Limit is omitted and fails if the result exceeds 5MB.', docsLink: 'https://docs.sim.ai/integrations/table', category: 'blocks', - // Superseded by table_v2 (GA): hidden from discovery like other legacy _vN - // blocks; existing workflows keep executing it. `triggers.enabled` below - // deliberately stays true — webhook execution gates on it at runtime, so - // flipping it would break every deployed v1 table-trigger workflow. + /** + * Superseded by {@link TableV2Block} (GA): hidden from discovery like other + * legacy `_vN` blocks, while placed instances keep resolving and executing. + * + * `triggers.enabled` below deliberately stays `true`. Webhook execution gates + * on it at runtime rather than on discovery, so flipping it would break every + * deployed v1 table-trigger workflow. + */ hideFromToolbar: true, sunset: { status: 'legacy', replacedBy: 'table_v2' }, bgColor: '#10B981', From a36c01b5fed5565db241e298e1f0a6720d559e8f Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Mon, 31 Aug 2026 18:53:51 -0700 Subject: [PATCH 6/7] docs(skills): require a GA target before adding sunset replacedBy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review finding. The add-integration cutover steps said to add `sunset.replacedBy` without stating the precondition, so an author following them while v2 was still preview-gated would get a `check-block-registry` failure — the exact constraint this cutover hit. The sibling add-block and add-block-preview skills already spell this out; this brings the third in line. --- .agents/skills/add-integration/SKILL.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.agents/skills/add-integration/SKILL.md b/.agents/skills/add-integration/SKILL.md index ed19b60487d..b727fa06bb3 100644 --- a/.agents/skills/add-integration/SKILL.md +++ b/.agents/skills/add-integration/SKILL.md @@ -601,7 +601,12 @@ If creating V2 versions (API-aligned outputs): 3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true`, and add `sunset: { status: 'legacy', replacedBy: '{service}_v2' }` — `check-block-registry` fails a legacy block with no `replacedBy`, and the amber legacy badge plus its - click-to-upgrade action read from that field + click-to-upgrade action read from that field. + + **Only add `replacedBy` once the target is GA.** The same check also fails when + the target is unregistered, itself sunset, or still `preview: true`. If v2 is + preview-gated, leave v1 alone until GA and drop `preview` in the *same commit* + that adds the sunset — splitting them breaks the build in between. 4. **Registry** - Register both versions ```typescript From ab67d3875b850fa28fa4c743e9a26436e9eb63d1 Mon Sep 17 00:00:00 2001 From: Marcus Chandra Date: Mon, 31 Aug 2026 19:11:27 -0700 Subject: [PATCH 7/7] fix(tables): drop the (Legacy) name suffix from the v1 block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The amber legacy badge and its click-to-upgrade action already mark a sunset block in the UI, so the suffix restated it — a placed v1 block rendered a "Table (Legacy)" type tag next to a "legacy" badge saying the same thing. The four earlier cutovers (gmail, github, confluence, cursor) carry the suffix, but the most recent one does not: the slack_v2 GA left v1 named "Slack" and leaned on the badge. Following that. --- apps/sim/blocks/blocks/table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/table.ts b/apps/sim/blocks/blocks/table.ts index bdfd00e91cd..0a3fa162dcf 100644 --- a/apps/sim/blocks/blocks/table.ts +++ b/apps/sim/blocks/blocks/table.ts @@ -206,7 +206,7 @@ const SORT_FIELD = ['sortBuilder', 'sort'] as const export const TableBlock: BlockConfig = { type: 'table', - name: 'Table (Legacy)', + name: 'Table', description: 'User-defined data tables', longDescription: 'Create and manage custom data tables. Store, query, and manipulate structured data within workflows. Query Rows returns every matching row when Limit is omitted and fails if the result exceeds 5MB.',