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
@@ -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'
Expand All @@ -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,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 = false // TODO: Add validation logic if needed
// 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: { ...blocks },
edges: [...edges],
loops: { ...(loops || {}) },
parallels: { ...(parallels || {}) },
})
return !result.valid
}, [blocks, edges, loops, parallels])
Copy link

Choose a reason for hiding this comment

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

Deploy button not disabled when validation errors exist

Medium Severity

The new hasValidationErrors value only feeds into isWorkflowBlockedisButtonDisabled, which is wired to the Run button. The Deploy component doesn't receive hasValidationErrors as a prop and its own isDisabled (isDeploying || !canDeploy || isEmpty) doesn't account for validation errors. Users can still click Deploy on an invalid workflow. The backend will reject it, but the UI-layer prevention described in the PR is missing for the Deploy button.

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.

Keyboard shortcut bypasses validation-disabled Run button

Medium Severity

The run-workflow keyboard shortcut handler (Mod+Enter) only checks isExecuting before calling runWorkflow(). It does not check hasValidationErrors or isButtonDisabled. Previously this didn't matter because hasValidationErrors was hardcoded to false. Now that it reflects real validation state, users can bypass the disabled Run button entirely by pressing the keyboard shortcut, running invalid workflows the UI is supposed to prevent.

Additional Locations (1)
Fix in Cursor Fix in Web

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

Expand Down