Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/add-block-preview/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<env>-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: '<v2-type>' }` (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)

Expand Down
4 changes: 4 additions & 0 deletions .agents/skills/add-block/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
10 changes: 9 additions & 1 deletion .agents/skills/add-integration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/docs/components/ui/icon-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
stt_v2: STTIcon,
supabase: SupabaseIcon,
table: Table,
table_v2: Table,
tailscale: TailscaleIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,14 +349,14 @@ export const BLOCK_DISPLAY_WORKFLOWS: Record<string, PreviewWorkflow> = {
],
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,
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/components/workflow-preview/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: [
Expand Down Expand Up @@ -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,
Expand All @@ -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: [
Expand Down
123 changes: 81 additions & 42 deletions apps/docs/content/docs/integrations/table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: User-defined data tables
import { BlockInfoCard } from "@/components/ui/block-info-card"

<BlockInfoCard
type="table"
type="table_v2"
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
color="#10B981"
/>

Expand All @@ -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:**
Expand Down Expand Up @@ -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.



Expand Down Expand Up @@ -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\) |
Comment thread
mzxchandra marked this conversation as resolved.
| `limit` | number | Limit used in the query |
| `nextCursor` | string | Cursor to fetch the next page, or null on the last page |
Comment thread
mzxchandra marked this conversation as resolved.

### Get Row

Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading