Skip to content

feat(tables): folder tools for the Table block, and folders as picker scope - #7391

Open
mzxchandra wants to merge 3 commits into
feat/file-folder-operationsfrom
feat/table-folder-tools
Open

feat(tables): folder tools for the Table block, and folders as picker scope#7391
mzxchandra wants to merge 3 commits into
feat/file-folder-operationsfrom
feat/table-folder-tools

Conversation

@mzxchandra

Copy link
Copy Markdown
Contributor

Summary

Adds the six folder tools to the Table block, so an agent can organize tables and not just read them. Stacked on #7388 — please merge that first.

Tables already had the folder machinery (the five folder operations, their application use cases, and a public v2 route) but no agent-callable tools, so a run could query a table's rows and could not file the table anywhere.

Six tools: table_list_folders, table_create_folder, table_update_folder, table_delete_folder, table_restore_folder, table_move.

Restore is addressed by the path the folder held when it was deleted rather than by id, because that is what restoreTableFolderUseCase takes. The response reports where the folder actually landed, which differs when its old parent is still archived or a live sibling has taken its name.

Executor admission

The folder operations delegated to Copilot only, so every one of these tools would have 403'd. Widened to toolReadOperation / toolWriteOperation(id, 'tables.use'):

Operation Caller
tables.folders.list table_list_folders
tables.folders.create table_create_folder
tables.folders.update table_update_folder
tables.folders.delete table_delete_folder
tables.folders.restore table_restore_folder
tables.update table_move

table_move reuses updateTableUseCase with only folderPath set rather than growing a second way to move a table. It never sets name or description, so no executor path gains rename powers. check:actorless-executor-operations and check:capability-subject both pass: neither the folder use cases nor updateTableUseCase requires a human subject, so an actorless run (schedule, webhook, deployed API) reaches them without an opaque 500.

Dispatch

In-process, as new cases in executeTableTool. The dispatcher keyed every case to a routed contract, which these have no business inventing — there is no HTTP route for them — so it now carries a response schema, and the folder cases pass standalone schemas through parseInternalOperationInput, the path the Knowledge handler already takes.

Reusing the public v2 folder contracts was the alternative and was rejected: recursive there is a z.stringbool() shaped by URL query encoding, and this flag is the guard between deleting one empty folder and deleting a subtree.

None of the tools sends a workspaceId. The executor mints a delegated principal bound to the run's workspace and the operations read it from there, so accepting one would declare a field the server ignores. table_move is still scoped to its table, but through its own schema: getTableContract, which the older scoped tools use, requires the workspaceId these deliberately omit, and reading it that way rejected every move as malformed.

Block surface

Each folder path is a tree selector paired with a manual text entry so a <reference> can be typed on a text surface. The one exception is the Folder that narrows the table picker: it never travels, because a folder cannot stand in for a table the way it stands in for a file, so it is basic-mode only rather than a pair whose advanced half would resolve a reference and have it discarded. It renders above the picker it narrows — below reads as a second choice rather than a filter.

The scope comparison is by decoded segment, never by string prefix: /a/bc starts with /a/b and is not inside it, and a folder genuinely named Q3/Q4 is one level. isFileInFolderScope now delegates to the same predicate instead of keeping a second copy.

Type of Change

  • New feature

Testing

Unit: 553 test files / 9800 tests pass across the touched areas. 8 new test files. All 45 audits pass, plus check:api-validation, check:client-boundary, biome.

Tests deliberately cover the seam between "the schema accepted the field" and "the use case received it" — a tool option that parses and is then dropped looks identical to one that works from both ends.

End-to-end, driven through the real UI as real workflow runs and verified in Postgres:

Step Result
create folder Folder created; audit reads Created table folder "/Q3%20Results" — the canonical percent-encoded path composed from the typed name
move table Q3_Pipeline moved from Reports into Q3 Results
delete folder, cascade off, non-empty Refused with Folder is not empty; both folders intact
delete folder, cascade on Folder and the table inside it both archived
restore folder See the known issue below
Picker narrowing No folder → ["leads","q3_pipeline"]; Folder = Reports["q3_pipeline"]

Reviewers should focus on: the tables.update widening for table_move, and the delete cascade guard (default off, visibility: 'user-only' so a model asked to "clean up" cannot set it on a guess).

Known issue, pre-existing and not from this PR

table_restore_folder throws in local dev only. restoreFolder (lib/folders/orchestration.ts) wraps its body in withFolderTreeLock but drops the tx, so the body queries the global db pool inside the transaction and trips packages/db/tx-tripwire.ts. That tripwire is throw when NODE_ENV !== 'production' and warn in production — re-running the same restore with DB_TX_TRIPWIRE=warn returns {"success":true,"restoredItems":{"folders":1,"tables":1}}.

Not introduced here: the untouched POST /api/v2/tables/folders/restore fails identically, as does the Recently-deleted restore path in lib/resources/orchestration/restore-resource.ts for workflow and knowledge folders. The sibling transitions (create at :397, relocate at :798) both thread tx; restore is the odd one out. The fix means threading a tx through restoreFolderWithoutTreeLock and its per-resource restoreChildren hooks across four resource types, so it belongs in its own PR rather than as a rider on this one.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

… scope

Tables already had the folder machinery - the five folder operations,
their application use cases, and a public v2 route - but no
agent-callable tools, so a run could query a table's rows and could not
file the table anywhere.

Six tools, mirroring the File block's set: `table_list_folders`,
`table_create_folder`, `table_update_folder`, `table_delete_folder`,
`table_restore_folder`, and `table_move`. Restore is addressed by the
path the folder held when it was deleted rather than by id, because that
is what `restoreTableFolderUseCase` takes; the response reports where the
folder actually landed, which differs when its old parent is still
archived or a live sibling has taken its name.

Executor admission. The folder operations delegated to Copilot only, so
every one of these tools would have 403'd. Widened to
`toolReadOperation` / `toolWriteOperation(id, 'tables.use')`:

  tables.folders.list      <- table_list_folders
  tables.folders.create    <- table_create_folder
  tables.folders.update    <- table_update_folder
  tables.folders.delete    <- table_delete_folder
  tables.folders.restore   <- table_restore_folder
  tables.update            <- table_move

`tables.use` rather than `tables.create` throughout: a folder organizes,
it does not add a table, so a workspace with table creation withheld can
still file what it has. `table_move` reuses `updateTableUseCase` with
only `folderPath` set rather than growing a second way to move a table.
Both `check:actorless-executor-operations` and `check:capability-subject`
pass with the widening - neither the folder use cases nor
`updateTableUseCase` requires a human subject, so an actorless run
(schedule, webhook, deployed API) can reach them without a 500.

Dispatch is in-process, as new cases in `executeTableTool`. The
dispatcher keyed every case to a routed contract, which these have no
business inventing - there is no HTTP route for them - so it now carries
a response schema and the folder cases pass standalone schemas through
`parseInternalOperationInput`, the path the Knowledge handler already
takes. Reusing the public v2 folder contracts was the alternative and was
rejected: `recursive` there is a `z.stringbool()` shaped by URL query
encoding, and this flag is the guard between deleting one empty folder
and deleting a subtree.

None of the tools sends a `workspaceId`. The executor mints a delegated
principal bound to the run's workspace and the operations read it from
there, so accepting one would declare a field the server ignores.
`table_move` is still scoped to its table, but through its own schema:
`getTableContract`, which the older scoped tools use, requires the
`workspaceId` these deliberately omit, and reading it that way rejected
every move as malformed.

On `table_v2`, each folder path is a tree selector paired with a manual
text entry so a `<reference>` can be typed on a text surface. The one
exception is the Folder that narrows the table picker: it never travels,
because a folder cannot stand in for a table the way it stands in for a
file, so it is basic-mode only rather than a pair whose advanced half
would resolve a reference and have it discarded. It renders above the
picker it narrows - below reads as a second choice rather than a filter.

The scope comparison is by decoded segment, never by string prefix:
`/a/bc` starts with `/a/b` and is not inside it, and a folder genuinely
named `Q3/Q4` is one level. `isFileInFolderScope` now delegates to the
same predicate instead of keeping a second copy.
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 2, 2026 7:49am UTC

Request Review

@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds agent-callable table-folder operations and folder-scoped table selection, including follow-up fixes that preserve model-supplied operation parameters and clear loaded selections excluded by a new scope.

  • Adds list, create, move, delete, and restore folder tools plus table movement.
  • Extends internal dispatch, operation admission, contracts, generated metadata, and documentation.
  • Adds canonical folder-scope filtering and targeted regression coverage for both previously reported issues.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/blocks/blocks/table_v2.ts Adds the six folder-oriented block operations and preserves model-supplied paths while giving explicit authored canvas values precedence.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/table-selector.tsx Filters table choices by folder scope and persistently clears a loaded selection when the selected scope excludes it.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table-selector/scope.ts Implements segment-aware folder containment with fail-open handling for malformed paths and incomplete folder caches.
apps/sim/lib/internal/table/execute-tool.ts Dispatches the new table-folder tools through internal operation schemas and handlers.
apps/sim/lib/internal/tool-operations/registry.server.ts Expands executor admission for the table read and write operations required by the new tools.
apps/sim/tools/table/folders.ts Defines the agent-facing table-folder tools and their structured operation results.

Sequence Diagram

sequenceDiagram
  participant Agent
  participant Block as Table block transformer
  participant Dispatcher as Internal table dispatcher
  participant UseCase as Table/folder use case
  Agent->>Block: Folder operation parameters
  Block->>Dispatcher: Validated canonical paths and options
  Dispatcher->>UseCase: Execute workspace-scoped operation
  UseCase-->>Dispatcher: Updated folder/table result
  Dispatcher-->>Agent: Structured tool response
Loading

Reviews (2): Last reviewed commit: "fix(tables): keep a model's folder desti..." | Re-trigger Greptile

Comment thread apps/sim/blocks/blocks/table_v2.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 26 files

Fix all with cubic | Re-trigger cubic

Comment thread apps/sim/blocks/blocks/table_v2.ts
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…ble selection

Two review findings, both on the block surface.

A block used as an Agent tool is handed the TOOL's schema, not this
block's subblock ids - `createLLMToolSchema(toolConfig, ...)` in
`providers/utils.ts` - so a model answers with `path` / `folderPath`
while the canvas stores `folderRef` / `moveTargetRef`. Every folder
transformer read only the canvas id and dropped the model's answer. Five
of them then failed schema validation, which is loud and recoverable.
`move` did not: the tool substitutes the workspace root for an absent
destination, so "move into /Reports" became "move to the root" with a
success result. The canvas value still wins where the author set one;
the model's answer is the fallback, matching what `columns` already does
in this file. `update_folder` branches on the canvas source instead,
because its destination is composed rather than picked and an unset
parent is a real destination (the root), not a missing one. The delete
cascade deliberately gets no model fallback - it is `user-only` on both
the tool param and the subblock.

Narrowing the folder scope filtered the selected table out of the picker
without touching the stored value, so the combobox showed its
placeholder while the block kept executing against the hidden table. The
selection is now cleared when it falls out of scope - but only for a
table that is actually loaded and actually out of scope, since a table
missing from the list is still-loading or deleted, and clearing there
would destroy a valid config over a transient cache state.
@mzxchandra

Copy link
Copy Markdown
Contributor Author

@greptile

@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings.

@mzxchandra

Copy link
Copy Markdown
Contributor Author

@cubic review

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@cubic review

@mzxchandra I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 26 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant