Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/sim/app/api/workflows/[id]/deploy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,24 @@ 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,
loops: normalizedData.loops || {},
parallels: normalizedData.parallels || {},
})
if (!stateValidation.valid) {
logger.warn(
`[${requestId}] Workflow validation failed for ${id}: ${stateValidation.errors.join('; ')}`
)
return createErrorResponse(
`Workflow has validation errors: ${stateValidation.errors.join('; ')}`,
400
)
}
Comment on lines +138 to +153
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing loops and parallels blocks valid workflow deployments

normalizedData already contains loops and parallels (used in the GET handler on lines 83–86 of this same file), but they are omitted from the validateWorkflowState call here. The validation function uses workflowState.loops || {} and workflowState.parallels || {}, so passing undefined means every edge whose source or target is a loop/parallel container will be flagged as referencing a non-existent block — causing the deployment to be rejected with a 400 even for a perfectly valid workflow.

Suggested change
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 stateValidation = validateWorkflowState({
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
parallels: normalizedData.parallels,
})


const scheduleValidation = validateWorkflowSchedules(normalizedData.blocks)
if (!scheduleValidation.isValid) {
logger.warn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -356,7 +357,16 @@ 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,
loops: state.loops || {},
parallels: state.parallels || {},
})
return !result.valid
})
Comment on lines +360 to +369
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing loops and parallels causes false-positive validation errors

validateWorkflowState checks edges against loop and parallel container IDs (lines 270–292 of validation.ts). Because state.loops and state.parallels are not passed here, any workflow that uses a loop or parallel block will have all edges connected to those containers reported as referencing "non-existent" source/target blocks — causing hasValidationErrors to be true for valid workflows and permanently disabling the Run button.

Suggested change
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 hasValidationErrors = useWorkflowStore((state) => {
if (Object.keys(state.blocks).length === 0) return false
const result = validateWorkflowState({
blocks: state.blocks,
edges: state.edges,
loops: state.loops,
parallels: state.parallels,
})
return !result.valid
})

Comment on lines +360 to +369
Copy link
Contributor

Choose a reason for hiding this comment

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

Expensive validation runs on every Zustand store update

validateWorkflowState iterates all blocks, calls getBlock() and getTool() per block, and checks all edges — all synchronously inside a Zustand selector. Selectors run on every state change, including every single keystroke in any block's input field. For larger workflows this will cause perceptible UI lag.

Consider memoising outside the selector or debouncing:

// Coarse selector — only re-validates when blocks/edges identity changes
const { blocks, edges, loops, parallels } = useWorkflowStore(
  useShallow((state) => ({
    blocks: state.blocks,
    edges: state.edges,
    loops: state.loops,
    parallels: state.parallels,
  }))
)
const hasValidationErrors = useMemo(() => {
  if (Object.keys(blocks).length === 0) return false
  return !validateWorkflowState({ blocks, edges, loops, parallels }).valid
}, [blocks, edges, loops, parallels])

This keeps the computation lazy but avoids re-running it on unrelated store updates (e.g. execution state, cursor position).

Copy link

Choose a reason for hiding this comment

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

Validation in selector mutates Zustand store state

Medium Severity

Calling validateWorkflowState inside a Zustand selector causes direct mutation of the store state. The validation function internally calls sanitizeAgentToolsInBlocks, which mutates block.subBlocks.tools.value on the original store objects (line 160 in validation.ts assigns toolsSubBlock.value = cleaned). Since the selector receives references to live store state, this silently rewrites agent tool arrays on every store update — outside of Zustand's set() — violating its immutability contract and potentially causing stale references or subtle inconsistencies in other components.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Expensive validation runs on every store update

Medium Severity

validateWorkflowState is called inside a Zustand selector, meaning it re-executes on every store state change — not just when blocks or edges change. This includes high-frequency updates like block dragging (position changes at ~60fps) and text input in sub-blocks. The function iterates all blocks, performs registry lookups, runs sanitizeAgentToolsInBlocks (which creates filtered/mapped arrays), builds Set objects from edge data, and allocates multiple temporary objects per call, creating unnecessary GC pressure and main-thread work.

Fix in Cursor Fix in Web

const isWorkflowBlocked = isExecuting || hasValidationErrors
const isButtonDisabled = !isExecuting && (isWorkflowBlocked || (!canRun && !isLoadingPermissions))

Expand Down