Skip to content

feat(folders): soft-delete folders and show in Recently Deleted - #4001

Merged
waleedlatif1 merged 12 commits into
stagingfrom
waleedlatif1/sidebar-folders
Apr 7, 2026
Merged

feat(folders): soft-delete folders and show in Recently Deleted#4001
waleedlatif1 merged 12 commits into
stagingfrom
waleedlatif1/sidebar-folders

Conversation

@waleedlatif1

@waleedlatif1waleedlatif1 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Folders are now soft-deleted (archived) instead of permanently removed, matching the existing pattern for workflows, tables, and knowledge bases
  • Users can restore deleted folders (and all their workflows) from Settings > Recently Deleted
  • Delete confirmation dialogs now show "You can restore it from Recently Deleted" for folders and workflows, with muted styling for "This action cannot be undone" where it remains

Changes

  • Schema: Add archivedAt column + index to workflowFolder table
  • Backend: Soft-delete via archivedAt timestamp instead of hard-delete; scope filter on GET /api/folders; new POST /api/folders/[id]/restore endpoint with batch workflow restoration
  • Frontend: Folders tab in Recently Deleted page; updated delete modal messaging; useRestoreFolder mutation hook
  • Styling: Changed "This action cannot be undone" from error-red to muted text across all confirmation dialogs

Test plan

  • Delete a folder with workflows → confirm it disappears from sidebar
  • Navigate to Settings > Recently Deleted > Folders → confirm folder appears
  • Restore folder → confirm it reappears in sidebar with its workflows
  • Delete a nested folder tree → confirm all levels appear in Recently Deleted
  • Restore a child folder whose parent is still deleted → confirm it appears at root
  • Verify delete modal shows restoration messaging for folders, workflows, and mixed selections
  • Verify "This action cannot be undone" uses muted text styling in all dialogs

Folders are now soft-deleted (archived) instead of permanently removed,
matching the existing pattern for workflows, tables, and knowledge bases.
Users can restore folders from Settings > Recently Deleted.
- Add `archivedAt` column to `workflowFolder` schema with index
- Change folder deletion to set `archivedAt` instead of hard-delete
- Add folder restore endpoint (POST /api/folders/[id]/restore)
- Batch-restore all workflows inside restored folders in one transaction
- Add scope filter to GET /api/folders (active/archived)
- Add Folders tab to Recently Deleted settings page
- Update delete modal messaging for restorable items
- Change "This action cannot be undone" styling to muted text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel

vercelBot commented Apr 7, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedApr 7, 2026 2:50am

Request Review

@cursor

cursorBot commented Apr 7, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Introduces new folder archival/restoration paths that touch DB schema, recursive lifecycle logic, and workflow restore behavior; risks are mainly around data consistency (archivedAt scoping, parent/child restoration, and cache invalidation).

Overview
Folders are now soft-deleted (archived) instead of hard-deleted, backed by a new workflow_folder.archived_at column + index, and folder listing now supports an active vs archived scope filter.

Adds end-to-end folder restore support: a new POST /api/folders/[id]/restore endpoint with permission checks, PostHog + audit logging, and transactional recursive restoration of folders and only the workflows archived with the same timestamp (with parent re-rooting when needed).

Updates the UI and client queries so Recently Deleted can show/restore folders, adjusts folder query keys/invalidation for scoped lists, and tweaks confirmation dialogs to reflect archiving/restorability (with muted styling for "This action cannot be undone").

Reviewed by Cursor Bugbot for commit a80cc8b. Configure here.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps

greptile-appsBot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds soft-delete (archive/restore) support for workflow folders, matching the existing pattern already in place for workflows, tables, knowledge bases, and files. Deleted folders and their contents are now moved to an archivedAt-gated state rather than hard-deleted, and appear under Settings > Recently Deleted where they can be restored in one click.

Key implementation highlights:

  • Shared-timestamp batch delete: deleteFolderRecursively threads a single Date object through all recursive archive operations, so every folder row and every workflow in the batch receives an identical archivedAt value.
  • Exact-timestamp restore scoping: restoreFolderRecursively uses eq(workflow.archivedAt, folderArchivedAt) to restore only those workflows archived as part of this specific folder deletion — workflows individually deleted before the folder are left archived.
  • Atomic transaction: The entire restore path (parent reparenting → folder unarchive → workflow / schedule / webhook / chat / form / MCP tool / A2A agent restore) is wrapped in a single db.transaction() with tx threaded through the recursive helper, so any failure rolls back completely.
  • Scope-aware query key: folderKeys.list(workspaceId, scope) adds a scope dimension so 'active' and 'archived' folder queries maintain separate cache entries while sharing a common folderKeys.lists() prefix for efficient targeted invalidation.
  • The AuditAction.FOLDER_RESTORED action and AuditResourceType.FOLDER resource type are both properly defined; the migration adds the nullable archived_at column plus a btree index safely.

Confidence Score: 5/5

Safe to merge — no P0/P1 issues found; implementation is correct and fully atomic with proper transaction wrapping and exact-timestamp restore scoping.

All remaining findings are P2 (a redundant ? modifier on the archivedAt type field). The critical bugs raised in earlier review rounds — non-atomic restore and restoration of independently-deleted workflows — are both fully resolved. The PR follows established soft-delete patterns and is consistent with the existing workflow/table implementations.

apps/sim/stores/folders/types.ts has a minor type precision issue (archivedAt?: Date | null should be archivedAt: Date | null); no files require attention for correctness or safety.

Important Files Changed

FilenameOverview
apps/sim/lib/workflows/orchestration/folder-lifecycle.tsCore soft-delete/restore logic: shared timestamp ensures exact-match restore scoping; full db.transaction() wraps restore atomically with tx threaded through recursion
apps/sim/app/api/folders/route.tsAdded scope query param with isNull/isNotNull filter; correct auth and permission checks preserved throughout
apps/sim/app/api/folders/[id]/restore/route.tsNew restore endpoint with proper auth, write-permission guard, and clean delegation to performRestoreFolder
apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsxFolders tab integrated cleanly alongside existing resource types; restore triggers correct cache invalidation of both active and archived scopes
apps/sim/hooks/queries/folders.tsuseRestoreFolder hook added with correct onSettled invalidation of both folder scopes and workflow lists; new archivedAt field mapped in mapFolder
apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsxDelete modal now shows 'You can restore it from Recently Deleted' for workflow/folder/mixed types; 'This action cannot be undone' correctly retained (muted) for workspace/task
apps/sim/hooks/queries/utils/folder-keys.tsQuery key factory adds scope dimension correctly; lists() prefix enables targeted invalidation across all scopes
apps/sim/stores/folders/types.tsarchivedAt field added but typed as '?: Date
packages/db/schema.tsarchivedAt nullable column + btree index added to workflowFolder table; schema matches migration
packages/db/migrations/0186_greedy_jocasta.sqlAdditive migration: adds archived_at nullable column + btree index to workflow_folder; safe on existing data
apps/sim/lib/workflows/lifecycle.tsarchiveWorkflowsByIdsInWorkspace correctly accepts shared archivedAt timestamp option used by folder deletion
apps/sim/lib/audit/log.tsFOLDER_RESTORED audit action added; FOLDER resource type was already present
apps/sim/lib/posthog/events.tsfolder_restored event added to PostHog event catalog

Sequence Diagram

sequenceDiagram
participant User
participant API as folders API
participant LC as folder-lifecycle.ts
participant DB
Note over User,DB: Soft-Delete (folder + contents)
User->>API: DELETE /api/folders/{id}
API->>LC: performDeleteFolder()
LC->>DB: countWorkflowsInFolderRecursively()
LC->>LC: deleteFolderRecursively(id, ws, timestamp T)
loop Each non-archived child folder
LC->>LC: deleteFolderRecursively(childId, ws, T)
end
LC->>DB: archiveWorkflowsByIdsInWorkspace(ids, archivedAt=T)
LC->>DB: UPDATE workflowFolder SET archivedAt=T
LC->>DB: recordAudit(FOLDER_DELETED)
API->>User: 200 OK
Note over User,DB: Restore flow
User->>API: POST /api/folders/{id}/restore
API->>LC: performRestoreFolder()
LC->>DB: SELECT folder WHERE id AND workspaceId
LC->>DB: BEGIN TRANSACTION
alt parent folder is archived
LC->>DB: UPDATE workflowFolder SET parentId=null
end
LC->>LC: restoreFolderRecursively(id, ws, folderArchivedAt=T, tx)
LC->>DB: UPDATE workflowFolder SET archivedAt=null
LC->>DB: SELECT workflows WHERE archivedAt=T (exact match)
LC->>DB: UPDATE workflow+schedule+webhook+chat+form+mcp+a2a SET archivedAt=null
loop Each child folder WHERE archivedAt=T
LC->>LC: restoreFolderRecursively(childId, ws, T, tx)
end
LC->>DB: COMMIT
LC->>DB: recordAudit(FOLDER_RESTORED)
API->>User: 200 {success, restoredItems}
Loading

Reviews (5): Last reviewed commit: "fix(folders): handle missing parent fold..." | Re-trigger Greptile

Comment threadapps/sim/lib/workflows/orchestration/folder-lifecycle.ts Outdated
Comment threadapps/sim/lib/workflows/orchestration/folder-lifecycle.ts
…workflows
Address two review findings:
- Wrap entire folder restore in a single DB transaction to prevent
partial state if any step fails
- Only restore workflows archived within 5s of the folder's archivedAt,
so individually-deleted workflows are not silently un-deleted
- Add folder_restored to PostHog event map
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 5-second time window for scoping which workflows to restore was
a fragile heuristic (magic number, race-prone, non-deterministic).
Restoring a folder now restores all archived workflows in it, matching
standard trash/recycle-bin behavior. Users can re-delete any workflow
they don't want after restore.
The single-transaction wrapping from the prior commit is kept — that
was a legitimate atomicity fix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
waleedlatif1and others added 2 commits April 6, 2026 18:40
Replace manually created migration with proper drizzle-kit generated
one that includes the snapshot file, fixing CI schema sync check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/workflows/orchestration/folder-lifecycle.ts
…mestamp
Use a single timestamp across the entire folder deletion — folders,
workflows, schedules, webhooks, etc. all get the exact same archivedAt.
On restore, match workflows by exact archivedAt equality with the
folder's timestamp, so individually-deleted workflows are not
silently un-deleted.
- Add optional archivedAt to ArchiveWorkflowOptions (backwards-compatible)
- Pass shared timestamp through deleteFolderRecursively → archiveWorkflowsByIdsInWorkspace
- Filter restore with eq(workflow.archivedAt, folderArchivedAt) instead of isNotNull
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…missing
When individually restoring a workflow from Recently Deleted, check if
its folder still exists and is active. If the folder is archived or
missing, clear folderId so the workflow appears at root instead of
being orphaned (invisible in sidebar).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

Three issues caught by audit:
1. Child folder restore used isNotNull instead of timestamp matching,
so individually-deleted child folders would be incorrectly restored.
Now uses eq(archivedAt, folderArchivedAt) for both workflows AND
child folders — consistent and deterministic.
2. No workspace archived check — could restore a folder into an
archived workspace. Now checks getWorkspaceWithOwner, matching
the existing restoreWorkflow pattern.
3. Re-restoring an already-restored folder returned an error. Now
returns success with zero counts (idempotent).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment threadapps/sim/lib/workflows/orchestration/folder-lifecycle.ts
Ensures optimistic folder objects include archivedAt: null for
consistency with the database schema shape.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/workflows/orchestration/folder-lifecycle.ts Outdated
Comment threadapps/sim/lib/workflows/orchestration/folder-lifecycle.ts
If the parent folder row no longer exists (not just archived), the
restored folder now correctly gets reparented to root instead of
retaining a dangling parentId reference.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@waleedlatif1

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit a80cc8b. Configure here.

@waleedlatif1
waleedlatif1 merged commit 89ae738 into stagingApr 7, 2026
12 checks passed
@waleedlatif1
waleedlatif1 deleted the waleedlatif1/sidebar-folders branch April 7, 2026 03:06
emir-karabeg pushed a commit that referenced this pull request Apr 7, 2026
* feat(folders): soft-delete folders and show in Recently Deleted
Folders are now soft-deleted (archived) instead of permanently removed,
matching the existing pattern for workflows, tables, and knowledge bases.
Users can restore folders from Settings > Recently Deleted.
- Add `archivedAt` column to `workflowFolder` schema with index
- Change folder deletion to set `archivedAt` instead of hard-delete
- Add folder restore endpoint (POST /api/folders/[id]/restore)
- Batch-restore all workflows inside restored folders in one transaction
- Add scope filter to GET /api/folders (active/archived)
- Add Folders tab to Recently Deleted settings page
- Update delete modal messaging for restorable items
- Change "This action cannot be undone" styling to muted text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(testing): add FOLDER_RESTORED to audit mock
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(folders): atomic restore transaction and scope to folder-deleted workflows
Address two review findings:
- Wrap entire folder restore in a single DB transaction to prevent
partial state if any step fails
- Only restore workflows archived within 5s of the folder's archivedAt,
so individually-deleted workflows are not silently un-deleted
- Add folder_restored to PostHog event map
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(folders): simplify restore to remove hacky 5s time window
The 5-second time window for scoping which workflows to restore was
a fragile heuristic (magic number, race-prone, non-deterministic).
Restoring a folder now restores all archived workflows in it, matching
standard trash/recycle-bin behavior. Users can re-delete any workflow
they don't want after restore.
The single-transaction wrapping from the prior commit is kept — that
was a legitimate atomicity fix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(db): regenerate folder soft-delete migration with drizzle-kit
Replace manually created migration with proper drizzle-kit generated
one that includes the snapshot file, fixing CI schema sync check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore(db): fix migration metadata formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(folders): scope restore to folder-deleted workflows via shared timestamp
Use a single timestamp across the entire folder deletion — folders,
workflows, schedules, webhooks, etc. all get the exact same archivedAt.
On restore, match workflows by exact archivedAt equality with the
folder's timestamp, so individually-deleted workflows are not
silently un-deleted.
- Add optional archivedAt to ArchiveWorkflowOptions (backwards-compatible)
- Pass shared timestamp through deleteFolderRecursively → archiveWorkflowsByIdsInWorkspace
- Filter restore with eq(workflow.archivedAt, folderArchivedAt) instead of isNotNull
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(workflows): clear folderId on restore when folder is archived or missing
When individually restoring a workflow from Recently Deleted, check if
its folder still exists and is active. If the folder is archived or
missing, clear folderId so the workflow appears at root instead of
being orphaned (invisible in sidebar).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(folders): format restoreFolderRecursively call to satisfy biome
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(folders): close remaining restore edge cases
Three issues caught by audit:
1. Child folder restore used isNotNull instead of timestamp matching,
so individually-deleted child folders would be incorrectly restored.
Now uses eq(archivedAt, folderArchivedAt) for both workflows AND
child folders — consistent and deterministic.
2. No workspace archived check — could restore a folder into an
archived workspace. Now checks getWorkspaceWithOwner, matching
the existing restoreWorkflow pattern.
3. Re-restoring an already-restored folder returned an error. Now
returns success with zero counts (idempotent).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(folders): add archivedAt to optimistic folder creation objects
Ensures optimistic folder objects include archivedAt: null for
consistency with the database schema shape.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(folders): handle missing parent folder during restore reparenting
If the parent folder row no longer exists (not just archived), the
restored folder now correctly gets reparented to root instead of
retaining a dangling parentId reference.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for freeto 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

@waleedlatif1