feat(workflows): add isLocked to workflows and folders with cascade lock#4031
feat(workflows): add isLocked to workflows and folders with cascade lock#4031waleedlatif1 wants to merge 1 commit intostagingfrom
Conversation
…ock support Add first-class `isLocked` property to workflows and folders that makes locked items fully read-only (canvas, sidebar rename/color/move/delete). Locked folders cascade to all contained workflows and sub-folders. Lock icon shown in sidebar, admin-only toggle via context menu. Coexists with block-level `locked` for granular protection. Also excludes block-level `locked` from diff detection so locking no longer flips deploy status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Enforces lock semantics end-to-end: APIs now restrict updates/deletes on locked resources (admin required to toggle Updates workflow diffing to ignore block-level Reviewed by Cursor Bugbot for commit de71cd7. Configure here. |
Greptile SummaryThis PR introduces an Key changes:
Issues found:
Confidence Score: 4/5Not safe to merge as-is: folder-level lock can be bypassed via direct API calls on child workflows, and locked items can still be duplicated from the sidebar. Two P1 issues exist: (1) the server-side workflow DELETE/PUT guards only check the workflow's own
|
| Filename | Overview |
|---|---|
| apps/sim/app/api/workflows/[id]/route.ts | Adds isLocked to the UpdateWorkflow schema and guards DELETE/PUT with 403; critical gap: checks only workflowData.isLocked, missing the server-side folder-cascade walk. |
| apps/sim/app/api/folders/[id]/route.ts | Adds isLocked to folder schema, correctly gates admin-only lock toggle and blocks non-expand mutations + DELETE on locked folders. |
| apps/sim/hooks/use-effective-lock.ts | New hook correctly walks the folder parent chain to compute effective lock state for both workflows and folders. |
| apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx | Adds lock icon, handleToggleLock, and disables rename/color/delete correctly; but disableDuplicate is missing the isEffectivelyLocked guard, allowing duplication of locked workflows. |
| apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx | Adds lock icon and toggle for folders with correct rename/delete/create guards; same disableDuplicate gap as workflow-item — missing isEffectivelyLocked check. |
| packages/db/migrations/0187_certain_pretty_boy.sql | Safe migration: adds is_locked boolean DEFAULT false NOT NULL to both workflow and workflow_folder tables; no data risk. |
| apps/sim/lib/workflows/comparison/compare.test.ts | Adds a regression test confirming that toggling block-level locked does not flip the deploy-badge diff; correctly validates existing behavior. |
| apps/sim/hooks/queries/folders.ts | Adds isLocked to mapFolder, UpdateFolderVariables, and the optimistic create/duplicate folder payloads; correctly defaults to false. |
| packages/db/schema.ts | Adds isLocked: boolean('is_locked').notNull().default(false) to both workflow and workflowFolder Drizzle table definitions; correct and consistent. |
| apps/sim/hooks/queries/utils/workflow-list-query.ts | Adds isLocked to WorkflowApiRow and maps it through mapWorkflow with a safe ?? false default. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User action: rename / delete / duplicate / move] --> B{Client-side\nisEffectivelyLocked?}
B -- Yes --> C[Block action in UI\nrename/delete/color/move disabled]
B -- No --> D[Allow action → API call]
D --> E{API route\nworkflowData.isLocked?}
E -- true --> F[Return 403]
E -- false --> G[Execute mutation ✓]
H[Folder locked\nworkflow.isLocked = false] -.->|Cascade walk\nclient-side only| B
H -.->|NOT checked\nserver-side| E
style H fill:#ffd6d6,stroke:#cc0000
style E fill:#ffd6d6,stroke:#cc0000
style G fill:#d4edda,stroke:#28a745
Reviews (1): Last reviewed commit: "feat(workflows): add isLocked to workflo..." | Re-trigger Greptile
| if (workflowData.isLocked) { | ||
| return NextResponse.json({ error: 'Workflow is locked' }, { status: 403 }) | ||
| } |
There was a problem hiding this comment.
Server-side folder cascade not enforced
The DELETE and PUT handlers only check workflowData.isLocked (the workflow's own flag) but not whether the workflow lives inside a locked folder. A client that knows a workflow's ID can bypass a folder-level lock entirely by calling the API directly.
For example:
- Admin locks folder F containing workflow W.
- Any editor calls
DELETE /api/workflows/Wdirectly. workflowData.isLockedisfalse, so the 403 guard is skipped and the deletion succeeds.
The same bypass exists in the PUT handler at line 312. The server-side logic should mirror the client-side isWorkflowEffectivelyLocked check by also looking up the workflow's folderId chain and checking isLocked on each ancestor.
The same issue is present in the PUT guard at line 312:
if (workflowData.isLocked && updates.isLocked === undefined) {
return NextResponse.json({ error: 'Workflow is locked' }, { status: 403 })
}Both of these should also check whether the workflow's folder (or any ancestor folder) is locked, either by fetching and walking the folder chain server-side or by adding an isEffectivelyLocked utility in the server context.
| showColorChange={!isMixedSelection && selectedWorkflows.size <= 1} | ||
| disableRename={!userPermissions.canEdit} | ||
| disableRename={!userPermissions.canEdit || isEffectivelyLocked} | ||
| disableDuplicate={!userPermissions.canEdit || isDuplicatingSelection} |
There was a problem hiding this comment.
Locked state not checked for
disableDuplicate
The PR description explicitly states that "sidebar actions (rename, color, move, delete, duplicate) blocked" for locked workflows. However, disableDuplicate does not check isEffectivelyLocked, so any editor can still duplicate a locked workflow from the context menu.
| disableDuplicate={!userPermissions.canEdit || isDuplicatingSelection} | |
| disableDuplicate={!userPermissions.canEdit || isDuplicatingSelection || isEffectivelyLocked} |
The same issue exists in folder-item.tsx at line 570:
disableDuplicate={!userPermissions.canEdit || isDuplicatingSelection || !hasExportableContent}That should include || isEffectivelyLocked as well.
| disableCreateFolder={ | ||
| !userPermissions.canEdit || createFolderMutation.isPending || isEffectivelyLocked | ||
| } | ||
| disableDuplicate={ |
There was a problem hiding this comment.
Locked state not checked for
disableDuplicate on folders
Mirrors the issue in workflow-item.tsx. A locked folder can still be duplicated because isEffectivelyLocked is not included in the disableDuplicate condition.
| disableDuplicate={ | |
| disableDuplicate={!userPermissions.canEdit || isDuplicatingSelection || !hasExportableContent || isEffectivelyLocked} |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit de71cd7. Configure here.
| if (hasNonExpandUpdates) { | ||
| return NextResponse.json({ error: 'Folder is locked' }, { status: 403 }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Lock enforcement bypassed when isLocked included in request
Medium Severity
The lock enforcement check only fires when isLocked === undefined, so an admin can modify any field on a locked resource by also including isLocked in the request body (even re-sending the current value). For example, { isLocked: true, name: "modified" } sent to a locked folder skips the guard entirely — renaming succeeds while the folder stays locked. The same pattern applies to the workflow PUT handler at line 312. This contradicts the comments stating "only allow toggling isLocked."
Additional Locations (1)
Reviewed by Cursor Bugbot for commit de71cd7. Configure here.


Summary
isLockedboolean column toworkflowandworkflow_foldertables with safe NOT NULL DEFAULT false migrationisLockedlockedfrom diff detection so locking no longer flips deploy badge from "Live" to "Update"lockedfor granular protection within unlocked workflowsTest plan
ALTER TABLE ADD COLUMNstatements execute cleanlybun run lintpasses🤖 Generated with Claude Code