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
17 changes: 17 additions & 0 deletions apps/sim/app/api/workflows/[id]/deploy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Comment on lines +139 to +145
Copy link
Contributor

Choose a reason for hiding this comment

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

Type assertion papers over a missing variables field

normalizedData may not include a variables property, so the cast as WorkflowState silently satisfies TypeScript while potentially producing undefined at runtime for that field. validateWorkflowState only uses blocks and edges, but if the function signature ever evolves, this cast could hide a real gap.

Consider spreading a safe default instead:

Suggested change
const workflowValidation = validateWorkflowState({
blocks: normalizedData.blocks,
edges: normalizedData.edges,
loops: normalizedData.loops,
parallels: normalizedData.parallels,
} as WorkflowState)
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -373,7 +380,7 @@ export const Panel = memo(function Panel() {
handler: () => {
if (isExecuting) {
void cancelWorkflow()
} else {
} else if (!isButtonDisabled) {
void runWorkflow()
}
},
Expand Down