diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx
index 80798ad2b0..6270761fc4 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx
@@ -1330,3 +1330,33 @@ describe('parseSpecialTags sim_key placeholder', () => {
}
})
})
+
+describe('recoverTrailingBareOptions', () => {
+ const bareOptions =
+ '{"1": {"title": "Fix the tracker", "description": "debug"}, "2": {"title": "Inspect the miss", "description": "look"}}'
+
+ it('renders a trailing bare-JSON options payload as an options card', () => {
+ const { segments } = parseSpecialTags(`Here they are.\n${bareOptions}`, false)
+ const last = segments[segments.length - 1]
+ expect(last.type).toBe('options')
+ if (last.type === 'options') {
+ expect(last.data['1']?.title).toBe('Fix the tracker')
+ }
+ expect(segments[0]).toEqual({ type: 'text', content: 'Here they are.' })
+ })
+
+ it('never recovers mid-stream — a partial JSON tail must not flicker into a card', () => {
+ const { segments } = parseSpecialTags(`Here they are.\n${bareOptions}`, true)
+ expect(segments.every((segment) => segment.type === 'text')).toBe(true)
+ })
+
+ it('leaves ordinary JSON prose alone', () => {
+ const { segments } = parseSpecialTags('The config is {"retries": 3, "mode": "fast"}', false)
+ expect(segments.every((segment) => segment.type === 'text')).toBe(true)
+ })
+
+ it('does not double-render when a real options tag already parsed', () => {
+ const { segments } = parseSpecialTags(`Pick one ${bareOptions}`, false)
+ expect(segments.filter((segment) => segment.type === 'options')).toHaveLength(1)
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx
index 0562ce7c61..21596a05ea 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx
@@ -1442,8 +1442,51 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS
segments.push({ type: 'text', content })
}
+ if (!isStreaming) {
+ recoverTrailingBareOptions(segments)
+ }
+
return { segments, hasPendingTag }
}
+/**
+ * Recovers a trailing bare-JSON options payload the model emitted WITHOUT the
+ * `` wrapper (observed when an automation prompt asks the model to
+ * "(re)send suggested actions" and it answers with the JSON as content). The
+ * shape check is strict — a non-empty object whose every value is
+ * { title, description } with numeric-string keys — so ordinary JSON in prose
+ * cannot false-positive. Only a message's FINAL text segment is considered,
+ * mirroring the tag contract (options go last), and only when no options tag
+ * already parsed. Never applied mid-stream: a partial JSON tail must not
+ * flicker between prose and a card.
+ */
+function recoverTrailingBareOptions(segments: ContentSegment[]): void {
+ const last = segments[segments.length - 1]
+ if (!last || last.type !== 'text') return
+ if (segments.some((segment) => segment.type === 'options')) return
+ const text = last.content
+ if (!text.trimEnd().endsWith('}')) return
+ // The payload nests objects, so the START brace is the first one from which
+ // the remainder parses — probe brace positions left to right (bounded).
+ let start = -1
+ let parsed: unknown
+ let probe = text.indexOf('{')
+ for (let attempts = 0; probe !== -1 && attempts < 20; attempts++) {
+ try {
+ parsed = JSON.parse(text.slice(probe).trim())
+ start = probe
+ break
+ } catch {
+ probe = text.indexOf('{', probe + 1)
+ }
+ }
+ if (start === -1) return
+ if (!isOptionsTagData(parsed) || Object.keys(parsed as object).length === 0) return
+ if (!Object.keys(parsed as object).every((key) => /^\d+$/.test(key))) return
+ const prefix = text.slice(0, start).replace(/\s+$/, '')
+ segments.pop()
+ if (prefix) segments.push({ type: 'text', content: prefix })
+ segments.push({ type: 'options', data: parsed })
+}
interface SpecialTagsProps {
segment: Exclude
diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts
index 91fe4c4c2e..3d522d7035 100644
--- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts
+++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts
@@ -113,6 +113,11 @@ export interface ToolCatalogEntry {
| 'set_global_workflow_variables'
| 'share_file'
| 'table'
+ | 'table_automations'
+ | 'table_columns'
+ | 'table_enrichments'
+ | 'table_manage'
+ | 'table_rows'
| 'terminal'
| 'update_deployment_version'
| 'update_workspace_mcp_server'
@@ -229,6 +234,11 @@ export interface ToolCatalogEntry {
| 'set_global_workflow_variables'
| 'share_file'
| 'table'
+ | 'table_automations'
+ | 'table_columns'
+ | 'table_enrichments'
+ | 'table_manage'
+ | 'table_rows'
| 'terminal'
| 'update_deployment_version'
| 'update_workspace_mcp_server'
@@ -2067,7 +2077,7 @@ export const EnrichmentRun: ToolCatalogEntry = {
enrichmentId: {
type: 'string',
description:
- "Which enrichment to run. Discover the full set and each one's inputs/outputs via user_table.list_enrichments.",
+ "Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.",
enum: [
'work-email',
'phone-number',
@@ -4900,6 +4910,505 @@ export const Table: ToolCatalogEntry = {
internal: true,
}
+export const TableAutomations: ToolCatalogEntry = {
+ id: 'table_automations',
+ name: 'table_automations',
+ route: 'sim',
+ mode: 'async',
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ autoRun: {
+ type: 'boolean',
+ description:
+ "On add: true fires dep-satisfied rows immediately (only when the user explicitly asked); default false stages silently. On update: toggles the group's auto-fire on dep satisfaction.",
+ },
+ blockId: {
+ type: 'string',
+ description: 'Source block ID inside the workflow (add_workflow_group_output)',
+ },
+ columnName: {
+ type: 'string',
+ description:
+ 'Target column name: required for delete_workflow_group_output (the bound column to drop); optional for add_workflow_group_output (auto-derived from path)',
+ },
+ dependencies: {
+ type: 'object',
+ description:
+ 'Dependencies before a row runs: { columns?: string[] } of input column names that must be filled. Output columns of upstream groups are valid; a group cannot depend on its own outputs.',
+ properties: {
+ columns: {
+ type: 'array',
+ description: 'Input column names that must be filled before the group runs a row.',
+ items: { type: 'string' },
+ },
+ },
+ },
+ deploymentMode: {
+ type: 'string',
+ description:
+ 'Which workflow version rows execute: "live" (default, editable draft — edits take effect immediately) or "deployed" (latest active deployment; fails if the workflow was never deployed).',
+ enum: ['live', 'deployed'],
+ },
+ groupId: {
+ type: 'string',
+ description:
+ 'Workflow group ID (required for update_workflow_group, delete_workflow_group, add_workflow_group_output, delete_workflow_group_output)',
+ },
+ groupIds: {
+ type: 'array',
+ description: 'Workflow group IDs to fire (required for run_column, non-empty)',
+ items: { type: 'string' },
+ },
+ mappingUpdates: {
+ type: 'array',
+ description:
+ 'Surgical per-output remap for update_workflow_group: each entry repoints ONE existing output column to a new (blockId, path) without touching the rest. Stale cells clear and backfill from saved execution logs where possible. Discover valid pairs via list_workflow_outputs first.',
+ items: {
+ type: 'object',
+ properties: {
+ blockId: { type: 'string', description: 'New source block ID for this column.' },
+ columnName: {
+ type: 'string',
+ description: 'The existing output column to remap (must be bound to this group).',
+ },
+ path: { type: 'string', description: 'New dotted output path on the new block.' },
+ },
+ required: ['columnName', 'blockId', 'path'],
+ },
+ },
+ name: {
+ type: 'string',
+ description: 'Display name for the group (optional on add/update)',
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ outputs: {
+ type: 'array',
+ description:
+ 'Outputs to surface as columns for add_workflow_group: each { blockId, path, columnName?, columnType? }; columnName auto-derives from path, columnType from the leaf type. Validated against list_workflow_outputs — invalid picks return the valid options. For update_workflow_group prefer add/delete_workflow_group_output and mappingUpdates; pass outputs only to restructure the whole set.',
+ items: {
+ type: 'object',
+ properties: {
+ blockId: { type: 'string', description: 'Source block ID inside the workflow.' },
+ columnName: {
+ type: 'string',
+ description:
+ 'Optional target column name; auto-derived from the path when omitted.',
+ },
+ columnType: {
+ type: 'string',
+ description: 'Optional column type; defaults from the leaf type when omitted.',
+ enum: ['string', 'number', 'boolean', 'date', 'json'],
+ },
+ path: { type: 'string', description: 'Dotted output path on the block.' },
+ },
+ required: ['blockId', 'path'],
+ },
+ },
+ path: {
+ type: 'string',
+ description: 'Dotted output path on the block (add_workflow_group_output)',
+ },
+ rowId: { type: 'string', description: 'Row ID for cancel_table_runs with scope "row".' },
+ rowIds: {
+ type: 'array',
+ description:
+ 'Optional row scope for run_column: only these rows are candidates (server eligibility still applies); omit for the whole table.',
+ items: { type: 'string' },
+ },
+ runMode: {
+ type: 'string',
+ description:
+ 'Run mode for run_column: "incomplete" (default) re-runs only rows with no output or a last failure; "all" re-runs every dep-satisfied row.',
+ enum: ['incomplete', 'all'],
+ },
+ scope: {
+ type: 'string',
+ description:
+ 'Cancellation scope for cancel_table_runs: "all" (whole table) or "row" (requires rowId).',
+ enum: ['all', 'row'],
+ },
+ tableId: {
+ type: 'string',
+ description: 'Table ID (required for everything except list_workflow_outputs)',
+ },
+ workflowId: {
+ type: 'string',
+ description: 'Workflow ID (required for add_workflow_group and list_workflow_outputs)',
+ },
+ },
+ },
+ operation: {
+ type: 'string',
+ description: 'The automation operation to perform',
+ enum: [
+ 'list_workflow_outputs',
+ 'add_workflow_group',
+ 'update_workflow_group',
+ 'delete_workflow_group',
+ 'add_workflow_group_output',
+ 'delete_workflow_group_output',
+ 'run_column',
+ 'cancel_table_runs',
+ ],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: { type: 'object', description: 'Operation-specific result payload.' },
+ message: { type: 'string', description: 'Human-readable outcome summary.' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded.' },
+ },
+ required: ['success', 'message'],
+ },
+}
+
+export const TableColumns: ToolCatalogEntry = {
+ id: 'table_columns',
+ name: 'table_columns',
+ route: 'sim',
+ mode: 'async',
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ column: {
+ type: 'object',
+ description:
+ 'Column definition for add_column: { name, type, unique?, position? }; select (enum) columns also take { options: [names], multiple?: true } — options is required for select.',
+ },
+ columnName: {
+ type: 'string',
+ description:
+ 'Column name (required for rename_column and update_column; single-column delete_column)',
+ },
+ columnNames: {
+ type: 'array',
+ description:
+ 'Array of column names to delete at once (preferred for multi-column delete_column)',
+ },
+ multiple: {
+ type: 'boolean',
+ description:
+ 'Whether a select cell may hold several options (default false). Switching true → false fails if any row has more than one selected.',
+ },
+ newName: { type: 'string', description: 'New column name (required for rename_column)' },
+ newType: {
+ type: 'string',
+ description:
+ 'New column type for update_column: string, number, boolean, date, json, select. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell.',
+ },
+ options: {
+ type: 'array',
+ description:
+ 'Choices for a select (enum) column as display names, e.g. ["Open", "Closed"]. Required when creating or converting to select. On update_column this REPLACES the whole list, matched BY NAME — send the full list including options you keep; omitting one deletes it and clears its cells. Max 100.',
+ items: { type: 'string' },
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ tableId: { type: 'string', description: 'Table ID (required for every operation)' },
+ unique: {
+ type: 'boolean',
+ description:
+ 'Set or clear the column unique constraint (update_column; not supported on select columns)',
+ },
+ },
+ required: ['tableId'],
+ },
+ operation: {
+ type: 'string',
+ description: 'The column operation to perform',
+ enum: ['add_column', 'rename_column', 'delete_column', 'update_column'],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: { type: 'object', description: 'Operation-specific result payload.' },
+ message: { type: 'string', description: 'Human-readable outcome summary.' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded.' },
+ },
+ required: ['success', 'message'],
+ },
+}
+
+export const TableEnrichments: ToolCatalogEntry = {
+ id: 'table_enrichments',
+ name: 'table_enrichments',
+ route: 'sim',
+ mode: 'async',
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ autoRun: {
+ type: 'boolean',
+ description:
+ 'true fires dep-satisfied rows immediately on add (only when the user explicitly asked); default false stages silently — fire later via table_automations run_column.',
+ },
+ dependencies: {
+ type: 'object',
+ description:
+ 'Optional dependency override: { columns?: string[] }; omit to default to the mapped input columns.',
+ properties: {
+ columns: {
+ type: 'array',
+ description:
+ 'Input column names that must be filled before the enrichment runs a row.',
+ items: { type: 'string' },
+ },
+ },
+ },
+ enrichmentId: {
+ type: 'string',
+ description:
+ 'Enrichment registry ID for add_enrichment — discover via list_enrichments (e.g. work-email, phone-number, company-domain, company-info).',
+ },
+ inputMappings: {
+ type: 'array',
+ description:
+ 'For add_enrichment: binds each enrichment input to an existing table column, as { inputName, columnName } where inputName is the enrichment input id from list_enrichments. Provide one for every required input.',
+ items: {
+ type: 'object',
+ properties: {
+ columnName: {
+ type: 'string',
+ description: 'Existing table column that supplies this input.',
+ },
+ inputName: {
+ type: 'string',
+ description: 'Enrichment input id to bind (from list_enrichments).',
+ },
+ },
+ required: ['inputName', 'columnName'],
+ },
+ },
+ name: {
+ type: 'string',
+ description:
+ "Optional display name for the enrichment column group; defaults to the enrichment's registry name.",
+ },
+ outputColumnNames: {
+ type: 'object',
+ description:
+ 'Optional output column name overrides, as { "": "" }; omit for defaults.',
+ additionalProperties: {
+ type: 'string',
+ description: 'Target column name for this enrichment output id.',
+ },
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ tableId: { type: 'string', description: 'Table ID (required for add_enrichment)' },
+ },
+ },
+ operation: {
+ type: 'string',
+ description: 'The enrichment operation to perform',
+ enum: ['list_enrichments', 'add_enrichment'],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: { type: 'object', description: 'Operation-specific result payload.' },
+ message: { type: 'string', description: 'Human-readable outcome summary.' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded.' },
+ },
+ required: ['success', 'message'],
+ },
+}
+
+export const TableManage: ToolCatalogEntry = {
+ id: 'table_manage',
+ name: 'table_manage',
+ route: 'sim',
+ mode: 'async',
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ description: { type: 'string', description: 'Table description (optional for create)' },
+ filePath: {
+ type: 'string',
+ description:
+ 'Canonical workspace file VFS path for create_from_file / import_file, e.g. files/{path}/{name}',
+ },
+ mapping: {
+ type: 'object',
+ description:
+ 'Optional explicit CSV-header → table-column mapping for import_file, as { "csvHeader": "columnName" | null }. null skips that header; omit a header to auto-map by sanitized name.',
+ additionalProperties: {
+ type: ['string', 'null'],
+ description: 'Target column name on the table; null skips that CSV header.',
+ },
+ },
+ mode: {
+ type: 'string',
+ description:
+ 'Import mode for import_file: append (default) adds rows; replace truncates existing rows in a transaction first.',
+ enum: ['append', 'replace'],
+ },
+ name: { type: 'string', description: 'Table name (required for create)' },
+ newName: { type: 'string', description: 'New table name (required for rename)' },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ schema: {
+ type: 'object',
+ description:
+ 'Table schema with a columns array (required for create). Each column: { name, type, unique? }; a select (enum) column also requires options (display names) and takes multiple?.',
+ },
+ tableId: {
+ type: 'string',
+ description: 'Table ID (required for import_file and rename)',
+ },
+ },
+ },
+ operation: {
+ type: 'string',
+ description: 'The lifecycle operation to perform',
+ enum: ['create', 'create_from_file', 'import_file', 'rename'],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: { type: 'object', description: 'Operation-specific result payload.' },
+ message: { type: 'string', description: 'Human-readable outcome summary.' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded.' },
+ },
+ required: ['success', 'message'],
+ },
+}
+
+export const TableRows: ToolCatalogEntry = {
+ id: 'table_rows',
+ name: 'table_rows',
+ route: 'sim',
+ mode: 'async',
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ columnName: {
+ type: 'string',
+ description: 'Column to set when using the values map format of batch_update_rows',
+ },
+ data: {
+ type: 'object',
+ description:
+ 'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME.',
+ },
+ filter: {
+ type: 'object',
+ description:
+ 'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES.',
+ },
+ limit: {
+ type: 'number',
+ description:
+ 'Optional cap on affected rows for the by-filter operations; omit to act on every match.',
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ position: {
+ type: 'integer',
+ description:
+ 'Zero-based index at which to insert the row (optional, insert_row only). Rows at and below shift down; omit to append.',
+ },
+ rowId: { type: 'string', description: 'Row ID (required for update_row, delete_row)' },
+ rowIds: {
+ type: 'array',
+ description: 'Array of row IDs to delete (required for batch_delete_rows)',
+ items: { type: 'string' },
+ },
+ rows: {
+ type: 'array',
+ description: 'Array of row data objects (required for batch_insert_rows)',
+ },
+ tableId: { type: 'string', description: 'Table ID (required for every operation)' },
+ updates: {
+ type: 'array',
+ description:
+ 'Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a)',
+ },
+ values: {
+ type: 'object',
+ description:
+ 'Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName)',
+ },
+ },
+ required: ['tableId'],
+ },
+ operation: {
+ type: 'string',
+ description: 'The row operation to perform',
+ enum: [
+ 'insert_row',
+ 'batch_insert_rows',
+ 'update_row',
+ 'batch_update_rows',
+ 'delete_row',
+ 'batch_delete_rows',
+ 'update_rows_by_filter',
+ 'delete_rows_by_filter',
+ ],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: { type: 'object', description: 'Operation-specific result payload.' },
+ message: { type: 'string', description: 'Human-readable outcome summary.' },
+ success: { type: 'boolean', description: 'Whether the operation succeeded.' },
+ },
+ required: ['success', 'message'],
+ },
+}
+
export const Terminal: ToolCatalogEntry = {
id: 'terminal',
name: 'terminal',
@@ -5788,6 +6297,101 @@ export const SearchKnowledgeBaseOperationValues = [
SearchKnowledgeBaseOperation.listTags,
] as const
+export const TableAutomationsOperation = {
+ listWorkflowOutputs: 'list_workflow_outputs',
+ addWorkflowGroup: 'add_workflow_group',
+ updateWorkflowGroup: 'update_workflow_group',
+ deleteWorkflowGroup: 'delete_workflow_group',
+ addWorkflowGroupOutput: 'add_workflow_group_output',
+ deleteWorkflowGroupOutput: 'delete_workflow_group_output',
+ runColumn: 'run_column',
+ cancelTableRuns: 'cancel_table_runs',
+} as const
+
+export type TableAutomationsOperation =
+ (typeof TableAutomationsOperation)[keyof typeof TableAutomationsOperation]
+
+export const TableAutomationsOperationValues = [
+ TableAutomationsOperation.listWorkflowOutputs,
+ TableAutomationsOperation.addWorkflowGroup,
+ TableAutomationsOperation.updateWorkflowGroup,
+ TableAutomationsOperation.deleteWorkflowGroup,
+ TableAutomationsOperation.addWorkflowGroupOutput,
+ TableAutomationsOperation.deleteWorkflowGroupOutput,
+ TableAutomationsOperation.runColumn,
+ TableAutomationsOperation.cancelTableRuns,
+] as const
+
+export const TableColumnsOperation = {
+ addColumn: 'add_column',
+ renameColumn: 'rename_column',
+ deleteColumn: 'delete_column',
+ updateColumn: 'update_column',
+} as const
+
+export type TableColumnsOperation =
+ (typeof TableColumnsOperation)[keyof typeof TableColumnsOperation]
+
+export const TableColumnsOperationValues = [
+ TableColumnsOperation.addColumn,
+ TableColumnsOperation.renameColumn,
+ TableColumnsOperation.deleteColumn,
+ TableColumnsOperation.updateColumn,
+] as const
+
+export const TableEnrichmentsOperation = {
+ listEnrichments: 'list_enrichments',
+ addEnrichment: 'add_enrichment',
+} as const
+
+export type TableEnrichmentsOperation =
+ (typeof TableEnrichmentsOperation)[keyof typeof TableEnrichmentsOperation]
+
+export const TableEnrichmentsOperationValues = [
+ TableEnrichmentsOperation.listEnrichments,
+ TableEnrichmentsOperation.addEnrichment,
+] as const
+
+export const TableManageOperation = {
+ create: 'create',
+ createFromFile: 'create_from_file',
+ importFile: 'import_file',
+ rename: 'rename',
+} as const
+
+export type TableManageOperation = (typeof TableManageOperation)[keyof typeof TableManageOperation]
+
+export const TableManageOperationValues = [
+ TableManageOperation.create,
+ TableManageOperation.createFromFile,
+ TableManageOperation.importFile,
+ TableManageOperation.rename,
+] as const
+
+export const TableRowsOperation = {
+ insertRow: 'insert_row',
+ batchInsertRows: 'batch_insert_rows',
+ updateRow: 'update_row',
+ batchUpdateRows: 'batch_update_rows',
+ deleteRow: 'delete_row',
+ batchDeleteRows: 'batch_delete_rows',
+ updateRowsByFilter: 'update_rows_by_filter',
+ deleteRowsByFilter: 'delete_rows_by_filter',
+} as const
+
+export type TableRowsOperation = (typeof TableRowsOperation)[keyof typeof TableRowsOperation]
+
+export const TableRowsOperationValues = [
+ TableRowsOperation.insertRow,
+ TableRowsOperation.batchInsertRows,
+ TableRowsOperation.updateRow,
+ TableRowsOperation.batchUpdateRows,
+ TableRowsOperation.deleteRow,
+ TableRowsOperation.batchDeleteRows,
+ TableRowsOperation.updateRowsByFilter,
+ TableRowsOperation.deleteRowsByFilter,
+] as const
+
export const TerminalOperation = {
run: 'run',
read: 'read',
@@ -6008,6 +6612,11 @@ export const TOOL_CATALOG: Record = {
[SetGlobalWorkflowVariables.id]: SetGlobalWorkflowVariables,
[ShareFile.id]: ShareFile,
[Table.id]: Table,
+ [TableAutomations.id]: TableAutomations,
+ [TableColumns.id]: TableColumns,
+ [TableEnrichments.id]: TableEnrichments,
+ [TableManage.id]: TableManage,
+ [TableRows.id]: TableRows,
[Terminal.id]: Terminal,
[UpdateDeploymentVersion.id]: UpdateDeploymentVersion,
[UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer,
diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts
index ccc7a598c0..a3fd31e1c8 100644
--- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts
+++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts
@@ -1971,7 +1971,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
enrichmentId: {
type: 'string',
description:
- "Which enrichment to run. Discover the full set and each one's inputs/outputs via user_table.list_enrichments.",
+ "Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.",
enum: [
'work-email',
'phone-number',
@@ -4749,6 +4749,579 @@ export const TOOL_RUNTIME_SCHEMAS: Record = {
},
resultSchema: undefined,
},
+ table_automations: {
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ autoRun: {
+ type: 'boolean',
+ description:
+ "On add: true fires dep-satisfied rows immediately (only when the user explicitly asked); default false stages silently. On update: toggles the group's auto-fire on dep satisfaction.",
+ },
+ blockId: {
+ type: 'string',
+ description: 'Source block ID inside the workflow (add_workflow_group_output)',
+ },
+ columnName: {
+ type: 'string',
+ description:
+ 'Target column name: required for delete_workflow_group_output (the bound column to drop); optional for add_workflow_group_output (auto-derived from path)',
+ },
+ dependencies: {
+ type: 'object',
+ description:
+ 'Dependencies before a row runs: { columns?: string[] } of input column names that must be filled. Output columns of upstream groups are valid; a group cannot depend on its own outputs.',
+ properties: {
+ columns: {
+ type: 'array',
+ description:
+ 'Input column names that must be filled before the group runs a row.',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ deploymentMode: {
+ type: 'string',
+ description:
+ 'Which workflow version rows execute: "live" (default, editable draft — edits take effect immediately) or "deployed" (latest active deployment; fails if the workflow was never deployed).',
+ enum: ['live', 'deployed'],
+ },
+ groupId: {
+ type: 'string',
+ description:
+ 'Workflow group ID (required for update_workflow_group, delete_workflow_group, add_workflow_group_output, delete_workflow_group_output)',
+ },
+ groupIds: {
+ type: 'array',
+ description: 'Workflow group IDs to fire (required for run_column, non-empty)',
+ items: {
+ type: 'string',
+ },
+ },
+ mappingUpdates: {
+ type: 'array',
+ description:
+ 'Surgical per-output remap for update_workflow_group: each entry repoints ONE existing output column to a new (blockId, path) without touching the rest. Stale cells clear and backfill from saved execution logs where possible. Discover valid pairs via list_workflow_outputs first.',
+ items: {
+ type: 'object',
+ properties: {
+ blockId: {
+ type: 'string',
+ description: 'New source block ID for this column.',
+ },
+ columnName: {
+ type: 'string',
+ description:
+ 'The existing output column to remap (must be bound to this group).',
+ },
+ path: {
+ type: 'string',
+ description: 'New dotted output path on the new block.',
+ },
+ },
+ required: ['columnName', 'blockId', 'path'],
+ },
+ },
+ name: {
+ type: 'string',
+ description: 'Display name for the group (optional on add/update)',
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ outputs: {
+ type: 'array',
+ description:
+ 'Outputs to surface as columns for add_workflow_group: each { blockId, path, columnName?, columnType? }; columnName auto-derives from path, columnType from the leaf type. Validated against list_workflow_outputs — invalid picks return the valid options. For update_workflow_group prefer add/delete_workflow_group_output and mappingUpdates; pass outputs only to restructure the whole set.',
+ items: {
+ type: 'object',
+ properties: {
+ blockId: {
+ type: 'string',
+ description: 'Source block ID inside the workflow.',
+ },
+ columnName: {
+ type: 'string',
+ description:
+ 'Optional target column name; auto-derived from the path when omitted.',
+ },
+ columnType: {
+ type: 'string',
+ description: 'Optional column type; defaults from the leaf type when omitted.',
+ enum: ['string', 'number', 'boolean', 'date', 'json'],
+ },
+ path: {
+ type: 'string',
+ description: 'Dotted output path on the block.',
+ },
+ },
+ required: ['blockId', 'path'],
+ },
+ },
+ path: {
+ type: 'string',
+ description: 'Dotted output path on the block (add_workflow_group_output)',
+ },
+ rowId: {
+ type: 'string',
+ description: 'Row ID for cancel_table_runs with scope "row".',
+ },
+ rowIds: {
+ type: 'array',
+ description:
+ 'Optional row scope for run_column: only these rows are candidates (server eligibility still applies); omit for the whole table.',
+ items: {
+ type: 'string',
+ },
+ },
+ runMode: {
+ type: 'string',
+ description:
+ 'Run mode for run_column: "incomplete" (default) re-runs only rows with no output or a last failure; "all" re-runs every dep-satisfied row.',
+ enum: ['incomplete', 'all'],
+ },
+ scope: {
+ type: 'string',
+ description:
+ 'Cancellation scope for cancel_table_runs: "all" (whole table) or "row" (requires rowId).',
+ enum: ['all', 'row'],
+ },
+ tableId: {
+ type: 'string',
+ description: 'Table ID (required for everything except list_workflow_outputs)',
+ },
+ workflowId: {
+ type: 'string',
+ description:
+ 'Workflow ID (required for add_workflow_group and list_workflow_outputs)',
+ },
+ },
+ },
+ operation: {
+ type: 'string',
+ description: 'The automation operation to perform',
+ enum: [
+ 'list_workflow_outputs',
+ 'add_workflow_group',
+ 'update_workflow_group',
+ 'delete_workflow_group',
+ 'add_workflow_group_output',
+ 'delete_workflow_group_output',
+ 'run_column',
+ 'cancel_table_runs',
+ ],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: {
+ type: 'object',
+ description: 'Operation-specific result payload.',
+ },
+ message: {
+ type: 'string',
+ description: 'Human-readable outcome summary.',
+ },
+ success: {
+ type: 'boolean',
+ description: 'Whether the operation succeeded.',
+ },
+ },
+ required: ['success', 'message'],
+ },
+ },
+ table_columns: {
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ column: {
+ type: 'object',
+ description:
+ 'Column definition for add_column: { name, type, unique?, position? }; select (enum) columns also take { options: [names], multiple?: true } — options is required for select.',
+ },
+ columnName: {
+ type: 'string',
+ description:
+ 'Column name (required for rename_column and update_column; single-column delete_column)',
+ },
+ columnNames: {
+ type: 'array',
+ description:
+ 'Array of column names to delete at once (preferred for multi-column delete_column)',
+ },
+ multiple: {
+ type: 'boolean',
+ description:
+ 'Whether a select cell may hold several options (default false). Switching true → false fails if any row has more than one selected.',
+ },
+ newName: {
+ type: 'string',
+ description: 'New column name (required for rename_column)',
+ },
+ newType: {
+ type: 'string',
+ description:
+ 'New column type for update_column: string, number, boolean, date, json, select. Converting to select also requires options; conversion fails if an existing cell value matches no option. A multiple select round-trips through text as a comma-separated cell.',
+ },
+ options: {
+ type: 'array',
+ description:
+ 'Choices for a select (enum) column as display names, e.g. ["Open", "Closed"]. Required when creating or converting to select. On update_column this REPLACES the whole list, matched BY NAME — send the full list including options you keep; omitting one deletes it and clears its cells. Max 100.',
+ items: {
+ type: 'string',
+ },
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ tableId: {
+ type: 'string',
+ description: 'Table ID (required for every operation)',
+ },
+ unique: {
+ type: 'boolean',
+ description:
+ 'Set or clear the column unique constraint (update_column; not supported on select columns)',
+ },
+ },
+ required: ['tableId'],
+ },
+ operation: {
+ type: 'string',
+ description: 'The column operation to perform',
+ enum: ['add_column', 'rename_column', 'delete_column', 'update_column'],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: {
+ type: 'object',
+ description: 'Operation-specific result payload.',
+ },
+ message: {
+ type: 'string',
+ description: 'Human-readable outcome summary.',
+ },
+ success: {
+ type: 'boolean',
+ description: 'Whether the operation succeeded.',
+ },
+ },
+ required: ['success', 'message'],
+ },
+ },
+ table_enrichments: {
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ autoRun: {
+ type: 'boolean',
+ description:
+ 'true fires dep-satisfied rows immediately on add (only when the user explicitly asked); default false stages silently — fire later via table_automations run_column.',
+ },
+ dependencies: {
+ type: 'object',
+ description:
+ 'Optional dependency override: { columns?: string[] }; omit to default to the mapped input columns.',
+ properties: {
+ columns: {
+ type: 'array',
+ description:
+ 'Input column names that must be filled before the enrichment runs a row.',
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ enrichmentId: {
+ type: 'string',
+ description:
+ 'Enrichment registry ID for add_enrichment — discover via list_enrichments (e.g. work-email, phone-number, company-domain, company-info).',
+ },
+ inputMappings: {
+ type: 'array',
+ description:
+ 'For add_enrichment: binds each enrichment input to an existing table column, as { inputName, columnName } where inputName is the enrichment input id from list_enrichments. Provide one for every required input.',
+ items: {
+ type: 'object',
+ properties: {
+ columnName: {
+ type: 'string',
+ description: 'Existing table column that supplies this input.',
+ },
+ inputName: {
+ type: 'string',
+ description: 'Enrichment input id to bind (from list_enrichments).',
+ },
+ },
+ required: ['inputName', 'columnName'],
+ },
+ },
+ name: {
+ type: 'string',
+ description:
+ "Optional display name for the enrichment column group; defaults to the enrichment's registry name.",
+ },
+ outputColumnNames: {
+ type: 'object',
+ description:
+ 'Optional output column name overrides, as { "": "" }; omit for defaults.',
+ additionalProperties: {
+ type: 'string',
+ description: 'Target column name for this enrichment output id.',
+ },
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ tableId: {
+ type: 'string',
+ description: 'Table ID (required for add_enrichment)',
+ },
+ },
+ },
+ operation: {
+ type: 'string',
+ description: 'The enrichment operation to perform',
+ enum: ['list_enrichments', 'add_enrichment'],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: {
+ type: 'object',
+ description: 'Operation-specific result payload.',
+ },
+ message: {
+ type: 'string',
+ description: 'Human-readable outcome summary.',
+ },
+ success: {
+ type: 'boolean',
+ description: 'Whether the operation succeeded.',
+ },
+ },
+ required: ['success', 'message'],
+ },
+ },
+ table_manage: {
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ description: {
+ type: 'string',
+ description: 'Table description (optional for create)',
+ },
+ filePath: {
+ type: 'string',
+ description:
+ 'Canonical workspace file VFS path for create_from_file / import_file, e.g. files/{path}/{name}',
+ },
+ mapping: {
+ type: 'object',
+ description:
+ 'Optional explicit CSV-header → table-column mapping for import_file, as { "csvHeader": "columnName" | null }. null skips that header; omit a header to auto-map by sanitized name.',
+ additionalProperties: {
+ type: ['string', 'null'],
+ description: 'Target column name on the table; null skips that CSV header.',
+ },
+ },
+ mode: {
+ type: 'string',
+ description:
+ 'Import mode for import_file: append (default) adds rows; replace truncates existing rows in a transaction first.',
+ enum: ['append', 'replace'],
+ },
+ name: {
+ type: 'string',
+ description: 'Table name (required for create)',
+ },
+ newName: {
+ type: 'string',
+ description: 'New table name (required for rename)',
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ schema: {
+ type: 'object',
+ description:
+ 'Table schema with a columns array (required for create). Each column: { name, type, unique? }; a select (enum) column also requires options (display names) and takes multiple?.',
+ },
+ tableId: {
+ type: 'string',
+ description: 'Table ID (required for import_file and rename)',
+ },
+ },
+ },
+ operation: {
+ type: 'string',
+ description: 'The lifecycle operation to perform',
+ enum: ['create', 'create_from_file', 'import_file', 'rename'],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: {
+ type: 'object',
+ description: 'Operation-specific result payload.',
+ },
+ message: {
+ type: 'string',
+ description: 'Human-readable outcome summary.',
+ },
+ success: {
+ type: 'boolean',
+ description: 'Whether the operation succeeded.',
+ },
+ },
+ required: ['success', 'message'],
+ },
+ },
+ table_rows: {
+ parameters: {
+ type: 'object',
+ properties: {
+ args: {
+ type: 'object',
+ description: 'Arguments for the operation',
+ properties: {
+ columnName: {
+ type: 'string',
+ description: 'Column to set when using the values map format of batch_update_rows',
+ },
+ data: {
+ type: 'object',
+ description:
+ 'Row data as column → value pairs (required for insert_row, update_row; the patch object for update_rows_by_filter). Select (enum) cells take the option NAME.',
+ },
+ filter: {
+ type: 'object',
+ description:
+ 'Predicate filter for update_rows_by_filter / delete_rows_by_filter: {"all":[...]} (AND) or {"any":[...]} (OR) of {field, op, value} leaves or nested groups. Ops: eq, ne, gt, gte, lt, lte, in, nin, like, ilike (* wildcard), nlike, nilike, contains, ncontains, startsWith, endsWith, isNull, isNotNull, isEmpty, isNotEmpty. in/nin take a non-empty array. Single-select columns match by eq/ne/in/nin; multiple-select by contains/ncontains — values are option NAMES.',
+ },
+ limit: {
+ type: 'number',
+ description:
+ 'Optional cap on affected rows for the by-filter operations; omit to act on every match.',
+ },
+ outputPath: {
+ type: 'string',
+ description:
+ 'Write this call\'s result to a NEW workspace file instead of returning it (e.g. "files/export.csv"). On success the tool result is REPLACED by a file receipt (fileId, vfsPath, size) — set it only when the file IS the goal. ".csv" serializes rows as a CSV table; ".json"/".txt"/".md"/".html" write pretty-printed JSON of the full result envelope. Missing parent folders are created; an existing path fails.',
+ },
+ position: {
+ type: 'integer',
+ description:
+ 'Zero-based index at which to insert the row (optional, insert_row only). Rows at and below shift down; omit to append.',
+ },
+ rowId: {
+ type: 'string',
+ description: 'Row ID (required for update_row, delete_row)',
+ },
+ rowIds: {
+ type: 'array',
+ description: 'Array of row IDs to delete (required for batch_delete_rows)',
+ items: {
+ type: 'string',
+ },
+ },
+ rows: {
+ type: 'array',
+ description: 'Array of row data objects (required for batch_insert_rows)',
+ },
+ tableId: {
+ type: 'string',
+ description: 'Table ID (required for every operation)',
+ },
+ updates: {
+ type: 'array',
+ description:
+ 'Array of per-row updates: [{ rowId, data: { col: val } }] (batch_update_rows format a)',
+ },
+ values: {
+ type: 'object',
+ description:
+ 'Map of rowId → value for single-column batch update (batch_update_rows format b, with columnName)',
+ },
+ },
+ required: ['tableId'],
+ },
+ operation: {
+ type: 'string',
+ description: 'The row operation to perform',
+ enum: [
+ 'insert_row',
+ 'batch_insert_rows',
+ 'update_row',
+ 'batch_update_rows',
+ 'delete_row',
+ 'batch_delete_rows',
+ 'update_rows_by_filter',
+ 'delete_rows_by_filter',
+ ],
+ },
+ },
+ required: ['operation', 'args'],
+ },
+ resultSchema: {
+ type: 'object',
+ properties: {
+ data: {
+ type: 'object',
+ description: 'Operation-specific result payload.',
+ },
+ message: {
+ type: 'string',
+ description: 'Human-readable outcome summary.',
+ },
+ success: {
+ type: 'boolean',
+ description: 'Whether the operation succeeded.',
+ },
+ },
+ required: ['success', 'message'],
+ },
+ },
terminal: {
parameters: {
type: 'object',
diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts
index 2eb7b1d681..7d87b43221 100644
--- a/apps/sim/lib/copilot/tools/server/router.ts
+++ b/apps/sim/lib/copilot/tools/server/router.ts
@@ -48,6 +48,11 @@ import { generateAudioServerTool } from '@/lib/copilot/tools/server/media/genera
import { generateVideoServerTool } from '@/lib/copilot/tools/server/media/generate-video'
import { searchOnlineServerTool } from '@/lib/copilot/tools/server/other/search-online'
import { queryUserTableServerTool } from '@/lib/copilot/tools/server/table/query-user-table'
+import { tableAutomationsServerTool } from '@/lib/copilot/tools/server/table/table-automations'
+import { tableColumnsServerTool } from '@/lib/copilot/tools/server/table/table-columns'
+import { tableEnrichmentsServerTool } from '@/lib/copilot/tools/server/table/table-enrichments'
+import { tableManageServerTool } from '@/lib/copilot/tools/server/table/table-manage'
+import { tableRowsServerTool } from '@/lib/copilot/tools/server/table/table-rows'
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials'
import { setEnvironmentVariablesServerTool } from '@/lib/copilot/tools/server/user/set-environment-variables'
@@ -172,6 +177,11 @@ const baseServerToolRegistry: Record = {
[enrichmentRunServerTool.name]: enrichmentRunServerTool,
[userTableServerTool.name]: userTableServerTool,
[queryUserTableServerTool.name]: queryUserTableServerTool,
+ [tableManageServerTool.name]: tableManageServerTool,
+ [tableRowsServerTool.name]: tableRowsServerTool,
+ [tableColumnsServerTool.name]: tableColumnsServerTool,
+ [tableAutomationsServerTool.name]: tableAutomationsServerTool,
+ [tableEnrichmentsServerTool.name]: tableEnrichmentsServerTool,
[workspaceFileServerTool.name]: workspaceFileServerTool,
[editContentServerTool.name]: editContentServerTool,
[createFileServerTool.name]: createFileServerTool,
diff --git a/apps/sim/lib/copilot/tools/server/table/table-automations.ts b/apps/sim/lib/copilot/tools/server/table/table-automations.ts
new file mode 100644
index 0000000000..e2b9492940
--- /dev/null
+++ b/apps/sim/lib/copilot/tools/server/table/table-automations.ts
@@ -0,0 +1,49 @@
+import { TableAutomations } from '@/lib/copilot/generated/tool-catalog-v1'
+import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
+import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
+
+type TableAutomationsArgs = {
+ operation: string
+ args?: Record
+}
+
+type TableAutomationsResult = {
+ success: boolean
+ message: string
+ data?: any
+}
+
+const ALLOWED_OPERATIONS = new Set([
+ 'list_workflow_outputs',
+ 'add_workflow_group',
+ 'update_workflow_group',
+ 'delete_workflow_group',
+ 'add_workflow_group_output',
+ 'delete_workflow_group_output',
+ 'run_column',
+ 'cancel_table_runs',
+])
+
+/**
+ * per-row workflow automations slice of the split user_table surface. Copilot access control is a
+ * per-agent tool allowlist, so each slice gets its own tool name with its own
+ * operation contract — enforced here (where execution happens) on top of the
+ * schema enum in the Go catalog. Delegates to the shared user_table executor,
+ * so argument semantics stay identical by construction.
+ */
+export const tableAutomationsServerTool: BaseServerTool<
+ TableAutomationsArgs,
+ TableAutomationsResult
+> = {
+ name: TableAutomations.id,
+ async execute(params: TableAutomationsArgs, context?: ServerToolContext) {
+ const operation = params?.operation
+ if (!ALLOWED_OPERATIONS.has(operation)) {
+ return {
+ success: false,
+ message: `table_automations does not support operation '${operation}' (allowed: list_workflow_outputs, add_workflow_group, update_workflow_group, delete_workflow_group, add_workflow_group_output, delete_workflow_group_output, run_column, cancel_table_runs); other table operations live on their own table_* tools`,
+ }
+ }
+ return userTableServerTool.execute(params, context)
+ },
+}
diff --git a/apps/sim/lib/copilot/tools/server/table/table-columns.ts b/apps/sim/lib/copilot/tools/server/table/table-columns.ts
new file mode 100644
index 0000000000..9e4ebc2f49
--- /dev/null
+++ b/apps/sim/lib/copilot/tools/server/table/table-columns.ts
@@ -0,0 +1,42 @@
+import { TableColumns } from '@/lib/copilot/generated/tool-catalog-v1'
+import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
+import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
+
+type TableColumnsArgs = {
+ operation: string
+ args?: Record
+}
+
+type TableColumnsResult = {
+ success: boolean
+ message: string
+ data?: any
+}
+
+const ALLOWED_OPERATIONS = new Set([
+ 'add_column',
+ 'rename_column',
+ 'delete_column',
+ 'update_column',
+])
+
+/**
+ * column DDL (add/rename/retype/delete) slice of the split user_table surface. Copilot access control is a
+ * per-agent tool allowlist, so each slice gets its own tool name with its own
+ * operation contract — enforced here (where execution happens) on top of the
+ * schema enum in the Go catalog. Delegates to the shared user_table executor,
+ * so argument semantics stay identical by construction.
+ */
+export const tableColumnsServerTool: BaseServerTool = {
+ name: TableColumns.id,
+ async execute(params: TableColumnsArgs, context?: ServerToolContext) {
+ const operation = params?.operation
+ if (!ALLOWED_OPERATIONS.has(operation)) {
+ return {
+ success: false,
+ message: `table_columns does not support operation '${operation}' (allowed: add_column, rename_column, delete_column, update_column); other table operations live on their own table_* tools`,
+ }
+ }
+ return userTableServerTool.execute(params, context)
+ },
+}
diff --git a/apps/sim/lib/copilot/tools/server/table/table-enrichments.ts b/apps/sim/lib/copilot/tools/server/table/table-enrichments.ts
new file mode 100644
index 0000000000..caa317ebdf
--- /dev/null
+++ b/apps/sim/lib/copilot/tools/server/table/table-enrichments.ts
@@ -0,0 +1,40 @@
+import { TableEnrichments } from '@/lib/copilot/generated/tool-catalog-v1'
+import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
+import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
+
+type TableEnrichmentsArgs = {
+ operation: string
+ args?: Record
+}
+
+type TableEnrichmentsResult = {
+ success: boolean
+ message: string
+ data?: any
+}
+
+const ALLOWED_OPERATIONS = new Set(['list_enrichments', 'add_enrichment'])
+
+/**
+ * prebuilt per-row enrichments slice of the split user_table surface. Copilot access control is a
+ * per-agent tool allowlist, so each slice gets its own tool name with its own
+ * operation contract — enforced here (where execution happens) on top of the
+ * schema enum in the Go catalog. Delegates to the shared user_table executor,
+ * so argument semantics stay identical by construction.
+ */
+export const tableEnrichmentsServerTool: BaseServerTool<
+ TableEnrichmentsArgs,
+ TableEnrichmentsResult
+> = {
+ name: TableEnrichments.id,
+ async execute(params: TableEnrichmentsArgs, context?: ServerToolContext) {
+ const operation = params?.operation
+ if (!ALLOWED_OPERATIONS.has(operation)) {
+ return {
+ success: false,
+ message: `table_enrichments does not support operation '${operation}' (allowed: list_enrichments, add_enrichment); other table operations live on their own table_* tools`,
+ }
+ }
+ return userTableServerTool.execute(params, context)
+ },
+}
diff --git a/apps/sim/lib/copilot/tools/server/table/table-manage.ts b/apps/sim/lib/copilot/tools/server/table/table-manage.ts
new file mode 100644
index 0000000000..b8030fc5f4
--- /dev/null
+++ b/apps/sim/lib/copilot/tools/server/table/table-manage.ts
@@ -0,0 +1,37 @@
+import { TableManage } from '@/lib/copilot/generated/tool-catalog-v1'
+import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
+import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
+
+type TableManageArgs = {
+ operation: string
+ args?: Record
+}
+
+type TableManageResult = {
+ success: boolean
+ message: string
+ data?: any
+}
+
+const ALLOWED_OPERATIONS = new Set(['create', 'create_from_file', 'import_file', 'rename'])
+
+/**
+ * table lifecycle (create, create_from_file, import_file, rename) slice of the split user_table surface. Copilot access control is a
+ * per-agent tool allowlist, so each slice gets its own tool name with its own
+ * operation contract — enforced here (where execution happens) on top of the
+ * schema enum in the Go catalog. Delegates to the shared user_table executor,
+ * so argument semantics stay identical by construction.
+ */
+export const tableManageServerTool: BaseServerTool = {
+ name: TableManage.id,
+ async execute(params: TableManageArgs, context?: ServerToolContext) {
+ const operation = params?.operation
+ if (!ALLOWED_OPERATIONS.has(operation)) {
+ return {
+ success: false,
+ message: `table_manage does not support operation '${operation}' (allowed: create, create_from_file, import_file, rename); other table operations live on their own table_* tools`,
+ }
+ }
+ return userTableServerTool.execute(params, context)
+ },
+}
diff --git a/apps/sim/lib/copilot/tools/server/table/table-rows.ts b/apps/sim/lib/copilot/tools/server/table/table-rows.ts
new file mode 100644
index 0000000000..aec5ee0821
--- /dev/null
+++ b/apps/sim/lib/copilot/tools/server/table/table-rows.ts
@@ -0,0 +1,46 @@
+import { TableRows } from '@/lib/copilot/generated/tool-catalog-v1'
+import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool'
+import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
+
+type TableRowsArgs = {
+ operation: string
+ args?: Record
+}
+
+type TableRowsResult = {
+ success: boolean
+ message: string
+ data?: any
+}
+
+const ALLOWED_OPERATIONS = new Set([
+ 'insert_row',
+ 'batch_insert_rows',
+ 'update_row',
+ 'batch_update_rows',
+ 'delete_row',
+ 'batch_delete_rows',
+ 'update_rows_by_filter',
+ 'delete_rows_by_filter',
+])
+
+/**
+ * row data (insert/update/delete, batch and by-filter) slice of the split user_table surface. Copilot access control is a
+ * per-agent tool allowlist, so each slice gets its own tool name with its own
+ * operation contract — enforced here (where execution happens) on top of the
+ * schema enum in the Go catalog. Delegates to the shared user_table executor,
+ * so argument semantics stay identical by construction.
+ */
+export const tableRowsServerTool: BaseServerTool = {
+ name: TableRows.id,
+ async execute(params: TableRowsArgs, context?: ServerToolContext) {
+ const operation = params?.operation
+ if (!ALLOWED_OPERATIONS.has(operation)) {
+ return {
+ success: false,
+ message: `table_rows does not support operation '${operation}' (allowed: insert_row, batch_insert_rows, update_row, batch_update_rows, delete_row, batch_delete_rows, update_rows_by_filter, delete_rows_by_filter); other table operations live on their own table_* tools`,
+ }
+ }
+ return userTableServerTool.execute(params, context)
+ },
+}
diff --git a/apps/sim/lib/copilot/tools/server/table/table-split.test.ts b/apps/sim/lib/copilot/tools/server/table/table-split.test.ts
new file mode 100644
index 0000000000..00233836d3
--- /dev/null
+++ b/apps/sim/lib/copilot/tools/server/table/table-split.test.ts
@@ -0,0 +1,60 @@
+/**
+ * @vitest-environment node
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const executeUserTable = vi.hoisted(() => vi.fn())
+
+vi.mock('@/lib/copilot/tools/server/table/user-table', () => ({
+ userTableServerTool: { execute: executeUserTable },
+}))
+
+import { tableAutomationsServerTool } from '@/lib/copilot/tools/server/table/table-automations'
+import { tableColumnsServerTool } from '@/lib/copilot/tools/server/table/table-columns'
+import { tableEnrichmentsServerTool } from '@/lib/copilot/tools/server/table/table-enrichments'
+import { tableManageServerTool } from '@/lib/copilot/tools/server/table/table-manage'
+import { tableRowsServerTool } from '@/lib/copilot/tools/server/table/table-rows'
+
+/**
+ * Every split tool delegates its own operations to the shared user_table
+ * executor untouched, and rejects operations that belong to a sibling slice
+ * without ever invoking it — the per-slice allowlist is the access contract.
+ */
+describe('split table tools', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ executeUserTable.mockResolvedValue({ success: true, message: 'ok' })
+ })
+
+ const cases = [
+ { tool: tableManageServerTool, own: 'create', foreign: 'insert_row' },
+ { tool: tableRowsServerTool, own: 'batch_update_rows', foreign: 'add_column' },
+ { tool: tableColumnsServerTool, own: 'update_column', foreign: 'create' },
+ { tool: tableAutomationsServerTool, own: 'run_column', foreign: 'add_enrichment' },
+ { tool: tableEnrichmentsServerTool, own: 'add_enrichment', foreign: 'run_column' },
+ ] as const
+
+ it.each(cases)(
+ '$tool.name delegates $own and rejects $foreign',
+ async ({ tool, own, foreign }) => {
+ const context = { userId: 'user-1', workspaceId: 'workspace-1', copilotToolExecution: true }
+ const params = { operation: own, args: { tableId: 'table-1' } }
+
+ await expect(tool.execute(params as never, context as never)).resolves.toEqual({
+ success: true,
+ message: 'ok',
+ })
+ expect(executeUserTable).toHaveBeenCalledWith(params, context)
+
+ executeUserTable.mockClear()
+ await expect(
+ tool.execute({ operation: foreign, args: { tableId: 'table-1' } } as never)
+ ).resolves.toMatchObject({
+ success: false,
+ message: expect.stringContaining(foreign),
+ })
+ expect(executeUserTable).not.toHaveBeenCalled()
+ }
+ )
+})
diff --git a/apps/sim/lib/copilot/tools/tool-display.ts b/apps/sim/lib/copilot/tools/tool-display.ts
index 9d3e30c37c..f15c80c522 100644
--- a/apps/sim/lib/copilot/tools/tool-display.ts
+++ b/apps/sim/lib/copilot/tools/tool-display.ts
@@ -444,6 +444,11 @@ const TOOL_TITLES: Record = {
user_table: 'Managing table',
run_code: 'Running code',
query_user_table: 'Querying table',
+ table_manage: 'Managing table',
+ table_rows: 'Editing table rows',
+ table_columns: 'Editing table columns',
+ table_automations: 'Managing table automations',
+ table_enrichments: 'Managing table enrichments',
workspace_file: 'Editing file',
edit_content: 'Applying file content',
create_workflow: 'Creating workflow',
@@ -682,6 +687,11 @@ export function getToolDisplayTitle(name: string, args?: Record
case 'knowledge_base':
return knowledgeBaseTitle(args)
case 'query_user_table':
+ case 'table_manage':
+ case 'table_rows':
+ case 'table_columns':
+ case 'table_automations':
+ case 'table_enrichments':
return queryUserTableTitle(args)
case 'search_knowledge_base':
return searchKnowledgeBaseTitle(args)