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..b727fa06bb3 100644 --- a/.agents/skills/add-integration/SKILL.md +++ b/.agents/skills/add-integration/SKILL.md @@ -598,7 +598,15 @@ 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. + + **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 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/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/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 744cbe6aa71..096574ad428 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. 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. @@ -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, 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` | 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,104 @@ 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` / `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 -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..1c9fe27fbb1 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: @@ -43,7 +43,7 @@ Later blocks read these by name: `` is the array, `` 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](#variations) 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,11 +134,11 @@ 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 -Every Table block's input and output is recorded in [logs](/logs-debugging). For a Query block, the log shows the filter and sort it sent and the rows it received. For an Update or Insert, it shows the row data written and the count affected. When a write does nothing or a query comes back empty, the log is where you check the filter and the data shape before looking anywhere else. +Every Table block's input and output is recorded in [logs](/logs-debugging). For a Query block, the log shows the filter and order it sent and the rows it received. For an Update or Insert, it shows the row data written and the count affected. When a write does nothing or a query comes back empty, the log is where you check the filter and the data shape before looking anywhere else. ## Next 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 28f1e9fcacd..72d0b67acc6 100644 --- a/apps/sim/blocks/blocks.test.ts +++ b/apps/sim/blocks/blocks.test.ts @@ -854,6 +854,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..0a3fa162dcf 100644 --- a/apps/sim/blocks/blocks/table.ts +++ b/apps/sim/blocks/blocks/table.ts @@ -212,6 +212,16 @@ 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 {@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', icon: Table, canvasPresentation: { diff --git a/apps/sim/blocks/blocks/table_v2.ts b/apps/sim/blocks/blocks/table_v2.ts index 127e5e7767e..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. @@ -246,11 +239,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/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', 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,