From 13767938d2a7c4da3434016b830a10c9ed8a577a Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Sat, 14 Mar 2026 20:22:24 +0800 Subject: [PATCH 1/5] fix: enable workflow validation before deployment --- apps/sim/app/api/workflows/[id]/deploy/route.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index 1dd8798a3f2..fb665a48199 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -134,6 +134,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return createErrorResponse('Failed to load workflow state', 500) } + // Validate workflow state before allowing deployment + const { validateWorkflowState } = await import('@/lib/workflows/sanitization/validation') + const stateValidation = validateWorkflowState({ + blocks: normalizedData.blocks, + edges: normalizedData.edges, + }) + if (!stateValidation.valid) { + logger.warn( + `[${requestId}] Workflow validation failed for ${id}: ${stateValidation.errors.join('; ')}` + ) + return createErrorResponse( + `Workflow has validation errors: ${stateValidation.errors.join('; ')}`, + 400 + ) + } + const scheduleValidation = validateWorkflowSchedules(normalizedData.blocks) if (!scheduleValidation.isValid) { logger.warn( From 70dd04d50b959ddc26f0b8230eb343c08343b9cb Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Sat, 14 Mar 2026 20:22:27 +0800 Subject: [PATCH 2/5] fix: enable workflow validation in panel --- .../w/[workflowId]/components/panel/panel.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 47e6e420709..28fc9e14393 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -26,6 +26,7 @@ import { } from '@/components/emcn' import { VariableIcon } from '@/components/icons' import { generateWorkflowJson } from '@/lib/workflows/operations/import-export' +import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' @@ -356,7 +357,14 @@ export const Panel = memo(function Panel() { // Compute run button state const canRun = userPermissions.canRead // Running only requires read permissions const isLoadingPermissions = userPermissions.isLoading - const hasValidationErrors = false // TODO: Add validation logic if needed + const hasValidationErrors = useWorkflowStore((state) => { + if (Object.keys(state.blocks).length === 0) return false + const result = validateWorkflowState({ + blocks: state.blocks, + edges: state.edges, + }) + return !result.valid + }) const isWorkflowBlocked = isExecuting || hasValidationErrors const isButtonDisabled = !isExecuting && (isWorkflowBlocked || (!canRun && !isLoadingPermissions)) From 30c68ae63b5f491e3f664bb272c0a0ea566303e7 Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Sat, 14 Mar 2026 21:01:44 +0800 Subject: [PATCH 3/5] fix: include loops and parallels in validation (addressing review feedback) --- apps/sim/app/api/workflows/[id]/deploy/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index fb665a48199..1a7e505c2af 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -139,6 +139,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const stateValidation = validateWorkflowState({ blocks: normalizedData.blocks, edges: normalizedData.edges, + loops: normalizedData.loops || {}, + parallels: normalizedData.parallels || {}, }) if (!stateValidation.valid) { logger.warn( From d646ee1f0e2a25c2041a08e9debe483a0d916e66 Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Sat, 14 Mar 2026 21:01:46 +0800 Subject: [PATCH 4/5] fix: include loops and parallels in validation (addressing review feedback) --- .../[workspaceId]/w/[workflowId]/components/panel/panel.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 28fc9e14393..29f3634a5ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -362,6 +362,8 @@ export const Panel = memo(function Panel() { const result = validateWorkflowState({ blocks: state.blocks, edges: state.edges, + loops: state.loops || {}, + parallels: state.parallels || {}, }) return !result.valid }) From 945b207aad2a134915f7774dd5f1bbf8faccfe5c Mon Sep 17 00:00:00 2001 From: guoyangzhen Date: Sun, 15 Mar 2026 22:24:10 +0800 Subject: [PATCH 5/5] fix: memoize validation selector to prevent mutation and unnecessary recomputation - Use individual selectors for blocks/edges/loops/parallels with useShallow - Memoize validation result with useMemo, only recomputing when deps change - Pass shallow copies of state to validateWorkflowState to prevent any internal mutation from affecting Zustand store state Addresses Bugbot review feedback for #3579 --- .../w/[workflowId]/components/panel/panel.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 29f3634a5ec..8a6af1462f5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -1,6 +1,6 @@ 'use client' -import { memo, useCallback, useEffect, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { ArrowUp, Lock, Square, Unlock } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' @@ -357,16 +357,25 @@ export const Panel = memo(function Panel() { // Compute run button state const canRun = userPermissions.canRead // Running only requires read permissions const isLoadingPermissions = userPermissions.isLoading - const hasValidationErrors = useWorkflowStore((state) => { - if (Object.keys(state.blocks).length === 0) return false + // Memoize workflow structure to avoid re-validating on every store change + // (e.g. block dragging at 60fps, text input, etc.) + const blocks = useWorkflowStore((state) => state.blocks) + const edges = useWorkflowStore((state) => state.edges) + const loops = useWorkflowStore((state) => state.loops) + const parallels = useWorkflowStore((state) => state.parallels) + + const hasValidationErrors = useMemo(() => { + if (Object.keys(blocks).length === 0) return false + // Pass shallow copies to validateWorkflowState to prevent any + // internal mutation from affecting Zustand store state const result = validateWorkflowState({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops || {}, - parallels: state.parallels || {}, + blocks: { ...blocks }, + edges: [...edges], + loops: { ...(loops || {}) }, + parallels: { ...(parallels || {}) }, }) return !result.valid - }) + }, [blocks, edges, loops, parallels]) const isWorkflowBlocked = isExecuting || hasValidationErrors const isButtonDisabled = !isExecuting && (isWorkflowBlocked || (!canRun && !isLoadingPermissions))