diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index 1dd8798a3f2..aae32b15d6d 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -20,6 +20,7 @@ import { createSchedulesForDeploy, validateWorkflowSchedules, } from '@/lib/workflows/schedules' +import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' import { validateWorkflowPermissions } from '@/lib/workflows/utils' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' import type { WorkflowState } from '@/stores/workflows/workflow/types' @@ -134,6 +135,22 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return createErrorResponse('Failed to load workflow state', 500) } + // Validate workflow state (block types, edges, tool references) + const workflowValidation = validateWorkflowState({ + blocks: normalizedData.blocks, + edges: normalizedData.edges, + loops: normalizedData.loops, + parallels: normalizedData.parallels, + variables: {}, + } as WorkflowState) + if (!workflowValidation.valid) { + const errorSummary = workflowValidation.errors.join('; ') + logger.warn( + `[${requestId}] Workflow validation failed for ${id}: ${errorSummary}` + ) + return createErrorResponse(`Workflow validation failed: ${errorSummary}`, 400) + } + const scheduleValidation = validateWorkflowSchedules(normalizedData.blocks) if (!scheduleValidation.isValid) { logger.warn( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts index b6a5d585e32..216582412ac 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployment.ts @@ -28,16 +28,13 @@ export function useDeployment({ /** * Handle deploy button click - * First deploy: calls API to deploy, then opens modal on success - * Already deployed: opens modal directly (validation happens on Update in modal) + * First deploy: runs pre-deploy checks, calls API to deploy, then opens modal on success + * Already deployed: runs pre-deploy checks, then opens modal (redeployment happens in modal) */ const handleDeployClick = useCallback(async () => { if (!workflowId) return { success: false, shouldOpenModal: false } - if (isDeployed) { - return { success: true, shouldOpenModal: true } - } - + // Always run pre-deploy checks, even for redeployments const { blocks, edges, loops, parallels } = useWorkflowStore.getState() const liveBlocks = mergeSubblockState(blocks, workflowId) const checkResult = runPreDeployChecks({ @@ -56,6 +53,10 @@ export function useDeployment({ return { success: false, shouldOpenModal: false } } + if (isDeployed) { + return { success: true, shouldOpenModal: true } + } + setIsDeploying(true) try { const response = await fetch(`/api/workflows/${workflowId}/deploy`, { 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..8b71c9b7cee 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 @@ -356,7 +356,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 + + // Validate workflow has connected blocks when multiple blocks exist. + // A single-block workflow (e.g. one starter or agent block) is valid without edges, + // but multiple blocks with no edges indicates a disconnected graph. + const blockCount = useWorkflowStore((state) => Object.keys(state.blocks).length) + const hasEdges = useWorkflowStore((state) => state.edges.length > 0) + const hasValidationErrors = blockCount > 1 && !hasEdges + const isWorkflowBlocked = isExecuting || hasValidationErrors const isButtonDisabled = !isExecuting && (isWorkflowBlocked || (!canRun && !isLoadingPermissions)) @@ -373,7 +380,7 @@ export const Panel = memo(function Panel() { handler: () => { if (isExecuting) { void cancelWorkflow() - } else { + } else if (!isButtonDisabled) { void runWorkflow() } },