From 0e8c4b40ce6d64693863c375c9ac090e693e328f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 12 Sep 2025 11:07:23 -0700 Subject: [PATCH 01/43] progress --- apps/sim/app/api/chat/utils.ts | 7 +- .../app/api/workflows/[id]/execute/route.ts | 20 +- apps/sim/app/api/workflows/route.ts | 93 +------ .../components/trigger-warning-dialog.tsx | 43 ++++ .../workflow-block/workflow-block.tsx | 2 +- .../hooks/use-workflow-execution.ts | 119 +++++++-- .../[workspaceId]/w/[workflowId]/workflow.tsx | 108 +++++++- apps/sim/blocks/blocks/api_trigger.ts | 34 +++ apps/sim/blocks/blocks/chat_trigger.ts | 26 ++ apps/sim/blocks/blocks/manual_trigger.ts | 22 ++ apps/sim/blocks/blocks/starter.ts | 1 + apps/sim/blocks/registry.ts | 6 + apps/sim/components/ui/tag-dropdown.tsx | 59 ++--- apps/sim/executor/index.ts | 168 +++++++----- apps/sim/lib/workflows/block-outputs.ts | 114 +++++++++ apps/sim/lib/workflows/triggers.ts | 242 ++++++++++++++++++ apps/sim/socket-server/database/operations.ts | 2 + apps/sim/stores/workflows/yaml/importer.ts | 9 + 18 files changed, 865 insertions(+), 210 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx create mode 100644 apps/sim/blocks/blocks/api_trigger.ts create mode 100644 apps/sim/blocks/blocks/chat_trigger.ts create mode 100644 apps/sim/blocks/blocks/manual_trigger.ts create mode 100644 apps/sim/lib/workflows/block-outputs.ts create mode 100644 apps/sim/lib/workflows/triggers.ts diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index 67ab219ac4..d6304dd517 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -11,6 +11,7 @@ import { hasAdminPermission } from '@/lib/permissions/utils' import { processStreamingBlockLogs } from '@/lib/tokenization' import { getEmailDomain } from '@/lib/urls/utils' import { decryptSecret, generateRequestId } from '@/lib/utils' +import { TriggerUtils } from '@/lib/workflows/triggers' import { getBlock } from '@/blocks' import { db } from '@/db' import { chat, userStats, workflow } from '@/db/schema' @@ -613,9 +614,13 @@ export async function executeWorkflowForChat( // Set up logging on the executor loggingSession.setupExecutor(executor) + // Determine the start block for chat execution + const startBlock = TriggerUtils.findStartBlock(mergedStates, 'chat') + const startBlockId = startBlock?.blockId + let result try { - result = await executor.execute(workflowId) + result = await executor.execute(workflowId, startBlockId) } catch (error: any) { logger.error(`[${requestId}] Chat workflow execution failed:`, error) await loggingSession.safeCompleteWithError({ diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index e71ae5f620..4ad59356b6 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -12,6 +12,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { decryptSecret, generateRequestId } from '@/lib/utils' import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers' +import { TriggerUtils } from '@/lib/workflows/triggers' import { createHttpResponseFromBlock, updateWorkflowRunCounts, @@ -272,6 +273,23 @@ async function executeWorkflow( true // Enable validation during execution ) + // Determine API trigger start block + const startBlock = TriggerUtils.findStartBlock(mergedStates, 'api') + if (!startBlock) { + logger.error(`[${requestId}] No API trigger configured for this workflow`) + throw new Error('No API trigger configured for this workflow') + } + const startBlockId = startBlock.blockId + + // Check if the API trigger has any outgoing connections + const outgoingConnections = serializedWorkflow.connections.filter( + (conn) => conn.source === startBlockId + ) + if (outgoingConnections.length === 0) { + logger.error(`[${requestId}] API trigger has no outgoing connections`) + throw new Error('API Trigger block must be connected to other blocks to execute') + } + const executor = new Executor({ workflow: serializedWorkflow, currentBlockStates: processedBlockStates, @@ -287,7 +305,7 @@ async function executeWorkflow( // Set up logging on the executor loggingSession.setupExecutor(executor) - const result = await executor.execute(workflowId) + const result = await executor.execute(workflowId, startBlockId) // Check if we got a StreamingExecution result (with stream + execution properties) // For API routes, we only care about the ExecutionResult part, not the stream diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index eff74ee62f..3ee1f05a6e 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -95,7 +95,7 @@ export async function POST(req: NextRequest) { const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body) const workflowId = crypto.randomUUID() - const starterId = crypto.randomUUID() + const manualTriggerId = crypto.randomUUID() const now = new Date() logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`) @@ -121,103 +121,32 @@ export async function POST(req: NextRequest) { }) await tx.insert(workflowBlocks).values({ - id: starterId, + id: manualTriggerId, workflowId: workflowId, - type: 'starter', - name: 'Start', + type: 'manual_trigger', + name: 'Manual', positionX: '100', positionY: '100', enabled: true, horizontalHandles: true, isWide: false, advancedMode: false, - triggerMode: false, + triggerMode: true, height: '95', subBlocks: { - startWorkflow: { - id: 'startWorkflow', - type: 'dropdown', - value: 'manual', - }, - webhookPath: { - id: 'webhookPath', - type: 'short-input', - value: '', - }, - webhookSecret: { - id: 'webhookSecret', - type: 'short-input', - value: '', - }, - scheduleType: { - id: 'scheduleType', - type: 'dropdown', - value: 'daily', - }, - minutesInterval: { - id: 'minutesInterval', - type: 'short-input', - value: '', - }, - minutesStartingAt: { - id: 'minutesStartingAt', - type: 'short-input', - value: '', - }, - hourlyMinute: { - id: 'hourlyMinute', - type: 'short-input', - value: '', - }, - dailyTime: { - id: 'dailyTime', - type: 'short-input', - value: '', - }, - weeklyDay: { - id: 'weeklyDay', - type: 'dropdown', - value: 'MON', - }, - weeklyDayTime: { - id: 'weeklyDayTime', - type: 'short-input', - value: '', - }, - monthlyDay: { - id: 'monthlyDay', - type: 'short-input', - value: '', - }, - monthlyTime: { - id: 'monthlyTime', - type: 'short-input', - value: '', - }, - cronExpression: { - id: 'cronExpression', - type: 'short-input', - value: '', - }, - timezone: { - id: 'timezone', - type: 'dropdown', - value: 'UTC', - }, - }, - outputs: { - response: { - type: { - input: 'any', - }, + inputFormat: { + id: 'inputFormat', + type: 'input-format', + value: {}, }, }, + outputs: {}, createdAt: now, updatedAt: now, }) logger.info( - `[${requestId}] Successfully created workflow ${workflowId} with start block in workflow_blocks table` + `[${requestId}] Successfully created workflow ${workflowId} with manual trigger in workflow_blocks table` ) }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx new file mode 100644 index 0000000000..aa44160df0 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx @@ -0,0 +1,43 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' + +interface TriggerWarningDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + triggerName: string + message?: string +} + +export function TriggerWarningDialog({ + open, + onOpenChange, + triggerName, + message, +}: TriggerWarningDialogProps) { + const defaultMessage = `A workflow can only have one ${triggerName} trigger block. Please remove the existing one before adding a new one.` + + return ( + + + + + {message && message.includes('legacy') + ? 'Cannot mix trigger types' + : `Only one ${triggerName} trigger allowed`} + + {message || defaultMessage} + + + onOpenChange(false)}>Got it + + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 240dd3fa24..8d645522ea 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -891,7 +891,7 @@ export function WorkflowBlock({ id, data }: NodeProps) {

Description

{config.longDescription}

- {config.outputs && ( + {config.outputs && Object.keys(config.outputs).length > 0 && (

Output

diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 8fa2a1321f..73ab60b967 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console/logger' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { processStreamingBlockLogs } from '@/lib/tokenization' -import { getBlock } from '@/blocks' +import { TriggerUtils } from '@/lib/workflows/triggers' import type { BlockOutput } from '@/blocks/types' import { Executor } from '@/executor' import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' @@ -560,22 +560,14 @@ export function useWorkflowExecution() { } }) - // Filter out trigger blocks for manual execution + // Do not filter out trigger blocks; executor may need to start from them const filteredStates = Object.entries(mergedStates).reduce( (acc, [id, block]) => { - // Skip blocks with undefined type if (!block || !block.type) { logger.warn(`Skipping block with undefined type: ${id}`, block) return acc } - - const blockConfig = getBlock(block.type) - const isTriggerBlock = blockConfig?.category === 'triggers' - - // Skip trigger blocks during manual execution - if (!isTriggerBlock) { - acc[id] = block - } + acc[id] = block return acc }, {} as typeof mergedStates @@ -632,15 +624,8 @@ export function useWorkflowExecution() { {} as Record ) - // Filter edges to exclude connections to/from trigger blocks - const triggerBlockIds = Object.keys(mergedStates).filter((id) => { - const blockConfig = getBlock(mergedStates[id].type) - return blockConfig?.category === 'triggers' - }) - - const filteredEdges = workflowEdges.filter( - (edge) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target) - ) + // Keep edges intact to allow execution starting from trigger blocks + const filteredEdges = workflowEdges // Derive subflows from the current filtered graph to avoid stale state const runtimeLoops = generateLoopBlocks(filteredStates) @@ -663,12 +648,98 @@ export function useWorkflowExecution() { selectedOutputIds = chatStore.getState().getSelectedWorkflowOutput(activeWorkflowId) } - // Create executor options + // Determine start block and workflow input based on execution type + let startBlockId: string | undefined + const finalWorkflowInput = workflowInput + + if (isExecutingFromChat) { + // For chat execution, find the appropriate chat trigger + const startBlock = TriggerUtils.findStartBlock(filteredStates, 'chat') + + if (!startBlock) { + throw new Error(TriggerUtils.getTriggerValidationMessage('chat', 'missing')) + } + + startBlockId = startBlock.blockId + + // Check if the chat trigger has any outgoing connections + const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId) + if (outgoingConnections.length === 0) { + throw new Error('Chat Trigger block must be connected to other blocks to execute') + } + } else { + // For manual editor runs: look for manual trigger + const entries = Object.entries(filteredStates) + const manualTriggers = TriggerUtils.findTriggersByType(filteredStates, 'manual') + + logger.info('Manual run trigger check:', { + manualTriggersCount: manualTriggers.length, + hasManualTrigger: manualTriggers.length > 0, + triggers: manualTriggers.map((t) => ({ + type: t.type, + isLegacy: t.type === 'starter', + mode: t.type === 'starter' ? t.subBlocks?.startWorkflow?.value : 'n/a', + })), + }) + + if (manualTriggers.length === 1) { + const blockEntry = entries.find(([, block]) => block === manualTriggers[0]) + if (blockEntry) { + startBlockId = blockEntry[0] + const trigger = manualTriggers[0] + + // Check if the manual trigger has any outgoing connections + const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId) + if (outgoingConnections.length === 0) { + const error = new Error( + 'Manual Trigger block must be connected to other blocks to execute' + ) + logger.error('Manual trigger has no outgoing connections') + setIsExecuting(false) + throw error + } + + logger.info('Manual trigger found:', { + startBlockId, + triggerType: trigger.type, + isLegacyStarter: trigger.type === 'starter', + outgoingConnections: outgoingConnections.length, + }) + } + } else if (manualTriggers.length > 1) { + const error = new Error(TriggerUtils.getTriggerValidationMessage('manual', 'multiple')) + logger.error('Multiple manual triggers found') + setIsExecuting(false) + throw error + } else { + const error = new Error('Manual run requires a Manual Trigger block') + logger.error('No manual trigger found for manual run') + setIsExecuting(false) + throw error + } + } + + // If we don't have a valid startBlockId at this point, throw an error + if (!startBlockId) { + const error = new Error('No valid trigger block found to start execution') + logger.error('No startBlockId found after trigger search') + setIsExecuting(false) + throw error + } + + // Log the final startBlockId + logger.info('Final execution setup:', { + startBlockId, + isExecutingFromChat, + hasWorkflowInput: !!workflowInput, + }) + + // Create executor options with the final workflow input const executorOptions: ExecutorOptions = { workflow, currentBlockStates, envVarValues, - workflowInput, + workflowInput: finalWorkflowInput, workflowVariables, contextExtensions: { stream: isExecutingFromChat, @@ -687,8 +758,8 @@ export function useWorkflowExecution() { const newExecutor = new Executor(executorOptions) setExecutor(newExecutor) - // Execute workflow - return newExecutor.execute(activeWorkflowId || '') + // Execute workflow with the determined start block + return newExecutor.execute(activeWorkflowId || '', startBlockId) } const handleExecutionError = (error: any, options?: { executionId?: string }) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index f51523d0d1..6a9ac9887d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -13,12 +13,14 @@ import ReactFlow, { } from 'reactflow' import 'reactflow/dist/style.css' import { createLogger } from '@/lib/logs/console/logger' +import { TriggerUtils } from '@/lib/workflows/triggers' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar' import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls' import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index' import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel' import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' +import { TriggerWarningDialog } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' @@ -75,6 +77,16 @@ const WorkflowContent = React.memo(() => { // Enhanced edge selection with parent context and unique identifier const [selectedEdgeInfo, setSelectedEdgeInfo] = useState(null) + // State for trigger warning dialog + const [triggerWarning, setTriggerWarning] = useState<{ + open: boolean + triggerName: string + message?: string + }>({ + open: false, + triggerName: '', + }) + // Hooks const params = useParams() const router = useRouter() @@ -480,6 +492,23 @@ const WorkflowContent = React.memo(() => { if (!type) return if (type === 'connectionBlock') return + // Check for single trigger constraint + if (TriggerUtils.wouldViolateSingleInstance(blocks, type)) { + // Check if it's because of a legacy starter block + if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(type)) { + setTriggerWarning({ + open: true, + triggerName: 'new trigger', + message: + 'Cannot add new trigger blocks when a legacy Start block exists. Please remove the Start block first.', + }) + } else { + const triggerName = TriggerUtils.getDefaultTriggerName(type) || 'trigger' + setTriggerWarning({ open: true, triggerName }) + } + return + } + // Special handling for container nodes (loop or parallel) if (type === 'loop' || type === 'parallel') { // Create a unique ID and name for the container @@ -549,7 +578,11 @@ const WorkflowContent = React.memo(() => { // Create a new block with a unique ID const id = crypto.randomUUID() - const name = `${blockConfig.name} ${Object.values(blocks).filter((b) => b.type === type).length + 1}` + // Prefer semantic default names for triggers to support , , references + const defaultTriggerName = TriggerUtils.getDefaultTriggerName(type) + const name = + defaultTriggerName || + `${blockConfig.name} ${Object.values(blocks).filter((b) => b.type === type).length + 1}` // Auto-connect logic const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled @@ -573,6 +606,26 @@ const WorkflowContent = React.memo(() => { } } + // Enforce only one API trigger + if (type === 'api_trigger') { + const existingApiTriggers = Object.values(blocks).filter((b) => b.type === 'api_trigger') + if (existingApiTriggers.length >= 1) { + // Surface a clean UI indication; for now, log and skip add + logger.warn('Only one API trigger is allowed per workflow') + return + } + } + // Enforce only one Manual trigger for manual run UX + if (type === 'manual_trigger') { + const existingManualTriggers = Object.values(blocks).filter( + (b) => b.type === 'manual_trigger' + ) + if (existingManualTriggers.length >= 1) { + logger.warn('Only one Manual trigger is recommended; manual run uses a single trigger') + return + } + } + // Add the block to the workflow with auto-connect edge addBlock(id, type, name, centerPosition, undefined, undefined, undefined, autoConnectEdge) } @@ -593,6 +646,7 @@ const WorkflowContent = React.memo(() => { findClosestOutput, determineSourceHandle, effectivePermissions.canEdit, + setTriggerWarning, ]) // Update the onDrop handler @@ -604,6 +658,23 @@ const WorkflowContent = React.memo(() => { const data = JSON.parse(event.dataTransfer.getData('application/json')) if (data.type === 'connectionBlock') return + // Check for single trigger constraint + if (TriggerUtils.wouldViolateSingleInstance(blocks, data.type)) { + // Check if it's because of a legacy starter block + if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(data.type)) { + setTriggerWarning({ + open: true, + triggerName: 'new trigger', + message: + 'Cannot add new trigger blocks when a legacy Start block exists. Please remove the Start block first.', + }) + } else { + const triggerName = TriggerUtils.getDefaultTriggerName(data.type) || 'trigger' + setTriggerWarning({ open: true, triggerName }) + } + return + } + const reactFlowBounds = event.currentTarget.getBoundingClientRect() const position = project({ x: event.clientX - reactFlowBounds.left, @@ -698,12 +769,15 @@ const WorkflowContent = React.memo(() => { // Generate id and name here so they're available in all code paths const id = crypto.randomUUID() + // Prefer semantic default names for triggers to support , , references + const defaultTriggerNameDrop = TriggerUtils.getDefaultTriggerName(data.type) const name = data.type === 'loop' ? `Loop ${Object.values(blocks).filter((b) => b.type === 'loop').length + 1}` : data.type === 'parallel' ? `Parallel ${Object.values(blocks).filter((b) => b.type === 'parallel').length + 1}` - : `${blockConfig!.name} ${Object.values(blocks).filter((b) => b.type === data.type).length + 1}` + : defaultTriggerNameDrop || + `${blockConfig!.name} ${Object.values(blocks).filter((b) => b.type === data.type).length + 1}` if (containerInfo) { // Calculate position relative to the container node @@ -775,6 +849,27 @@ const WorkflowContent = React.memo(() => { } } } else { + // Check if adding this trigger would violate constraints + if (TriggerUtils.wouldViolateSingleInstance(blocks, data.type)) { + // Check if it's because of a legacy starter block + if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(data.type)) { + setTriggerWarning({ + open: true, + triggerName: 'new trigger', + message: + 'Cannot add new trigger blocks when a legacy Start block exists. Please remove the Start block first.', + }) + } else { + const triggerName = TriggerUtils.getDefaultTriggerName(data.type) || data.type + setTriggerWarning({ + open: true, + triggerName, + message: `Only one ${triggerName} trigger allowed`, + }) + } + return + } + // Regular auto-connect logic const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled let autoConnectEdge @@ -810,6 +905,7 @@ const WorkflowContent = React.memo(() => { determineSourceHandle, isPointInLoopNodeWrapper, getNodes, + setTriggerWarning, ] ) @@ -1676,6 +1772,14 @@ const WorkflowContent = React.memo(() => { {/* Show DiffControls if diff is available (regardless of current view mode) */} + + {/* Trigger warning dialog */} + setTriggerWarning({ ...triggerWarning, open })} + triggerName={triggerWarning.triggerName} + message={triggerWarning.message} + />
) diff --git a/apps/sim/blocks/blocks/api_trigger.ts b/apps/sim/blocks/blocks/api_trigger.ts new file mode 100644 index 0000000000..cb7f66c92f --- /dev/null +++ b/apps/sim/blocks/blocks/api_trigger.ts @@ -0,0 +1,34 @@ +import { ApiIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const ApiTriggerBlock: BlockConfig = { + type: 'api_trigger', + name: 'API Trigger', + description: 'Expose as HTTP API endpoint', + longDescription: + 'API trigger to start the workflow via authenticated HTTP calls with structured input.', + category: 'triggers', + bgColor: '#10B981', + icon: ApiIcon, + subBlocks: [ + { + id: 'inputFormat', + title: 'Input Format', + type: 'input-format', + layout: 'full', + description: 'Define the JSON input schema accepted by the API endpoint.', + }, + ], + tools: { + access: [], + }, + inputs: {}, + outputs: { + // Dynamic outputs will be added from inputFormat at runtime + // Always includes 'input' field plus any fields defined in inputFormat + }, + triggers: { + enabled: true, + available: ['api'], + }, +} diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts new file mode 100644 index 0000000000..98a41f978d --- /dev/null +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -0,0 +1,26 @@ +import { StartIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const ChatTriggerBlock: BlockConfig = { + type: 'chat_trigger', + name: 'Chat Trigger', + description: 'Start workflow from a chat deployment', + longDescription: 'Chat trigger to run the workflow via deployed chat interfaces.', + category: 'triggers', + bgColor: '#8B5CF6', + icon: StartIcon, + subBlocks: [], + tools: { + access: [], + }, + inputs: {}, + outputs: { + input: { type: 'string', description: 'User message' }, + conversationId: { type: 'string', description: 'Conversation ID' }, + files: { type: 'array', description: 'Uploaded files' }, + }, + triggers: { + enabled: true, + available: ['chat'], + }, +} diff --git a/apps/sim/blocks/blocks/manual_trigger.ts b/apps/sim/blocks/blocks/manual_trigger.ts new file mode 100644 index 0000000000..80c80035be --- /dev/null +++ b/apps/sim/blocks/blocks/manual_trigger.ts @@ -0,0 +1,22 @@ +import { StartIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' + +export const ManualTriggerBlock: BlockConfig = { + type: 'manual_trigger', + name: 'Manual Trigger', + description: 'Run workflow manually from the editor', + longDescription: 'Manual trigger to start the workflow during test runs.', + category: 'triggers', + bgColor: '#2FB3FF', + icon: StartIcon, + subBlocks: [], + tools: { + access: [], + }, + inputs: {}, + outputs: {}, + triggers: { + enabled: true, + available: ['manual'], + }, +} diff --git a/apps/sim/blocks/blocks/starter.ts b/apps/sim/blocks/blocks/starter.ts index 7a9ed26d4f..68efe3c013 100644 --- a/apps/sim/blocks/blocks/starter.ts +++ b/apps/sim/blocks/blocks/starter.ts @@ -9,6 +9,7 @@ export const StarterBlock: BlockConfig = { category: 'blocks', bgColor: '#2FB3FF', icon: StartIcon, + hideFromToolbar: true, subBlocks: [ // Main trigger selector { diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 81080e3680..7029bee3ed 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -6,8 +6,10 @@ import { AgentBlock } from '@/blocks/blocks/agent' import { AirtableBlock } from '@/blocks/blocks/airtable' import { ApiBlock } from '@/blocks/blocks/api' +import { ApiTriggerBlock } from '@/blocks/blocks/api_trigger' import { ArxivBlock } from '@/blocks/blocks/arxiv' import { BrowserUseBlock } from '@/blocks/blocks/browser_use' +import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger' import { ClayBlock } from '@/blocks/blocks/clay' import { ConditionBlock } from '@/blocks/blocks/condition' import { ConfluenceBlock } from '@/blocks/blocks/confluence' @@ -34,6 +36,7 @@ import { JiraBlock } from '@/blocks/blocks/jira' import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LinearBlock } from '@/blocks/blocks/linear' import { LinkupBlock } from '@/blocks/blocks/linkup' +import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger' import { McpBlock } from '@/blocks/blocks/mcp' import { Mem0Block } from '@/blocks/blocks/mem0' import { MemoryBlock } from '@/blocks/blocks/memory' @@ -142,6 +145,9 @@ export const registry: Record = { stagehand_agent: StagehandAgentBlock, slack: SlackBlock, starter: StarterBlock, + manual_trigger: ManualTriggerBlock, + chat_trigger: ChatTriggerBlock, + api_trigger: ApiTriggerBlock, supabase: SupabaseBlock, tavily: TavilyBlock, telegram: TelegramBlock, diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx index 1ee1067d01..97ef575310 100644 --- a/apps/sim/components/ui/tag-dropdown.tsx +++ b/apps/sim/components/ui/tag-dropdown.tsx @@ -4,6 +4,7 @@ import { ChevronRight } from 'lucide-react' import { BlockPathCalculator } from '@/lib/block-path-calculator' import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format' import { cn } from '@/lib/utils' +import { getBlockOutputPaths, getBlockOutputType } from '@/lib/workflows/block-outputs' import { getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' import { Serializer } from '@/serializer' @@ -146,6 +147,11 @@ const getOutputTypeForPath = ( return field.type } } + } else if (blockConfig?.category === 'triggers') { + // For trigger blocks, use the dynamic output helper + const blockState = useWorkflowStore.getState().blocks[blockId] + const subBlocks = blockState?.subBlocks || {} + return getBlockOutputType(block.type, outputPath, subBlocks) } else { const operationValue = getSubBlockValue(blockId, 'operation') if (blockConfig && operationValue) { @@ -630,7 +636,29 @@ export const TagDropdown: React.FC = ({ let blockTags: string[] - if (accessibleBlock.type === 'evaluator') { + // For trigger blocks, use the dynamic output helper + if (blockConfig.category === 'triggers' || accessibleBlock.type === 'starter') { + const subBlocks = blocks[accessibleBlockId]?.subBlocks || {} + const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, subBlocks) + + if (dynamicOutputs.length > 0) { + blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) + } else if (accessibleBlock.type === 'starter') { + // Legacy starter block fallback + const startWorkflowValue = getSubBlockValue(accessibleBlockId, 'startWorkflow') + if (startWorkflowValue === 'chat') { + blockTags = [ + `${normalizedBlockName}.input`, + `${normalizedBlockName}.conversationId`, + `${normalizedBlockName}.files`, + ] + } else { + blockTags = [normalizedBlockName] + } + } else { + blockTags = [] + } + } else if (accessibleBlock.type === 'evaluator') { const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics') if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { @@ -651,34 +679,7 @@ export const TagDropdown: React.FC = ({ blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { - if (accessibleBlock.type === 'starter') { - const startWorkflowValue = getSubBlockValue(accessibleBlockId, 'startWorkflow') - - if (startWorkflowValue === 'chat') { - // For chat mode, provide input, conversationId, and files - blockTags = [ - `${normalizedBlockName}.input`, - `${normalizedBlockName}.conversationId`, - `${normalizedBlockName}.files`, - ] - } else { - const inputFormatValue = getSubBlockValue(accessibleBlockId, 'inputFormat') - - if ( - inputFormatValue && - Array.isArray(inputFormatValue) && - inputFormatValue.length > 0 - ) { - blockTags = inputFormatValue - .filter((field: { name?: string }) => field.name && field.name.trim() !== '') - .map((field: { name: string }) => `${normalizedBlockName}.${field.name}`) - } else { - blockTags = [normalizedBlockName] - } - } - } else { - blockTags = [normalizedBlockName] - } + blockTags = [normalizedBlockName] } else { const blockState = blocks[accessibleBlockId] if (blockState?.triggerMode && blockConfig.triggers?.enabled) { diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index c2777d3542..ab94e27089 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -630,17 +630,25 @@ export class Executor { */ private validateWorkflow(startBlockId?: string): void { if (startBlockId) { - // If starting from a specific block (webhook trigger or schedule trigger), validate that block exists const startBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) if (!startBlock || !startBlock.enabled) { throw new Error(`Start block ${startBlockId} not found or disabled`) } - // Trigger blocks (webhook and schedule) can have incoming connections, so no need to check that + return + } + + const starterBlock = this.actualWorkflow.blocks.find( + (block) => block.metadata?.id === BlockType.STARTER + ) + + const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => { + return block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true + }) + + if (hasTriggerBlocks) { + // When triggers exist, we allow execution without a starter block } else { - // Default validation for starter block - const starterBlock = this.actualWorkflow.blocks.find( - (block) => block.metadata?.id === BlockType.STARTER - ) + // Legacy workflows: require a valid starter block and basic connection checks if (!starterBlock || !starterBlock.enabled) { throw new Error('Workflow must have an enabled starter block') } @@ -652,22 +660,15 @@ export class Executor { throw new Error('Starter block cannot have incoming connections') } - // Check if there are any trigger blocks on the canvas - const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => { - return block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true - }) - - // Only check outgoing connections for starter blocks if there are no trigger blocks - if (!hasTriggerBlocks) { - const outgoingFromStarter = this.actualWorkflow.connections.filter( - (conn) => conn.source === starterBlock.id - ) - if (outgoingFromStarter.length === 0) { - throw new Error('Starter block must have at least one outgoing connection') - } + const outgoingFromStarter = this.actualWorkflow.connections.filter( + (conn) => conn.source === starterBlock.id + ) + if (outgoingFromStarter.length === 0) { + throw new Error('Starter block must have at least one outgoing connection') } } + // General graph validations const blockIds = new Set(this.actualWorkflow.blocks.map((block) => block.id)) for (const conn of this.actualWorkflow.connections) { if (!blockIds.has(conn.source)) { @@ -762,10 +763,10 @@ export class Executor { // Determine which block to initialize as the starting point let initBlock: SerializedBlock | undefined if (startBlockId) { - // Starting from a specific block (webhook trigger or schedule trigger) + // Starting from a specific block (webhook trigger, schedule trigger, or new trigger blocks) initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) } else { - // Default to starter block + // Default to starter block (legacy) initBlock = this.actualWorkflow.blocks.find( (block) => block.metadata?.id === BlockType.STARTER ) @@ -774,8 +775,15 @@ export class Executor { if (initBlock) { // Initialize the starting block with the workflow input try { + // Get inputFormat from either old location (config.params) or new location (metadata.subBlocks) const blockParams = initBlock.config.params - const inputFormat = blockParams?.inputFormat + let inputFormat = blockParams?.inputFormat + + // For new trigger blocks (api_trigger, etc), inputFormat is in metadata.subBlocks + const metadataWithSubBlocks = initBlock.metadata as any + if (!inputFormat && metadataWithSubBlocks?.subBlocks?.inputFormat?.value) { + inputFormat = metadataWithSubBlocks.subBlocks.inputFormat.value + } // If input format is defined, structure the input according to the schema if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) { @@ -841,11 +849,19 @@ export class Executor { // Use the structured input if we processed fields, otherwise use raw input const finalInput = hasProcessedFields ? structuredInput : rawInputData - // Initialize the starting block with structured input (flattened) - const blockOutput = { - input: finalInput, - conversationId: this.workflowInput?.conversationId, // Add conversationId to root - ...finalInput, // Add input fields directly at top level + // Initialize the starting block with structured input + let blockOutput: any + + // For API triggers, fields should be at root level without 'input' field + if (initBlock.metadata?.id === 'api_trigger') { + blockOutput = { ...finalInput } + } else { + // For legacy starter blocks, keep the old behavior + blockOutput = { + input: finalInput, + conversationId: this.workflowInput?.conversationId, // Add conversationId to root + ...finalInput, // Add input fields directly at top level + } } // Add files if present (for all trigger types) @@ -863,54 +879,66 @@ export class Executor { // This ensures files are captured in trace spans and execution logs this.createStartedBlockWithFilesLog(initBlock, blockOutput, context) } else { - // Handle structured input (like API calls or chat messages) - if (this.workflowInput && typeof this.workflowInput === 'object') { - // Check if this is a chat workflow input (has both input and conversationId) - if ( - Object.hasOwn(this.workflowInput, 'input') && - Object.hasOwn(this.workflowInput, 'conversationId') - ) { - // Chat workflow: extract input, conversationId, and files to root level - const starterOutput: any = { - input: this.workflowInput.input, - conversationId: this.workflowInput.conversationId, - } - - // Add files if present - if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { - starterOutput.files = this.workflowInput.files - } - - context.blockStates.set(initBlock.id, { - output: starterOutput, - executed: true, - executionTime: 0, - }) + // Handle triggers without inputFormat + let starterOutput: any + + // Handle different trigger types + if (initBlock.metadata?.id === 'chat_trigger') { + // Chat trigger: extract input, conversationId, and files + starterOutput = { + input: this.workflowInput?.input || '', + conversationId: this.workflowInput?.conversationId || '', + } - // Create a block log for the starter block if it has files - // This ensures files are captured in trace spans and execution logs - this.createStartedBlockWithFilesLog(initBlock, starterOutput, context) - } else { - // API workflow: spread the raw data directly (no wrapping) - const starterOutput = { ...this.workflowInput } - - context.blockStates.set(initBlock.id, { - output: starterOutput, - executed: true, - executionTime: 0, - }) + if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { + starterOutput.files = this.workflowInput.files } + } else if (initBlock.metadata?.id === 'manual_trigger') { + // Manual trigger: no outputs + starterOutput = {} + } else if (initBlock.metadata?.id === 'api_trigger') { + // API trigger without inputFormat: spread the raw input directly + starterOutput = { ...(this.workflowInput || {}) } } else { - // Fallback for primitive input values - const starterOutput = { - input: this.workflowInput, + // Legacy starter block handling + if (this.workflowInput && typeof this.workflowInput === 'object') { + // Check if this is a chat workflow input (has both input and conversationId) + if ( + Object.hasOwn(this.workflowInput, 'input') && + Object.hasOwn(this.workflowInput, 'conversationId') + ) { + // Chat workflow: extract input, conversationId, and files to root level + starterOutput = { + input: this.workflowInput.input, + conversationId: this.workflowInput.conversationId, + } + + // Add files if present + if (this.workflowInput.files && Array.isArray(this.workflowInput.files)) { + starterOutput.files = this.workflowInput.files + } + } else { + // API workflow: spread the raw data directly (no wrapping) + starterOutput = { ...this.workflowInput } + } + } else { + // Fallback for primitive input values + starterOutput = { + input: this.workflowInput, + } } + } - context.blockStates.set(initBlock.id, { - output: starterOutput, - executed: true, - executionTime: 0, - }) + context.blockStates.set(initBlock.id, { + output: starterOutput, + executed: true, + executionTime: 0, + }) + + // Create a block log for the starter block if it has files + // This ensures files are captured in trace spans and execution logs + if (starterOutput.files) { + this.createStartedBlockWithFilesLog(initBlock, starterOutput, context) } } } catch (e) { diff --git a/apps/sim/lib/workflows/block-outputs.ts b/apps/sim/lib/workflows/block-outputs.ts new file mode 100644 index 0000000000..2ef2fff71a --- /dev/null +++ b/apps/sim/lib/workflows/block-outputs.ts @@ -0,0 +1,114 @@ +import { getBlock } from '@/blocks' +import type { BlockConfig } from '@/blocks/types' + +/** + * Get the effective outputs for a block, including dynamic outputs from inputFormat + */ +export function getBlockOutputs( + blockType: string, + subBlocks?: Record +): Record { + const blockConfig = getBlock(blockType) + if (!blockConfig) return {} + + // Start with the static outputs defined in the config + let outputs = { ...(blockConfig.outputs || {}) } + + // Special handling for starter block (legacy) + if (blockType === 'starter') { + const startWorkflowValue = subBlocks?.startWorkflow?.value + + if (startWorkflowValue === 'chat') { + // Chat mode outputs + return { + input: { type: 'string', description: 'User message' }, + conversationId: { type: 'string', description: 'Conversation ID' }, + files: { type: 'array', description: 'Uploaded files' }, + } + } + if ( + startWorkflowValue === 'api' || + startWorkflowValue === 'run' || + startWorkflowValue === 'manual' + ) { + // API/manual mode - use inputFormat fields only + const inputFormatValue = subBlocks?.inputFormat?.value + outputs = {} + + if (Array.isArray(inputFormatValue)) { + inputFormatValue.forEach((field: { name?: string; type?: string }) => { + if (field.name && field.name.trim() !== '') { + outputs[field.name] = { + type: (field.type || 'any') as any, + description: `Field from input format`, + } + } + }) + } + + return outputs + } + } + + // For blocks with inputFormat, add dynamic outputs + if (hasInputFormat(blockConfig) && subBlocks?.inputFormat?.value) { + const inputFormatValue = subBlocks.inputFormat.value + + if (Array.isArray(inputFormatValue)) { + // For API trigger, only use inputFormat fields + if (blockType === 'api_trigger') { + outputs = {} // Clear all default outputs + + // Add each field from inputFormat as an output at root level + inputFormatValue.forEach((field: { name?: string; type?: string }) => { + if (field.name && field.name.trim() !== '') { + outputs[field.name] = { + type: (field.type || 'any') as any, + description: `Field from input format`, + } + } + }) + } + } else if (blockType === 'api_trigger') { + // If no inputFormat defined, API trigger has no outputs + outputs = {} + } + } + + return outputs +} + +/** + * Check if a block config has an inputFormat sub-block + */ +function hasInputFormat(blockConfig: BlockConfig): boolean { + return blockConfig.subBlocks?.some((sb) => sb.type === 'input-format') || false +} + +/** + * Get output paths for a block (for tag dropdown) + */ +export function getBlockOutputPaths(blockType: string, subBlocks?: Record): string[] { + const outputs = getBlockOutputs(blockType, subBlocks) + return Object.keys(outputs) +} + +/** + * Get the type of a specific output path + */ +export function getBlockOutputType( + blockType: string, + outputPath: string, + subBlocks?: Record +): string { + const outputs = getBlockOutputs(blockType, subBlocks) + const output = outputs[outputPath] + + if (!output) return 'any' + + if (typeof output === 'object' && 'type' in output) { + return output.type + } + + return typeof output === 'string' ? output : 'any' +} diff --git a/apps/sim/lib/workflows/triggers.ts b/apps/sim/lib/workflows/triggers.ts new file mode 100644 index 0000000000..1db857f8a1 --- /dev/null +++ b/apps/sim/lib/workflows/triggers.ts @@ -0,0 +1,242 @@ +import { getBlock } from '@/blocks' + +/** + * Unified trigger type definitions + */ +export const TRIGGER_TYPES = { + MANUAL: 'manual_trigger', + CHAT: 'chat_trigger', + API: 'api_trigger', + WEBHOOK: 'webhook', + SCHEDULE: 'schedule', + STARTER: 'starter', // Legacy +} as const + +export type TriggerType = (typeof TRIGGER_TYPES)[keyof typeof TRIGGER_TYPES] + +/** + * Trigger classification and utilities + */ +export class TriggerUtils { + /** + * Check if a block is any kind of trigger + */ + static isTriggerBlock(block: { type: string; triggerMode?: boolean }): boolean { + const blockConfig = getBlock(block.type) + + return ( + // New trigger blocks (explicit category) + blockConfig?.category === 'triggers' || + // Blocks with trigger mode enabled + block.triggerMode === true || + // Legacy starter block + block.type === TRIGGER_TYPES.STARTER + ) + } + + /** + * Check if a block is a specific trigger type + */ + static isTriggerType(block: { type: string }, triggerType: TriggerType): boolean { + return block.type === triggerType + } + + /** + * Check if a type string is any trigger type + */ + static isAnyTriggerType(type: string): boolean { + return ( + type === TRIGGER_TYPES.MANUAL || + type === TRIGGER_TYPES.CHAT || + type === TRIGGER_TYPES.API || + type === TRIGGER_TYPES.WEBHOOK || + type === TRIGGER_TYPES.SCHEDULE + ) + } + + /** + * Check if a block is a chat-compatible trigger + */ + static isChatTrigger(block: { type: string; subBlocks?: any }): boolean { + if (block.type === TRIGGER_TYPES.CHAT) { + return true + } + + // Legacy: starter block in chat mode + if (block.type === TRIGGER_TYPES.STARTER) { + return block.subBlocks?.startWorkflow?.value === 'chat' + } + + return false + } + + /** + * Check if a block is a manual-compatible trigger + */ + static isManualTrigger(block: { type: string; subBlocks?: any }): boolean { + if (block.type === TRIGGER_TYPES.MANUAL) { + return true + } + + // Legacy: starter block in manual mode + if (block.type === TRIGGER_TYPES.STARTER) { + return block.subBlocks?.startWorkflow?.value === 'manual' + } + + return false + } + + /** + * Check if a block is an API-compatible trigger + */ + static isApiTrigger(block: { type: string; subBlocks?: any }): boolean { + if (block.type === TRIGGER_TYPES.API) { + return true + } + + // Legacy: starter block in API mode + if (block.type === TRIGGER_TYPES.STARTER) { + const mode = block.subBlocks?.startWorkflow?.value + return mode === 'api' || mode === 'run' + } + + return false + } + + /** + * Get the default name for a trigger type + */ + static getDefaultTriggerName(triggerType: string): string | null { + switch (triggerType) { + case TRIGGER_TYPES.CHAT: + return 'Chat' + case TRIGGER_TYPES.MANUAL: + return 'Manual' + case TRIGGER_TYPES.API: + return 'API' + case TRIGGER_TYPES.WEBHOOK: + return 'Webhook' + case TRIGGER_TYPES.SCHEDULE: + return 'Schedule' + default: + return null + } + } + + /** + * Find trigger blocks of a specific type in a workflow + */ + static findTriggersByType( + blocks: T[] | Record, + triggerType: 'chat' | 'manual' | 'api' + ): T[] { + const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks) + + switch (triggerType) { + case 'chat': + return blockArray.filter((block) => TriggerUtils.isChatTrigger(block)) + case 'manual': + return blockArray.filter((block) => TriggerUtils.isManualTrigger(block)) + case 'api': + return blockArray.filter((block) => TriggerUtils.isApiTrigger(block)) + default: + return [] + } + } + + /** + * Find the appropriate start block for a given execution context + */ + static findStartBlock( + blocks: Record, + executionType: 'chat' | 'manual' | 'api' + ): { blockId: string; block: T } | null { + const entries = Object.entries(blocks) + + // Look for new trigger blocks first + const triggers = TriggerUtils.findTriggersByType(blocks, executionType) + if (triggers.length > 0) { + const blockId = entries.find(([, b]) => b === triggers[0])?.[0] + if (blockId) { + return { blockId, block: triggers[0] } + } + } + + // Legacy fallback: look for starter block + const starterEntry = entries.find(([, block]) => block.type === TRIGGER_TYPES.STARTER) + if (starterEntry) { + return { blockId: starterEntry[0], block: starterEntry[1] } + } + + return null + } + + /** + * Check if multiple triggers of a restricted type exist + */ + static hasMultipleTriggers( + blocks: T[] | Record, + triggerType: TriggerType + ): boolean { + const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks) + const count = blockArray.filter((block) => block.type === triggerType).length + return count > 1 + } + + /** + * Check if a trigger type requires single instance constraint + */ + static requiresSingleInstance(triggerType: string): boolean { + return ( + triggerType === TRIGGER_TYPES.API || + triggerType === TRIGGER_TYPES.MANUAL || + triggerType === TRIGGER_TYPES.CHAT + ) + } + + /** + * Check if a workflow has a legacy starter block + */ + static hasLegacyStarter(blocks: T[] | Record): boolean { + const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks) + return blockArray.some((block) => block.type === TRIGGER_TYPES.STARTER) + } + + /** + * Check if adding a trigger would violate single instance constraint + */ + static wouldViolateSingleInstance( + blocks: T[] | Record, + triggerType: string + ): boolean { + const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks) + + // Can't add new triggers if legacy starter block exists + if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(triggerType)) { + return true + } + + // Check single-instance rules + if (!TriggerUtils.requiresSingleInstance(triggerType)) { + return false + } + + return blockArray.some((block) => block.type === triggerType) + } + + /** + * Get trigger validation message + */ + static getTriggerValidationMessage( + triggerType: 'chat' | 'manual' | 'api', + issue: 'missing' | 'multiple' + ): string { + const triggerName = triggerType.charAt(0).toUpperCase() + triggerType.slice(1) + + if (issue === 'missing') { + return `${triggerName} execution requires a ${triggerName} Trigger block` + } + + return `Multiple ${triggerName} Trigger blocks found. Keep only one.` + } +} diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index 2ad00b49c6..fc97d6db17 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -234,6 +234,8 @@ async function handleBlockOperationTx( throw new Error('Missing required fields for add block operation') } + // Note: single-API-trigger enforcement is handled client-side to avoid disconnects + logger.debug(`[SERVER] Adding block: ${payload.type} (${payload.id})`, { isSubflowType: isSubflowBlockType(payload.type), }) diff --git a/apps/sim/stores/workflows/yaml/importer.ts b/apps/sim/stores/workflows/yaml/importer.ts index c31d16dbdd..3a01662efc 100644 --- a/apps/sim/stores/workflows/yaml/importer.ts +++ b/apps/sim/stores/workflows/yaml/importer.ts @@ -157,6 +157,15 @@ function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[]; war } }) } + // Enforce only one API trigger in YAML + if (block.type === 'api_trigger') { + const apiCount = Object.values(yamlWorkflow.blocks).filter( + (b) => b.type === 'api_trigger' + ).length + if (apiCount > 1) { + errors.push('Only one API trigger is allowed per workflow (YAML contains multiple).') + } + } }) return { errors, warnings } From 396efc99f163781203653e4bd2bafdcea591a113 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 12 Sep 2025 17:14:17 -0700 Subject: [PATCH 02/43] progress --- .../app/api/workflows/[id]/execute/route.ts | 25 +++-- .../hooks/use-workflow-execution.ts | 106 +++++++++++++----- apps/sim/executor/resolver/resolver.ts | 45 +++++--- 3 files changed, 125 insertions(+), 51 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 4ad59356b6..f3a2937e0c 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -274,20 +274,29 @@ async function executeWorkflow( ) // Determine API trigger start block + // API execution ONLY works with API trigger blocks (or legacy starter in api/run mode) const startBlock = TriggerUtils.findStartBlock(mergedStates, 'api') + if (!startBlock) { logger.error(`[${requestId}] No API trigger configured for this workflow`) - throw new Error('No API trigger configured for this workflow') + throw new Error( + 'No API trigger configured for this workflow. Add an API Trigger block or use a Start block in API mode.' + ) } + const startBlockId = startBlock.blockId + const triggerBlock = startBlock.block - // Check if the API trigger has any outgoing connections - const outgoingConnections = serializedWorkflow.connections.filter( - (conn) => conn.source === startBlockId - ) - if (outgoingConnections.length === 0) { - logger.error(`[${requestId}] API trigger has no outgoing connections`) - throw new Error('API Trigger block must be connected to other blocks to execute') + // Check if the API trigger has any outgoing connections (except for legacy starter blocks) + // Legacy starter blocks have their own validation in the executor + if (triggerBlock.type !== 'starter') { + const outgoingConnections = serializedWorkflow.connections.filter( + (conn) => conn.source === startBlockId + ) + if (outgoingConnections.length === 0) { + logger.error(`[${requestId}] API trigger has no outgoing connections`) + throw new Error('API Trigger block must be connected to other blocks to execute') + } } const executor = new Executor({ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 73ab60b967..93f6b09931 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -650,7 +650,7 @@ export function useWorkflowExecution() { // Determine start block and workflow input based on execution type let startBlockId: string | undefined - const finalWorkflowInput = workflowInput + let finalWorkflowInput = workflowInput if (isExecutingFromChat) { // For chat execution, find the appropriate chat trigger @@ -668,55 +668,105 @@ export function useWorkflowExecution() { throw new Error('Chat Trigger block must be connected to other blocks to execute') } } else { - // For manual editor runs: look for manual trigger + // For manual editor runs: look for Manual trigger OR API trigger const entries = Object.entries(filteredStates) + + // Find manual triggers and API triggers const manualTriggers = TriggerUtils.findTriggersByType(filteredStates, 'manual') + const apiTriggers = TriggerUtils.findTriggersByType(filteredStates, 'api') logger.info('Manual run trigger check:', { manualTriggersCount: manualTriggers.length, - hasManualTrigger: manualTriggers.length > 0, - triggers: manualTriggers.map((t) => ({ + apiTriggersCount: apiTriggers.length, + manualTriggers: manualTriggers.map((t) => ({ + type: t.type, + name: t.name, + isLegacy: t.type === 'starter', + })), + apiTriggers: apiTriggers.map((t) => ({ type: t.type, + name: t.name, isLegacy: t.type === 'starter', - mode: t.type === 'starter' ? t.subBlocks?.startWorkflow?.value : 'n/a', })), }) - if (manualTriggers.length === 1) { - const blockEntry = entries.find(([, block]) => block === manualTriggers[0]) + let selectedTrigger: any = null + let selectedBlockId: string | null = null + + // Check for API triggers first (they take precedence over manual triggers) + if (apiTriggers.length === 1) { + selectedTrigger = apiTriggers[0] + const blockEntry = entries.find(([, block]) => block === selectedTrigger) if (blockEntry) { - startBlockId = blockEntry[0] - const trigger = manualTriggers[0] + selectedBlockId = blockEntry[0] + + // Extract test values from the API trigger's inputFormat + if (selectedTrigger.type === 'api_trigger' || selectedTrigger.type === 'starter') { + const inputFormatValue = selectedTrigger.subBlocks?.inputFormat?.value + if (Array.isArray(inputFormatValue)) { + const testInput: Record = {} + inputFormatValue.forEach((field: any) => { + if (field && typeof field === 'object' && field.name && field.value !== undefined) { + testInput[field.name] = field.value + } + }) - // Check if the manual trigger has any outgoing connections - const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId) - if (outgoingConnections.length === 0) { - const error = new Error( - 'Manual Trigger block must be connected to other blocks to execute' - ) - logger.error('Manual trigger has no outgoing connections') - setIsExecuting(false) - throw error + // Use the test input as workflow input + if (Object.keys(testInput).length > 0) { + finalWorkflowInput = testInput + logger.info('Using API trigger test values for manual run:', testInput) + } + } } - - logger.info('Manual trigger found:', { - startBlockId, - triggerType: trigger.type, - isLegacyStarter: trigger.type === 'starter', - outgoingConnections: outgoingConnections.length, - }) + } + } else if (apiTriggers.length > 1) { + const error = new Error('Multiple API Trigger blocks found. Keep only one.') + logger.error('Multiple API triggers found') + setIsExecuting(false) + throw error + } else if (manualTriggers.length === 1) { + // No API trigger, check for manual trigger + selectedTrigger = manualTriggers[0] + const blockEntry = entries.find(([, block]) => block === selectedTrigger) + if (blockEntry) { + selectedBlockId = blockEntry[0] } } else if (manualTriggers.length > 1) { - const error = new Error(TriggerUtils.getTriggerValidationMessage('manual', 'multiple')) + const error = new Error('Multiple Manual Trigger blocks found. Keep only one.') logger.error('Multiple manual triggers found') setIsExecuting(false) throw error } else { - const error = new Error('Manual run requires a Manual Trigger block') - logger.error('No manual trigger found for manual run') + const error = new Error('Manual run requires a Manual Trigger or API Trigger block') + logger.error('No manual or API triggers found for manual run') setIsExecuting(false) throw error } + + if (selectedBlockId && selectedTrigger) { + startBlockId = selectedBlockId + + // Check if the trigger has any outgoing connections (except for legacy starter blocks) + // Legacy starter blocks have their own validation in the executor + if (selectedTrigger.type !== 'starter') { + const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId) + if (outgoingConnections.length === 0) { + const triggerName = selectedTrigger.name || selectedTrigger.type + const error = new Error(`${triggerName} must be connected to other blocks to execute`) + logger.error('Trigger has no outgoing connections', { triggerName, startBlockId }) + setIsExecuting(false) + throw error + } + } + + logger.info('Trigger found for manual run:', { + startBlockId, + triggerType: selectedTrigger.type, + triggerName: selectedTrigger.name, + isLegacyStarter: selectedTrigger.type === 'starter', + usingTestValues: selectedTrigger.type === 'api_trigger', + }) + } } // If we don't have a valid startBlockId at this point, throw an error diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index 1b96091260..257efa151b 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -520,15 +520,25 @@ export class InputResolver { // System references and regular block references are both processed // Accessibility validation happens later in validateBlockReference - // Special case for "start" references - if (blockRef.toLowerCase() === 'start') { - // Find the starter block - const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') - if (starterBlock) { - const blockState = context.blockStates.get(starterBlock.id) + // Special case for trigger block references (start, api, chat, manual) + const blockRefLower = blockRef.toLowerCase() + const triggerTypeMap: Record = { + start: 'starter', + api: 'api_trigger', + chat: 'chat_trigger', + manual: 'manual_trigger', + } + + const triggerType = triggerTypeMap[blockRefLower] + if (triggerType) { + const triggerBlock = this.workflow.blocks.find( + (block) => block.metadata?.id === triggerType + ) + if (triggerBlock) { + const blockState = context.blockStates.get(triggerBlock.id) if (blockState) { - // For starter block, start directly with the flattened output - // This enables direct access to and + // For trigger blocks, start directly with the flattened output + // This enables direct access to , , etc let replacementValue: any = blockState.output for (const part of pathParts) { @@ -537,7 +547,7 @@ export class InputResolver { `[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`, replacementValue ) - throw new Error(`Invalid path "${part}" in "${path}" for starter block.`) + throw new Error(`Invalid path "${part}" in "${path}" for trigger block.`) } // Handle array indexing syntax like "files[0]" or "items[1]" @@ -550,14 +560,14 @@ export class InputResolver { const arrayValue = replacementValue[arrayName] if (!Array.isArray(arrayValue)) { throw new Error( - `Property "${arrayName}" is not an array in path "${path}" for starter block.` + `Property "${arrayName}" is not an array in path "${path}" for trigger block.` ) } // Then access the array element if (index < 0 || index >= arrayValue.length) { throw new Error( - `Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for starter block.` + `Array index ${index} is out of bounds for "${arrayName}" (length: ${arrayValue.length}) in path "${path}" for trigger block.` ) } @@ -569,17 +579,22 @@ export class InputResolver { if (replacementValue === undefined) { logger.warn( - `[resolveBlockReferences] No value found at path "${part}" in starter block.` + `[resolveBlockReferences] No value found at path "${part}" in trigger block.` ) - throw new Error(`No value found at path "${path}" in starter block.`) + throw new Error(`No value found at path "${path}" in trigger block.`) } } // Format the value based on block type and path let formattedValue: string - // Special handling for all blocks referencing starter input - if (blockRef.toLowerCase() === 'start' && pathParts.join('.').includes('input')) { + // Special handling for all blocks referencing trigger input + // For starter and chat triggers, check for 'input' field. For API trigger, any field access counts + const isTriggerInputRef = + (blockRefLower === 'start' && pathParts.join('.').includes('input')) || + (blockRefLower === 'chat' && pathParts.join('.').includes('input')) || + (blockRefLower === 'api' && pathParts.length > 0) + if (isTriggerInputRef) { const blockType = currentBlock.metadata?.id // Format based on which block is consuming this value From d3383c8382fdfff0630929fece9588972376efe0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 12 Sep 2025 17:27:53 -0700 Subject: [PATCH 03/43] deploy command update --- .../components/deploy-modal/deploy-modal.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx index d589b78cec..59f0b5aaa2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx @@ -85,10 +85,16 @@ export function DeployModal({ let inputFormatExample = '' try { const blocks = Object.values(useWorkflowStore.getState().blocks) + + // Check for API trigger block first (takes precedence) + const apiTriggerBlock = blocks.find((block) => block.type === 'api_trigger') + // Fall back to legacy starter block const starterBlock = blocks.find((block) => block.type === 'starter') - if (starterBlock) { - const inputFormat = useSubBlockStore.getState().getValue(starterBlock.id, 'inputFormat') + const targetBlock = apiTriggerBlock || starterBlock + + if (targetBlock) { + const inputFormat = useSubBlockStore.getState().getValue(targetBlock.id, 'inputFormat') if (inputFormat && Array.isArray(inputFormat) && inputFormat.length > 0) { const exampleData: Record = {} From d4543c20da8cc704ad396b7c8f829a56db36d181 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 12 Sep 2025 18:38:02 -0700 Subject: [PATCH 04/43] add trigger mode modal --- apps/sim/app/api/workflows/route.ts | 70 ++---- .../trigger-selector/trigger-placeholder.tsx | 52 +++++ .../trigger-selector-modal.tsx | 196 +++++++++++++++++ .../[workspaceId]/w/[workflowId]/workflow.tsx | 98 ++++++++- .../toolbar-block/toolbar-block.tsx | 18 +- .../sidebar/components/toolbar/toolbar.tsx | 201 +++++++++++------- apps/sim/hooks/use-collaborative-workflow.ts | 12 +- apps/sim/lib/workflows/trigger-utils.ts | 108 ++++++++++ apps/sim/lib/workflows/triggers.ts | 11 + apps/sim/stores/workflows/registry/store.ts | 113 +--------- 10 files changed, 627 insertions(+), 252 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx create mode 100644 apps/sim/lib/workflows/trigger-utils.ts diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 3ee1f05a6e..7d95b4398b 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -5,7 +5,7 @@ import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' import { generateRequestId } from '@/lib/utils' import { db } from '@/db' -import { workflow, workflowBlocks, workspace } from '@/db/schema' +import { workflow, workspace } from '@/db/schema' import { verifyWorkspaceMembership } from './utils' const logger = createLogger('WorkflowAPI') @@ -95,61 +95,31 @@ export async function POST(req: NextRequest) { const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body) const workflowId = crypto.randomUUID() - const manualTriggerId = crypto.randomUUID() const now = new Date() logger.info(`[${requestId}] Creating workflow ${workflowId} for user ${session.user.id}`) - await db.transaction(async (tx) => { - await tx.insert(workflow).values({ - id: workflowId, - userId: session.user.id, - workspaceId: workspaceId || null, - folderId: folderId || null, - name, - description, - color, - lastSynced: now, - createdAt: now, - updatedAt: now, - isDeployed: false, - collaborators: [], - runCount: 0, - variables: {}, - isPublished: false, - marketplaceData: null, - }) - - await tx.insert(workflowBlocks).values({ - id: manualTriggerId, - workflowId: workflowId, - type: 'manual_trigger', - name: 'Manual', - positionX: '100', - positionY: '100', - enabled: true, - horizontalHandles: true, - isWide: false, - advancedMode: false, - triggerMode: true, - height: '95', - subBlocks: { - inputFormat: { - id: 'inputFormat', - type: 'input-format', - value: {}, - }, - }, - outputs: {}, - createdAt: now, - updatedAt: now, - }) - - logger.info( - `[${requestId}] Successfully created workflow ${workflowId} with manual trigger in workflow_blocks table` - ) + await db.insert(workflow).values({ + id: workflowId, + userId: session.user.id, + workspaceId: workspaceId || null, + folderId: folderId || null, + name, + description, + color, + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: false, + collaborators: [], + runCount: 0, + variables: {}, + isPublished: false, + marketplaceData: null, }) + logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`) + return NextResponse.json({ id: workflowId, name, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx new file mode 100644 index 0000000000..03a12e2236 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx @@ -0,0 +1,52 @@ +'use client' + +import { Plus, Zap } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface TriggerPlaceholderProps { + onClick: () => void + className?: string +} + +export function TriggerPlaceholder({ onClick, className }: TriggerPlaceholderProps) { + return ( +
+ +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx new file mode 100644 index 0000000000..46d03d96f9 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx @@ -0,0 +1,196 @@ +'use client' + +import { useMemo, useState } from 'react' +import { Search } from 'lucide-react' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { cn } from '@/lib/utils' +import { getAllTriggerBlocks, getTriggerDisplayName } from '@/lib/workflows/trigger-utils' + +interface TriggerSelectorModalProps { + open: boolean + onClose: () => void + onSelect: (triggerId: string, enableTriggerMode?: boolean) => void +} + +export function TriggerSelectorModal({ open, onClose, onSelect }: TriggerSelectorModalProps) { + const [hoveredId, setHoveredId] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + + // Get all trigger options from the centralized source + const triggerOptions = useMemo(() => getAllTriggerBlocks(), []) + + const filteredOptions = useMemo(() => { + if (!searchQuery.trim()) return triggerOptions + + const query = searchQuery.toLowerCase() + return triggerOptions.filter( + (option) => + option.name.toLowerCase().includes(query) || + option.description.toLowerCase().includes(query) + ) + }, [searchQuery, triggerOptions]) + + const coreOptions = useMemo( + () => filteredOptions.filter((opt) => opt.category === 'core'), + [filteredOptions] + ) + + const integrationOptions = useMemo( + () => filteredOptions.filter((opt) => opt.category === 'integration'), + [filteredOptions] + ) + + return ( + !open && onClose()}> + + + + How do you want to trigger this workflow? + +

+ Choose how your workflow will be started. You can add more triggers later from the + sidebar. +

+
+ +
+ {/* Search Input */} +
+ + setSearchQuery(e.target.value)} + className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' + autoComplete='off' + autoCorrect='off' + autoCapitalize='off' + spellCheck='false' + /> +
+
+ +
+ {/* Core Triggers Section */} + {coreOptions.length > 0 && ( + <> +

Core Triggers

+
+ {coreOptions.map((option) => { + const Icon = option.icon + const isHovered = hoveredId === option.id + + return ( + + ) + })} +
+ + )} + + {/* Integration Triggers Section */} + {integrationOptions.length > 0 && ( + <> +

+ Integration Triggers +

+
+ {integrationOptions.map((option) => { + const Icon = option.icon + const isHovered = hoveredId === option.id + + return ( + + ) + })} +
+ + )} + + {filteredOptions.length === 0 && ( +
+ No triggers found matching "{searchQuery}" +
+ )} +
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 6a9ac9887d..036886dea9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -20,6 +20,8 @@ import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/compo import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index' import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel' import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' +import { TriggerPlaceholder } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder' +import { TriggerSelectorModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal' import { TriggerWarningDialog } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' @@ -87,6 +89,9 @@ const WorkflowContent = React.memo(() => { triggerName: '', }) + // State for trigger selector modal + const [showTriggerSelector, setShowTriggerSelector] = useState(false) + // Hooks const params = useParams() const router = useRouter() @@ -112,6 +117,11 @@ const WorkflowContent = React.memo(() => { // Extract workflow data from the abstraction const { blocks, edges, loops, parallels, isDiffMode } = currentWorkflow + // Check if workflow is empty (no blocks) + const isWorkflowEmpty = useMemo(() => { + return Object.keys(blocks).length === 0 + }, [blocks]) + // Get diff analysis for edge reconstruction const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore() @@ -487,7 +497,7 @@ const WorkflowContent = React.memo(() => { return } - const { type } = event.detail + const { type, enableTriggerMode } = event.detail if (!type) return if (type === 'connectionBlock') return @@ -627,7 +637,18 @@ const WorkflowContent = React.memo(() => { } // Add the block to the workflow with auto-connect edge - addBlock(id, type, name, centerPosition, undefined, undefined, undefined, autoConnectEdge) + // Enable trigger mode if this is a trigger-capable block from the triggers tab + addBlock( + id, + type, + name, + centerPosition, + undefined, + undefined, + undefined, + autoConnectEdge, + enableTriggerMode + ) } window.addEventListener('add-block-from-toolbar', handleAddBlockFromToolbar as EventListener) @@ -649,6 +670,34 @@ const WorkflowContent = React.memo(() => { setTriggerWarning, ]) + // Handler for trigger selection from modal + const handleTriggerSelect = useCallback( + (triggerId: string, enableTriggerMode?: boolean) => { + setShowTriggerSelector(false) + + // Get the trigger name + const triggerName = TriggerUtils.getDefaultTriggerName(triggerId) || triggerId + + // Create the trigger block at the center of the viewport + const centerPosition = project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }) + const id = `${triggerId}_${Date.now()}` + + // Add the trigger block with trigger mode if specified + addBlock( + id, + triggerId, + triggerName, + centerPosition, + undefined, + undefined, + undefined, + undefined, + enableTriggerMode || false + ) + }, + [project, addBlock] + ) + // Update the onDrop handler const onDrop = useCallback( (event: React.DragEvent) => { @@ -792,10 +841,22 @@ const WorkflowContent = React.memo(() => { ) // Add block with parent info - addBlock(id, data.type, name, relativePosition, { - parentId: containerInfo.loopId, - extent: 'parent', - }) + // Note: Blocks dropped inside containers don't get trigger mode from drag + // since containers don't support trigger blocks + addBlock( + id, + data.type, + name, + relativePosition, + { + parentId: containerInfo.loopId, + extent: 'parent', + }, + undefined, + undefined, + undefined, + false + ) // Resize the container node to fit the new block // Immediate resize without delay @@ -890,7 +951,19 @@ const WorkflowContent = React.memo(() => { } // Regular canvas drop with auto-connect edge - addBlock(id, data.type, name, position, undefined, undefined, undefined, autoConnectEdge) + // Use enableTriggerMode from drag data if present (when dragging from Triggers tab) + const enableTriggerMode = data.enableTriggerMode || false + addBlock( + id, + data.type, + name, + position, + undefined, + undefined, + undefined, + autoConnectEdge, + enableTriggerMode + ) } } catch (err) { logger.error('Error dropping block:', { err }) @@ -1780,6 +1853,17 @@ const WorkflowContent = React.memo(() => { triggerName={triggerWarning.triggerName} message={triggerWarning.message} /> + + {/* Trigger selector for empty workflows */} + {isWorkflowEmpty && effectivePermissions.canEdit && ( + setShowTriggerSelector(true)} /> + )} + + setShowTriggerSelector(false)} + onSelect={handleTriggerSelect} + /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx index 98a9f2fdf0..ad3573d24a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx @@ -7,9 +7,14 @@ import type { BlockConfig } from '@/blocks/types' export type ToolbarBlockProps = { config: BlockConfig disabled?: boolean + enableTriggerMode?: boolean } -export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) { +export function ToolbarBlock({ + config, + disabled = false, + enableTriggerMode = false, +}: ToolbarBlockProps) { const userPermissions = useUserPermissionsContext() const handleDragStart = (e: React.DragEvent) => { @@ -17,7 +22,13 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) { e.preventDefault() return } - e.dataTransfer.setData('application/json', JSON.stringify({ type: config.type })) + e.dataTransfer.setData( + 'application/json', + JSON.stringify({ + type: config.type, + enableTriggerMode, + }) + ) e.dataTransfer.effectAllowed = 'move' } @@ -29,10 +40,11 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) { const event = new CustomEvent('add-block-from-toolbar', { detail: { type: config.type, + enableTriggerMode, }, }) window.dispatchEvent(event) - }, [config.type, disabled]) + }, [config.type, disabled, enableTriggerMode]) const blockContent = (
{ - const allBlocks = getAllBlocks() + // Get blocks based on the active tab using centralized logic + const sourceBlocks = activeTab === 'blocks' ? getBlocksForSidebar() : getTriggersForSidebar() // Filter blocks based on search query - const filteredBlocks = allBlocks.filter((block) => { - if (block.type === 'starter' || block.hideFromToolbar) return false - - return ( + const filteredBlocks = sourceBlocks.filter((block) => { + const matchesSearch = !searchQuery.trim() || block.name.toLowerCase().includes(searchQuery.toLowerCase()) || block.description.toLowerCase().includes(searchQuery.toLowerCase()) - ) + + return matchesSearch }) - // Separate blocks by category: 'blocks', 'tools', and 'triggers' + // Separate blocks by category const regularBlockConfigs = filteredBlocks.filter((block) => block.category === 'blocks') const toolConfigs = filteredBlocks.filter((block) => block.category === 'tools') - const triggerConfigs = filteredBlocks.filter((block) => block.category === 'triggers') + // For triggers tab, include both 'triggers' category and tools with trigger capability + const triggerConfigs = + activeTab === 'triggers' + ? filteredBlocks + : filteredBlocks.filter((block) => block.category === 'triggers') // Create regular block items and sort alphabetically const regularBlockItems: BlockItem[] = regularBlockConfigs @@ -54,23 +64,25 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: })) .sort((a, b) => a.name.localeCompare(b.name)) - // Create special blocks (loop and parallel) if they match search + // Create special blocks (loop and parallel) only for blocks tab const specialBlockItems: BlockItem[] = [] - if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) { - specialBlockItems.push({ - name: 'Loop', - type: 'loop', - isCustom: true, - }) - } - - if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) { - specialBlockItems.push({ - name: 'Parallel', - type: 'parallel', - isCustom: true, - }) + if (activeTab === 'blocks') { + if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) { + specialBlockItems.push({ + name: 'Loop', + type: 'loop', + isCustom: true, + }) + } + + if (!searchQuery.trim() || 'parallel'.toLowerCase().includes(searchQuery.toLowerCase())) { + specialBlockItems.push({ + name: 'Parallel', + type: 'parallel', + isCustom: true, + }) + } } // Sort special blocks alphabetically @@ -95,65 +107,98 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: tools: toolConfigs, triggers: triggerBlockItems, } - }, [searchQuery]) + }, [searchQuery, activeTab]) return (
- {/* Search */} -
-
- - setSearchQuery(e.target.value)} - className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' - autoComplete='off' - autoCorrect='off' - autoCapitalize='off' - spellCheck='false' - /> + {/* Tabs */} + +
+ + + + Blocks + + + + Triggers + +
-
- - {/* Content */} - -
- {/* Regular Blocks Section */} - {regularBlocks.map((block) => ( - - ))} - - {/* Special Blocks Section (Loop & Parallel) */} - {specialBlocks.map((block) => { - if (block.type === 'loop') { - return - } - if (block.type === 'parallel') { - return - } - return null - })} - - {/* Triggers Section */} - {triggers.map((trigger) => ( - - ))} - {/* Tools Section */} - {tools.map((tool) => ( - - ))} + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' + autoComplete='off' + autoCorrect='off' + autoCapitalize='off' + spellCheck='false' + /> +
- + + {/* Blocks Tab Content */} + + +
+ {/* Regular Blocks */} + {regularBlocks.map((block) => ( + + ))} + + {/* Special Blocks (Loop & Parallel) */} + {specialBlocks.map((block) => { + if (block.type === 'loop') { + return + } + if (block.type === 'parallel') { + return ( + + ) + } + return null + })} + + {/* Tools */} + {tools.map((tool) => ( + + ))} +
+
+
+ + {/* Triggers Tab Content */} + + +
+ {triggers.length > 0 ? ( + triggers.map((trigger) => ( + + )) + ) : ( +
+ {searchQuery ? 'No triggers found' : 'Add triggers from the workflow canvas'} +
+ )} +
+
+
+
) } diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 03ab9b37db..fe1604168c 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -544,7 +544,8 @@ export function useCollaborativeWorkflow() { data?: Record, parentId?: string, extent?: 'parent', - autoConnectEdge?: Edge + autoConnectEdge?: Edge, + triggerMode?: boolean ) => { // Skip socket operations when in diff mode if (isShowingDiff) { @@ -577,6 +578,7 @@ export function useCollaborativeWorkflow() { horizontalHandles: true, isWide: false, advancedMode: false, + triggerMode: triggerMode || false, height: 0, parentId, extent, @@ -586,7 +588,7 @@ export function useCollaborativeWorkflow() { // Skip if applying remote changes if (isApplyingRemoteChange.current) { workflowStore.addBlock(id, type, name, position, data, parentId, extent, { - triggerMode: false, + triggerMode: triggerMode || false, }) if (autoConnectEdge) { workflowStore.addEdge(autoConnectEdge) @@ -611,7 +613,7 @@ export function useCollaborativeWorkflow() { // Apply locally first (immediate UI feedback) workflowStore.addBlock(id, type, name, position, data, parentId, extent, { - triggerMode: false, + triggerMode: triggerMode || false, }) if (autoConnectEdge) { workflowStore.addEdge(autoConnectEdge) @@ -653,7 +655,7 @@ export function useCollaborativeWorkflow() { horizontalHandles: true, isWide: false, advancedMode: false, - triggerMode: false, + triggerMode: triggerMode || false, height: 0, // Default height, will be set by the UI parentId, extent, @@ -680,7 +682,7 @@ export function useCollaborativeWorkflow() { // Apply locally workflowStore.addBlock(id, type, name, position, data, parentId, extent, { - triggerMode: false, + triggerMode: triggerMode || false, }) if (autoConnectEdge) { workflowStore.addEdge(autoConnectEdge) diff --git a/apps/sim/lib/workflows/trigger-utils.ts b/apps/sim/lib/workflows/trigger-utils.ts new file mode 100644 index 0000000000..931039188d --- /dev/null +++ b/apps/sim/lib/workflows/trigger-utils.ts @@ -0,0 +1,108 @@ +import { getAllBlocks, getBlock } from '@/blocks' +import type { BlockConfig } from '@/blocks/types' + +export interface TriggerInfo { + id: string + name: string + description: string + icon: React.ComponentType<{ className?: string }> + color: string + category: 'core' | 'integration' + enableTriggerMode?: boolean +} + +/** + * Get all blocks that can act as triggers + * This includes both dedicated trigger blocks and tools with trigger capabilities + */ +export function getAllTriggerBlocks(): TriggerInfo[] { + const allBlocks = getAllBlocks() + const triggers: TriggerInfo[] = [] + + for (const block of allBlocks) { + // Skip hidden blocks + if (block.hideFromToolbar) continue + + // Check if it's a core trigger block (category: 'triggers') + if (block.category === 'triggers') { + triggers.push({ + id: block.type, + name: block.name, + description: block.description, + icon: block.icon, + color: block.bgColor, + category: 'core', + }) + } + // Check if it's a tool with trigger capability (has trigger-config subblock) + else if (hasTriggerCapability(block)) { + triggers.push({ + id: block.type, + name: block.name, + description: block.description.replace(' or trigger workflows from ', ', trigger from '), + icon: block.icon, + color: block.bgColor, + category: 'integration', + enableTriggerMode: true, + }) + } + } + + // Sort: core triggers first, then integration triggers, alphabetically within each category + return triggers.sort((a, b) => { + if (a.category !== b.category) { + return a.category === 'core' ? -1 : 1 + } + return a.name.localeCompare(b.name) + }) +} + +/** + * Check if a block has trigger capability (contains a trigger-config subblock) + */ +export function hasTriggerCapability(block: BlockConfig): boolean { + return block.subBlocks.some((subBlock) => subBlock.type === 'trigger-config') +} + +/** + * Get blocks that should appear in the triggers tab + * This includes all trigger blocks and tools with trigger mode + */ +export function getTriggersForSidebar(): BlockConfig[] { + const allBlocks = getAllBlocks() + return allBlocks.filter((block) => { + if (block.hideFromToolbar) return false + // Include blocks with triggers category or trigger-config subblock + return block.category === 'triggers' || hasTriggerCapability(block) + }) +} + +/** + * Get blocks that should appear in the blocks tab + * This excludes only dedicated trigger blocks, not tools with trigger capability + */ +export function getBlocksForSidebar(): BlockConfig[] { + const allBlocks = getAllBlocks() + return allBlocks.filter((block) => { + if (block.hideFromToolbar) return false + if (block.type === 'starter') return false // Legacy block + // Only exclude blocks with 'triggers' category + // Tools with trigger capability should still appear in blocks tab + return block.category !== 'triggers' + }) +} + +/** + * Get the proper display name for a trigger block in the UI + */ +export function getTriggerDisplayName(blockType: string): string { + const block = getBlock(blockType) + if (!block) return blockType + + // Special case for generic_webhook - show as "Webhook" in UI + if (blockType === 'generic_webhook') { + return 'Webhook' + } + + return block.name +} diff --git a/apps/sim/lib/workflows/triggers.ts b/apps/sim/lib/workflows/triggers.ts index 1db857f8a1..a62bdee1ef 100644 --- a/apps/sim/lib/workflows/triggers.ts +++ b/apps/sim/lib/workflows/triggers.ts @@ -107,6 +107,17 @@ export class TriggerUtils { * Get the default name for a trigger type */ static getDefaultTriggerName(triggerType: string): string | null { + // Use the block's actual name from the registry + const block = getBlock(triggerType) + if (block) { + // Special case for generic_webhook - show as "Webhook" in UI + if (triggerType === 'generic_webhook') { + return 'Webhook' + } + return block.name + } + + // Fallback for legacy or unknown types switch (triggerType) { case TRIGGER_TYPES.CHAT: return 'Chat' diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index 56cf71f553..bf6e616968 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -12,7 +12,6 @@ import type { import { getNextWorkflowColor } from '@/stores/workflows/registry/utils' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import type { BlockState } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowRegistry') @@ -641,104 +640,9 @@ export const useWorkflowRegistry = create()( logger.info(`Created workflow from marketplace: ${options.marketplaceId}`) } else { - // Create starter block for new workflow - const starterId = crypto.randomUUID() - const starterBlock = { - id: starterId, - type: 'starter' as const, - name: 'Start', - position: { x: 100, y: 100 }, - subBlocks: { - startWorkflow: { - id: 'startWorkflow', - type: 'dropdown' as const, - value: 'manual', - }, - webhookPath: { - id: 'webhookPath', - type: 'short-input' as const, - value: '', - }, - webhookSecret: { - id: 'webhookSecret', - type: 'short-input' as const, - value: '', - }, - scheduleType: { - id: 'scheduleType', - type: 'dropdown' as const, - value: 'daily', - }, - minutesInterval: { - id: 'minutesInterval', - type: 'short-input' as const, - value: '', - }, - minutesStartingAt: { - id: 'minutesStartingAt', - type: 'short-input' as const, - value: '', - }, - hourlyMinute: { - id: 'hourlyMinute', - type: 'short-input' as const, - value: '', - }, - dailyTime: { - id: 'dailyTime', - type: 'short-input' as const, - value: '', - }, - weeklyDay: { - id: 'weeklyDay', - type: 'dropdown' as const, - value: 'MON', - }, - weeklyDayTime: { - id: 'weeklyDayTime', - type: 'short-input' as const, - value: '', - }, - monthlyDay: { - id: 'monthlyDay', - type: 'short-input' as const, - value: '', - }, - monthlyTime: { - id: 'monthlyTime', - type: 'short-input' as const, - value: '', - }, - cronExpression: { - id: 'cronExpression', - type: 'short-input' as const, - value: '', - }, - timezone: { - id: 'timezone', - type: 'dropdown' as const, - value: 'UTC', - }, - }, - outputs: { - response: { - type: { - input: 'any', - }, - }, - }, - enabled: true, - horizontalHandles: true, - isWide: false, - advancedMode: false, - triggerMode: false, - height: 0, - } - + // Create empty workflow (no default blocks) initialState = { - blocks: { - [starterId]: starterBlock, - }, + blocks: {}, edges: [], loops: {}, parallels: {}, @@ -750,9 +654,7 @@ export const useWorkflowRegistry = create()( past: [], present: { state: { - blocks: { - [starterId]: starterBlock, - }, + blocks: {}, edges: [], loops: {}, parallels: {}, @@ -788,15 +690,8 @@ export const useWorkflowRegistry = create()( // Initialize subblock values to ensure they're available for sync if (!options.marketplaceId) { - // For non-marketplace workflows, initialize subblock values from the starter block + // For non-marketplace workflows, initialize empty subblock values const subblockValues: Record> = {} - const blocks = initialState.blocks as Record - for (const [blockId, block] of Object.entries(blocks)) { - subblockValues[blockId] = {} - for (const [subblockId, subblock] of Object.entries(block.subBlocks)) { - subblockValues[blockId][subblockId] = (subblock as any).value - } - } // Update the subblock store with the initial values useSubBlockStore.setState((state) => ({ From 5fc001876afe3da959959b02b31da32230b8f218 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 12 Sep 2025 18:47:47 -0700 Subject: [PATCH 05/43] fix trigger icons' --- apps/sim/blocks/blocks/api_trigger.ts | 2 +- apps/sim/blocks/blocks/chat_trigger.ts | 8 ++++++-- apps/sim/blocks/blocks/generic_webhook.ts | 8 ++++++-- apps/sim/blocks/blocks/manual_trigger.ts | 10 +++++++--- apps/sim/blocks/blocks/schedule.ts | 8 ++++++-- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/sim/blocks/blocks/api_trigger.ts b/apps/sim/blocks/blocks/api_trigger.ts index cb7f66c92f..5ef45eeb1d 100644 --- a/apps/sim/blocks/blocks/api_trigger.ts +++ b/apps/sim/blocks/blocks/api_trigger.ts @@ -8,7 +8,7 @@ export const ApiTriggerBlock: BlockConfig = { longDescription: 'API trigger to start the workflow via authenticated HTTP calls with structured input.', category: 'triggers', - bgColor: '#10B981', + bgColor: '#10B981', // Emerald for API icon: ApiIcon, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts index 98a41f978d..7566a2dfc1 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -1,6 +1,10 @@ -import { StartIcon } from '@/components/icons' +import type { SVGProps } from 'react' +import { createElement } from 'react' +import { MessageCircle } from 'lucide-react' import type { BlockConfig } from '@/blocks/types' +const ChatTriggerIcon = (props: SVGProps) => createElement(MessageCircle, props) + export const ChatTriggerBlock: BlockConfig = { type: 'chat_trigger', name: 'Chat Trigger', @@ -8,7 +12,7 @@ export const ChatTriggerBlock: BlockConfig = { longDescription: 'Chat trigger to run the workflow via deployed chat interfaces.', category: 'triggers', bgColor: '#8B5CF6', - icon: StartIcon, + icon: ChatTriggerIcon, subBlocks: [], tools: { access: [], diff --git a/apps/sim/blocks/blocks/generic_webhook.ts b/apps/sim/blocks/blocks/generic_webhook.ts index 22a94e13c9..6cb0b548ca 100644 --- a/apps/sim/blocks/blocks/generic_webhook.ts +++ b/apps/sim/blocks/blocks/generic_webhook.ts @@ -1,13 +1,17 @@ -import { WebhookIcon } from '@/components/icons' +import type { SVGProps } from 'react' +import { createElement } from 'react' +import { Webhook } from 'lucide-react' import type { BlockConfig } from '@/blocks/types' +const WebhookIcon = (props: SVGProps) => createElement(Webhook, props) + export const GenericWebhookBlock: BlockConfig = { type: 'generic_webhook', name: 'Webhook', description: 'Receive webhooks from any service by configuring a custom webhook.', category: 'triggers', icon: WebhookIcon, - bgColor: '#10B981', // Green color for triggers + bgColor: '#F97316', // Orange color for webhooks subBlocks: [ // Generic webhook configuration - always visible diff --git a/apps/sim/blocks/blocks/manual_trigger.ts b/apps/sim/blocks/blocks/manual_trigger.ts index 80c80035be..3fd3d33e5d 100644 --- a/apps/sim/blocks/blocks/manual_trigger.ts +++ b/apps/sim/blocks/blocks/manual_trigger.ts @@ -1,14 +1,18 @@ -import { StartIcon } from '@/components/icons' +import type { SVGProps } from 'react' +import { createElement } from 'react' +import { Play } from 'lucide-react' import type { BlockConfig } from '@/blocks/types' +const ManualTriggerIcon = (props: SVGProps) => createElement(Play, props) + export const ManualTriggerBlock: BlockConfig = { type: 'manual_trigger', name: 'Manual Trigger', description: 'Run workflow manually from the editor', longDescription: 'Manual trigger to start the workflow during test runs.', category: 'triggers', - bgColor: '#2FB3FF', - icon: StartIcon, + bgColor: '#3B82F6', + icon: ManualTriggerIcon, subBlocks: [], tools: { access: [], diff --git a/apps/sim/blocks/blocks/schedule.ts b/apps/sim/blocks/blocks/schedule.ts index b9e7ddafc4..ff3eefaf57 100644 --- a/apps/sim/blocks/blocks/schedule.ts +++ b/apps/sim/blocks/blocks/schedule.ts @@ -1,6 +1,10 @@ -import { ScheduleIcon } from '@/components/icons' +import type { SVGProps } from 'react' +import { createElement } from 'react' +import { Clock } from 'lucide-react' import type { BlockConfig } from '@/blocks/types' +const ScheduleIcon = (props: SVGProps) => createElement(Clock, props) + export const ScheduleBlock: BlockConfig = { type: 'schedule', name: 'Schedule', @@ -8,7 +12,7 @@ export const ScheduleBlock: BlockConfig = { longDescription: 'Integrate Schedule into the workflow. Can trigger a workflow on a schedule configuration.', category: 'triggers', - bgColor: '#7B68EE', + bgColor: '#6366F1', icon: ScheduleIcon, subBlocks: [ From d72a84f95545bcf40704d86d26bfff38dd4189e1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 13 Sep 2025 11:40:49 -0700 Subject: [PATCH 06/43] fix corners for add trigger card --- .../components/trigger-selector/trigger-placeholder.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx index 03a12e2236..229beaf339 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx @@ -40,12 +40,6 @@ export function TriggerPlaceholder({ onClick, className }: TriggerPlaceholderPro Click to Add Trigger

- - {/* Animated corner accents */} -
-
-
-
) From de3ed5c103239ce064abbe54ce0218b35f17401c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 13 Sep 2025 11:53:00 -0700 Subject: [PATCH 07/43] update serialization error visual in console --- .../console/components/console-entry/console-entry.tsx | 7 +++++-- .../w/[workflowId]/hooks/use-workflow-execution.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/console-entry/console-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/console-entry/console-entry.tsx index 82ccf17ac8..3062c08732 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/console-entry/console-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/console/components/console-entry/console-entry.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { format } from 'date-fns' import { AlertCircle, + AlertTriangle, Check, ChevronDown, ChevronUp, @@ -369,8 +370,10 @@ export function ConsoleEntry({ entry, consoleWidth }: ConsoleEntryProps) { } }, [showCopySuccess]) - const BlockIcon = blockConfig?.icon - const blockColor = blockConfig?.bgColor || '#6B7280' + // Special handling for serialization errors + const BlockIcon = entry.blockType === 'serializer' ? AlertTriangle : blockConfig?.icon + const blockColor = + entry.blockType === 'serializer' ? '#EF4444' : blockConfig?.bgColor || '#6B7280' // Handle image load error callback const handleImageLoadError = (hasError: boolean) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 93f6b09931..ddca587d81 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -850,7 +850,7 @@ export function useWorkflowExecution() { try { // Prefer attributing to specific subflow if we have a structured error let blockId = 'serialization' - let blockName = 'Serialization' + let blockName = 'Workflow' let blockType = 'serializer' if (error instanceof WorkflowValidationError) { blockId = error.blockId || blockId From 7e2dae0bc0446104b60f2586db7c4e93f5fc09e6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 13 Sep 2025 12:44:51 -0700 Subject: [PATCH 08/43] works --- apps/sim/app/api/workflows/[id]/execute/route.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.test.ts index 8d7aceb531..00626d98c4 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.test.ts @@ -292,7 +292,7 @@ describe('Workflow Execution API Route', () => { const Executor = (await import('@/executor')).Executor expect(Executor).toHaveBeenCalled() - expect(executeMock).toHaveBeenCalledWith('workflow-id') + expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id') }) /** @@ -337,7 +337,7 @@ describe('Workflow Execution API Route', () => { const Executor = (await import('@/executor')).Executor expect(Executor).toHaveBeenCalled() - expect(executeMock).toHaveBeenCalledWith('workflow-id') + expect(executeMock).toHaveBeenCalledWith('workflow-id', 'starter-id') expect(Executor).toHaveBeenCalledWith( expect.objectContaining({ From 330e0ed2977c7bb0745f3f4e604a84a275ef49d4 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 12:36:40 -0700 Subject: [PATCH 09/43] improvement(copilot-context): structured context for copilot --- apps/sim/blocks/blocks/agent.ts | 2 + apps/sim/blocks/blocks/airtable.ts | 4 +- apps/sim/blocks/blocks/browser_use.ts | 5 +- apps/sim/blocks/blocks/clay.ts | 6 +- apps/sim/blocks/blocks/confluence.ts | 5 +- apps/sim/blocks/blocks/discord.ts | 4 +- apps/sim/blocks/blocks/elevenlabs.ts | 6 +- apps/sim/blocks/blocks/exa.ts | 4 +- apps/sim/blocks/blocks/firecrawl.ts | 5 +- apps/sim/blocks/blocks/generic_webhook.ts | 1 + apps/sim/blocks/blocks/github.ts | 5 +- apps/sim/blocks/blocks/gmail.ts | 5 +- apps/sim/blocks/blocks/google.ts | 5 +- apps/sim/blocks/blocks/google_calendar.ts | 4 +- apps/sim/blocks/blocks/google_docs.ts | 4 +- apps/sim/blocks/blocks/google_drive.ts | 5 +- apps/sim/blocks/blocks/google_sheets.ts | 4 +- apps/sim/blocks/blocks/huggingface.ts | 4 +- apps/sim/blocks/blocks/hunter.ts | 5 +- apps/sim/blocks/blocks/image_generator.ts | 5 +- apps/sim/blocks/blocks/jina.ts | 6 +- apps/sim/blocks/blocks/jira.ts | 5 +- apps/sim/blocks/blocks/linear.ts | 5 +- apps/sim/blocks/blocks/linkup.ts | 5 +- apps/sim/blocks/blocks/mem0.ts | 6 +- apps/sim/blocks/blocks/microsoft_excel.ts | 4 +- apps/sim/blocks/blocks/microsoft_planner.ts | 5 +- apps/sim/blocks/blocks/microsoft_teams.ts | 5 +- apps/sim/blocks/blocks/mistral_parse.ts | 5 +- apps/sim/blocks/blocks/notion.ts | 4 +- apps/sim/blocks/blocks/onedrive.ts | 5 +- apps/sim/blocks/blocks/openai.ts | 5 +- apps/sim/blocks/blocks/outlook.ts | 5 +- apps/sim/blocks/blocks/parallel.ts | 5 +- apps/sim/blocks/blocks/perplexity.ts | 5 +- apps/sim/blocks/blocks/pinecone.ts | 4 +- apps/sim/blocks/blocks/qdrant.ts | 5 +- apps/sim/blocks/blocks/reddit.ts | 4 +- apps/sim/blocks/blocks/router.ts | 3 +- apps/sim/blocks/blocks/s3.ts | 2 + apps/sim/blocks/blocks/serper.ts | 4 +- apps/sim/blocks/blocks/sharepoint.ts | 4 +- apps/sim/blocks/blocks/slack.ts | 5 +- apps/sim/blocks/blocks/stagehand.ts | 5 +- apps/sim/blocks/blocks/stagehand_agent.ts | 5 +- apps/sim/blocks/blocks/supabase.ts | 3 +- apps/sim/blocks/blocks/tavily.ts | 2 + apps/sim/blocks/blocks/telegram.ts | 3 + apps/sim/blocks/blocks/translate.ts | 3 +- apps/sim/blocks/blocks/twilio.ts | 2 + apps/sim/blocks/blocks/typeform.ts | 2 + apps/sim/blocks/blocks/vision.ts | 5 +- apps/sim/blocks/blocks/wealthbox.ts | 4 +- apps/sim/blocks/blocks/webhook.ts | 3 + apps/sim/blocks/blocks/whatsapp.ts | 3 + apps/sim/blocks/blocks/x.ts | 4 +- apps/sim/blocks/blocks/youtube.ts | 10 +- apps/sim/blocks/types.ts | 9 + .../client/blocks/get-blocks-metadata.ts | 1 + .../server/blocks/get-blocks-and-tools.ts | 38 ++-- .../server/blocks/get-blocks-metadata-tool.ts | 191 ++++++++++-------- apps/sim/lib/copilot/tools/shared/schemas.ts | 12 +- 62 files changed, 322 insertions(+), 177 deletions(-) diff --git a/apps/sim/blocks/blocks/agent.ts b/apps/sim/blocks/blocks/agent.ts index 552416eb45..1ea2415436 100644 --- a/apps/sim/blocks/blocks/agent.ts +++ b/apps/sim/blocks/blocks/agent.ts @@ -2,6 +2,7 @@ import { AgentIcon } from '@/components/icons' import { isHosted } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import { getAllModelProviders, getBaseModelProviders, @@ -61,6 +62,7 @@ export const AgentBlock: BlockConfig = { type: 'agent', name: 'Agent', description: 'Build an agent', + authMode: AuthMode.ApiKey, longDescription: 'The Agent block is a core workflow block that is a wrapper around an LLM. It takes in system/user prompts and calls an LLM provider. It can also make tool calls by directly containing tools inside of its tool input. It can additionally return structured output.', docsLink: 'https://docs.sim.ai/blocks/agent', diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index 342297eda9..354ea43fe9 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -1,13 +1,15 @@ import { AirtableIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { AirtableResponse } from '@/tools/airtable/types' export const AirtableBlock: BlockConfig = { type: 'airtable', name: 'Airtable', description: 'Read, create, and update Airtable', + authMode: AuthMode.OAuth, longDescription: - 'Integrates Airtable into the workflow. Can create, get, list, or update Airtable records. Requires OAuth. Can be used in trigger mode to trigger a workflow when an update is made to an Airtable table.', + 'Integrates Airtable into the workflow. Can create, get, list, or update Airtable records. Can be used in trigger mode to trigger a workflow when an update is made to an Airtable table.', docsLink: 'https://docs.sim.ai/tools/airtable', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/browser_use.ts b/apps/sim/blocks/blocks/browser_use.ts index 955125e642..2de35d058a 100644 --- a/apps/sim/blocks/blocks/browser_use.ts +++ b/apps/sim/blocks/blocks/browser_use.ts @@ -1,13 +1,14 @@ import { BrowserUseIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { BrowserUseResponse } from '@/tools/browser_use/types' export const BrowserUseBlock: BlockConfig = { type: 'browser_use', name: 'Browser Use', description: 'Run browser automation tasks', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser. Requires API Key.', + 'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser.', docsLink: 'https://docs.sim.ai/tools/browser_use', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/clay.ts b/apps/sim/blocks/blocks/clay.ts index f52d877718..5b16434943 100644 --- a/apps/sim/blocks/blocks/clay.ts +++ b/apps/sim/blocks/blocks/clay.ts @@ -1,13 +1,13 @@ import { ClayIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { ClayPopulateResponse } from '@/tools/clay/types' export const ClayBlock: BlockConfig = { type: 'clay', name: 'Clay', description: 'Populate Clay workbook', - longDescription: - 'Integrate Clay into the workflow. Can populate a table with data. Requires an API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Clay into the workflow. Can populate a table with data.', docsLink: 'https://docs.sim.ai/tools/clay', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index e4f4ee0980..9f98983b84 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -1,13 +1,14 @@ import { ConfluenceIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { ConfluenceResponse } from '@/tools/confluence/types' export const ConfluenceBlock: BlockConfig = { type: 'confluence', name: 'Confluence', description: 'Interact with Confluence', - longDescription: - 'Integrate Confluence into the workflow. Can read and update a page. Requires OAuth.', + authMode: AuthMode.OAuth, + longDescription: 'Integrate Confluence into the workflow. Can read and update a page.', docsLink: 'https://docs.sim.ai/tools/confluence', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index 432d875b74..edde8bf538 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -1,13 +1,15 @@ import { DiscordIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { DiscordResponse } from '@/tools/discord/types' export const DiscordBlock: BlockConfig = { type: 'discord', name: 'Discord', description: 'Interact with Discord', + authMode: AuthMode.BotToken, longDescription: - 'Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information. Requires bot API key.', + 'Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information.', category: 'tools', bgColor: '#E0E0E0', icon: DiscordIcon, diff --git a/apps/sim/blocks/blocks/elevenlabs.ts b/apps/sim/blocks/blocks/elevenlabs.ts index e503d9419b..8e3f1feb36 100644 --- a/apps/sim/blocks/blocks/elevenlabs.ts +++ b/apps/sim/blocks/blocks/elevenlabs.ts @@ -1,13 +1,13 @@ import { ElevenLabsIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types' export const ElevenLabsBlock: BlockConfig = { type: 'elevenlabs', name: 'ElevenLabs', description: 'Convert TTS using ElevenLabs', - longDescription: - 'Integrate ElevenLabs into the workflow. Can convert text to speech. Requires API key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.', docsLink: 'https://docs.sim.ai/tools/elevenlabs', category: 'tools', bgColor: '#181C1E', diff --git a/apps/sim/blocks/blocks/exa.ts b/apps/sim/blocks/blocks/exa.ts index 4d939e5cb3..eb39302ce6 100644 --- a/apps/sim/blocks/blocks/exa.ts +++ b/apps/sim/blocks/blocks/exa.ts @@ -1,13 +1,15 @@ import { ExaAIIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { ExaResponse } from '@/tools/exa/types' export const ExaBlock: BlockConfig = { type: 'exa', name: 'Exa', description: 'Search with Exa AI', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Exa into the workflow. Can search, get contents, find similar links, answer a question, and perform research. Requires API Key.', + 'Integrate Exa into the workflow. Can search, get contents, find similar links, answer a question, and perform research.', docsLink: 'https://docs.sim.ai/tools/exa', category: 'tools', bgColor: '#1F40ED', diff --git a/apps/sim/blocks/blocks/firecrawl.ts b/apps/sim/blocks/blocks/firecrawl.ts index 2cf2947dd2..6487f5d213 100644 --- a/apps/sim/blocks/blocks/firecrawl.ts +++ b/apps/sim/blocks/blocks/firecrawl.ts @@ -1,13 +1,14 @@ import { FirecrawlIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { FirecrawlResponse } from '@/tools/firecrawl/types' export const FirecrawlBlock: BlockConfig = { type: 'firecrawl', name: 'Firecrawl', description: 'Scrape or search the web', - longDescription: - 'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Firecrawl into the workflow. Can search, scrape, or crawl websites.', docsLink: 'https://docs.sim.ai/tools/firecrawl', category: 'tools', bgColor: '#181C1E', diff --git a/apps/sim/blocks/blocks/generic_webhook.ts b/apps/sim/blocks/blocks/generic_webhook.ts index 22a94e13c9..bb2b048c3e 100644 --- a/apps/sim/blocks/blocks/generic_webhook.ts +++ b/apps/sim/blocks/blocks/generic_webhook.ts @@ -8,6 +8,7 @@ export const GenericWebhookBlock: BlockConfig = { category: 'triggers', icon: WebhookIcon, bgColor: '#10B981', // Green color for triggers + triggerAllowed: true, subBlocks: [ // Generic webhook configuration - always visible diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index c987d52730..963ea91a63 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -1,17 +1,20 @@ import { GithubIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { GitHubResponse } from '@/tools/github/types' export const GitHubBlock: BlockConfig = { type: 'github', name: 'GitHub', description: 'Interact with GitHub or trigger workflows from GitHub events', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Requires github token API Key. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.', + 'Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed.', docsLink: 'https://docs.sim.ai/tools/github', category: 'tools', bgColor: '#181C1E', icon: GithubIcon, + triggerAllowed: true, subBlocks: [ { id: 'operation', diff --git a/apps/sim/blocks/blocks/gmail.ts b/apps/sim/blocks/blocks/gmail.ts index f837bc0c0b..adce423750 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -1,17 +1,20 @@ import { GmailIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { GmailToolResponse } from '@/tools/gmail/types' export const GmailBlock: BlockConfig = { type: 'gmail', name: 'Gmail', description: 'Send Gmail or trigger workflows from Gmail events', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Gmail into the workflow. Can send, read, and search emails. Requires OAuth. Can be used in trigger mode to trigger a workflow when a new email is received.', + 'Integrate Gmail into the workflow. Can send, read, and search emails. Can be used in trigger mode to trigger a workflow when a new email is received.', docsLink: 'https://docs.sim.ai/tools/gmail', category: 'tools', bgColor: '#E0E0E0', icon: GmailIcon, + triggerAllowed: true, subBlocks: [ // Operation selector { diff --git a/apps/sim/blocks/blocks/google.ts b/apps/sim/blocks/blocks/google.ts index 91f7b4c824..dec8a1fc8e 100644 --- a/apps/sim/blocks/blocks/google.ts +++ b/apps/sim/blocks/blocks/google.ts @@ -1,13 +1,14 @@ import { GoogleIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { GoogleSearchResponse } from '@/tools/google/types' export const GoogleSearchBlock: BlockConfig = { type: 'google_search', name: 'Google Search', description: 'Search the web', - longDescription: - 'Integrate Google Search into the workflow. Can search the web. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Google Search into the workflow. Can search the web.', docsLink: 'https://docs.sim.ai/tools/google_search', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index aeaac0bdd8..7f1a22e30d 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -1,13 +1,15 @@ import { GoogleCalendarIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { GoogleCalendarResponse } from '@/tools/google_calendar/types' export const GoogleCalendarBlock: BlockConfig = { type: 'google_calendar', name: 'Google Calendar', description: 'Manage Google Calendar events', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Google Calendar into the workflow. Can create, read, update, and list calendar events. Requires OAuth.', + 'Integrate Google Calendar into the workflow. Can create, read, update, and list calendar events.', docsLink: 'https://docs.sim.ai/tools/google_calendar', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/google_docs.ts b/apps/sim/blocks/blocks/google_docs.ts index 84a4786af7..f023bdfe30 100644 --- a/apps/sim/blocks/blocks/google_docs.ts +++ b/apps/sim/blocks/blocks/google_docs.ts @@ -1,13 +1,15 @@ import { GoogleDocsIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { GoogleDocsResponse } from '@/tools/google_docs/types' export const GoogleDocsBlock: BlockConfig = { type: 'google_docs', name: 'Google Docs', description: 'Read, write, and create documents', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Google Docs into the workflow. Can read, write, and create documents. Requires OAuth.', + 'Integrate Google Docs into the workflow. Can read, write, and create documents.', docsLink: 'https://docs.sim.ai/tools/google_docs', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 0b42940b74..718a9f6644 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -1,13 +1,14 @@ import { GoogleDriveIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { GoogleDriveResponse } from '@/tools/google_drive/types' export const GoogleDriveBlock: BlockConfig = { type: 'google_drive', name: 'Google Drive', description: 'Create, upload, and list files', - longDescription: - 'Integrate Google Drive into the workflow. Can create, upload, and list files. Requires OAuth.', + authMode: AuthMode.OAuth, + longDescription: 'Integrate Google Drive into the workflow. Can create, upload, and list files.', docsLink: 'https://docs.sim.ai/tools/google_drive', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/google_sheets.ts b/apps/sim/blocks/blocks/google_sheets.ts index 3262c4db05..51dff49095 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -1,13 +1,15 @@ import { GoogleSheetsIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { GoogleSheetsResponse } from '@/tools/google_sheets/types' export const GoogleSheetsBlock: BlockConfig = { type: 'google_sheets', name: 'Google Sheets', description: 'Read, write, and update data', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Google Sheets into the workflow. Can read, write, append, and update data. Requires OAuth.', + 'Integrate Google Sheets into the workflow. Can read, write, append, and update data.', docsLink: 'https://docs.sim.ai/tools/google_sheets', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/huggingface.ts b/apps/sim/blocks/blocks/huggingface.ts index 72ab72281c..5ce7fd2fe4 100644 --- a/apps/sim/blocks/blocks/huggingface.ts +++ b/apps/sim/blocks/blocks/huggingface.ts @@ -1,13 +1,15 @@ import { HuggingFaceIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { HuggingFaceChatResponse } from '@/tools/huggingface/types' export const HuggingFaceBlock: BlockConfig = { type: 'huggingface', name: 'Hugging Face', description: 'Use Hugging Face Inference API', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Hugging Face into the workflow. Can generate completions using the Hugging Face Inference API. Requires API Key.', + 'Integrate Hugging Face into the workflow. Can generate completions using the Hugging Face Inference API.', docsLink: 'https://docs.sim.ai/tools/huggingface', category: 'tools', bgColor: '#0B0F19', diff --git a/apps/sim/blocks/blocks/hunter.ts b/apps/sim/blocks/blocks/hunter.ts index 738222fdbc..8b8f7ca4ad 100644 --- a/apps/sim/blocks/blocks/hunter.ts +++ b/apps/sim/blocks/blocks/hunter.ts @@ -1,13 +1,14 @@ import { HunterIOIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { HunterResponse } from '@/tools/hunter/types' export const HunterBlock: BlockConfig = { type: 'hunter', name: 'Hunter io', description: 'Find and verify professional email addresses', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses. Requires API Key.', + 'Integrate Hunter into the workflow. Can search domains, find email addresses, verify email addresses, discover companies, find companies, and count email addresses.', docsLink: 'https://docs.sim.ai/tools/hunter', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/image_generator.ts b/apps/sim/blocks/blocks/image_generator.ts index 0bb1291898..950d9f21d7 100644 --- a/apps/sim/blocks/blocks/image_generator.ts +++ b/apps/sim/blocks/blocks/image_generator.ts @@ -1,13 +1,14 @@ import { ImageIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { DalleResponse } from '@/tools/openai/types' export const ImageGeneratorBlock: BlockConfig = { type: 'image_generator', name: 'Image Generator', description: 'Generate images', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Image Generator into the workflow. Can generate images using DALL-E 3 or GPT Image. Requires API Key.', + 'Integrate Image Generator into the workflow. Can generate images using DALL-E 3 or GPT Image.', docsLink: 'https://docs.sim.ai/tools/image_generator', category: 'tools', bgColor: '#4D5FFF', diff --git a/apps/sim/blocks/blocks/jina.ts b/apps/sim/blocks/blocks/jina.ts index aa31371f40..7831555eee 100644 --- a/apps/sim/blocks/blocks/jina.ts +++ b/apps/sim/blocks/blocks/jina.ts @@ -1,13 +1,13 @@ import { JinaAIIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { ReadUrlResponse } from '@/tools/jina/types' export const JinaBlock: BlockConfig = { type: 'jina', name: 'Jina', description: 'Convert website content into text', - longDescription: - 'Integrate Jina into the workflow. Extracts content from websites. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Jina into the workflow. Extracts content from websites.', docsLink: 'https://docs.sim.ai/tools/jina', category: 'tools', bgColor: '#333333', diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index a55e592adc..d59f0683cd 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -1,13 +1,14 @@ import { JiraIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { JiraResponse } from '@/tools/jira/types' export const JiraBlock: BlockConfig = { type: 'jira', name: 'Jira', description: 'Interact with Jira', - longDescription: - 'Integrate Jira into the workflow. Can read, write, and update issues. Requires OAuth.', + authMode: AuthMode.OAuth, + longDescription: 'Integrate Jira into the workflow. Can read, write, and update issues.', docsLink: 'https://docs.sim.ai/tools/jira', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 132842f161..f64c446494 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -1,5 +1,6 @@ import { LinearIcon } from '@/components/icons' import type { BlockConfig, BlockIcon } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { LinearResponse } from '@/tools/linear/types' const LinearBlockIcon: BlockIcon = (props) => LinearIcon(props as any) @@ -8,8 +9,8 @@ export const LinearBlock: BlockConfig = { type: 'linear', name: 'Linear', description: 'Read and create issues in Linear', - longDescription: - 'Integrate Linear into the workflow. Can read and create issues. Requires OAuth.', + authMode: AuthMode.OAuth, + longDescription: 'Integrate Linear into the workflow. Can read and create issues.', category: 'tools', icon: LinearBlockIcon, bgColor: '#5E6AD2', diff --git a/apps/sim/blocks/blocks/linkup.ts b/apps/sim/blocks/blocks/linkup.ts index f037e08c06..cad5cf3ca8 100644 --- a/apps/sim/blocks/blocks/linkup.ts +++ b/apps/sim/blocks/blocks/linkup.ts @@ -1,12 +1,13 @@ import { LinkupIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { LinkupSearchToolResponse } from '@/tools/linkup/types' export const LinkupBlock: BlockConfig = { type: 'linkup', name: 'Linkup', description: 'Search the web with Linkup', - longDescription: 'Integrate Linkup into the workflow. Can search the web. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Linkup into the workflow. Can search the web.', docsLink: 'https://docs.sim.ai/tools/linkup', category: 'tools', bgColor: '#D6D3C7', diff --git a/apps/sim/blocks/blocks/mem0.ts b/apps/sim/blocks/blocks/mem0.ts index e2884c69d2..f91884f225 100644 --- a/apps/sim/blocks/blocks/mem0.ts +++ b/apps/sim/blocks/blocks/mem0.ts @@ -1,13 +1,13 @@ import { Mem0Icon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { Mem0Response } from '@/tools/mem0/types' export const Mem0Block: BlockConfig = { type: 'mem0', name: 'Mem0', description: 'Agent memory management', - longDescription: - 'Integrate Mem0 into the workflow. Can add, search, and retrieve memories. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Mem0 into the workflow. Can add, search, and retrieve memories.', bgColor: '#181C1E', icon: Mem0Icon, category: 'tools', diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 7c4f08e2fe..3f9a5ca692 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -1,13 +1,15 @@ import { MicrosoftExcelIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types' export const MicrosoftExcelBlock: BlockConfig = { type: 'microsoft_excel', name: 'Microsoft Excel', description: 'Read, write, and update data', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Microsoft Excel into the workflow. Can read, write, update, and add to table. Requires OAuth.', + 'Integrate Microsoft Excel into the workflow. Can read, write, update, and add to table.', docsLink: 'https://docs.sim.ai/tools/microsoft_excel', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index 3dde715eed..553baf6577 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -1,5 +1,6 @@ import { MicrosoftPlannerIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { MicrosoftPlannerResponse } from '@/tools/microsoft_planner/types' interface MicrosoftPlannerBlockParams { @@ -19,8 +20,8 @@ export const MicrosoftPlannerBlock: BlockConfig = { type: 'microsoft_planner', name: 'Microsoft Planner', description: 'Read and create tasks in Microsoft Planner', - longDescription: - 'Integrate Microsoft Planner into the workflow. Can read and create tasks. Requires OAuth.', + authMode: AuthMode.OAuth, + longDescription: 'Integrate Microsoft Planner into the workflow. Can read and create tasks.', docsLink: 'https://docs.sim.ai/tools/microsoft_planner', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index a19ca9db65..8944a7225a 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -1,15 +1,18 @@ import { MicrosoftTeamsIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { MicrosoftTeamsResponse } from '@/tools/microsoft_teams/types' export const MicrosoftTeamsBlock: BlockConfig = { type: 'microsoft_teams', name: 'Microsoft Teams', description: 'Read, write, and create messages', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel.', + 'Integrate Microsoft Teams into the workflow. Can read and write chat messages, and read and write channel messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel.', docsLink: 'https://docs.sim.ai/tools/microsoft_teams', category: 'tools', + triggerAllowed: true, bgColor: '#E0E0E0', icon: MicrosoftTeamsIcon, subBlocks: [ diff --git a/apps/sim/blocks/blocks/mistral_parse.ts b/apps/sim/blocks/blocks/mistral_parse.ts index f9554c6867..c1d7e45d39 100644 --- a/apps/sim/blocks/blocks/mistral_parse.ts +++ b/apps/sim/blocks/blocks/mistral_parse.ts @@ -1,12 +1,13 @@ import { MistralIcon } from '@/components/icons' -import type { BlockConfig, SubBlockLayout, SubBlockType } from '@/blocks/types' +import { AuthMode, type BlockConfig, type SubBlockLayout, type SubBlockType } from '@/blocks/types' import type { MistralParserOutput } from '@/tools/mistral/types' export const MistralParseBlock: BlockConfig = { type: 'mistral_parse', name: 'Mistral Parser', description: 'Extract text from PDF documents', - longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL. Requires API Key.`, + authMode: AuthMode.ApiKey, + longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.`, docsLink: 'https://docs.sim.ai/tools/mistral_parse', category: 'tools', bgColor: '#000000', diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 1393ca231f..ccfda9114a 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -1,13 +1,15 @@ import { NotionIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { NotionResponse } from '@/tools/notion/types' export const NotionBlock: BlockConfig = { type: 'notion', name: 'Notion', description: 'Manage Notion pages', + authMode: AuthMode.OAuth, longDescription: - 'Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace. Requires OAuth.', + 'Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace.', docsLink: 'https://docs.sim.ai/tools/notion', category: 'tools', bgColor: '#181C1E', diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index ca39032910..512a2a29b1 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -1,13 +1,14 @@ import { MicrosoftOneDriveIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { OneDriveResponse } from '@/tools/onedrive/types' export const OneDriveBlock: BlockConfig = { type: 'onedrive', name: 'OneDrive', description: 'Create, upload, and list files', - longDescription: - 'Integrate OneDrive into the workflow. Can create, upload, and list files. Requires OAuth.', + authMode: AuthMode.OAuth, + longDescription: 'Integrate OneDrive into the workflow. Can create, upload, and list files.', docsLink: 'https://docs.sim.ai/tools/onedrive', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/openai.ts b/apps/sim/blocks/blocks/openai.ts index 4df9835640..aac744f0e5 100644 --- a/apps/sim/blocks/blocks/openai.ts +++ b/apps/sim/blocks/blocks/openai.ts @@ -1,12 +1,13 @@ import { OpenAIIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' export const OpenAIBlock: BlockConfig = { type: 'openai', name: 'Embeddings', description: 'Generate Open AI embeddings', - longDescription: - 'Integrate Embeddings into the workflow. Can generate embeddings from text. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Embeddings into the workflow. Can generate embeddings from text.', category: 'tools', docsLink: 'https://docs.sim.ai/tools/openai', bgColor: '#10a37f', diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 91f583baff..27dbb7e628 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -1,15 +1,18 @@ import { OutlookIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { OutlookResponse } from '@/tools/outlook/types' export const OutlookBlock: BlockConfig = { type: 'outlook', name: 'Outlook', description: 'Access Outlook', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Outlook into the workflow. Can read, draft, and send email messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a new email is received.', + 'Integrate Outlook into the workflow. Can read, draft, and send email messages. Can be used in trigger mode to trigger a workflow when a new email is received.', docsLink: 'https://docs.sim.ai/tools/outlook', category: 'tools', + triggerAllowed: true, bgColor: '#E0E0E0', icon: OutlookIcon, subBlocks: [ diff --git a/apps/sim/blocks/blocks/parallel.ts b/apps/sim/blocks/blocks/parallel.ts index e6714f25e3..69de17d1d4 100644 --- a/apps/sim/blocks/blocks/parallel.ts +++ b/apps/sim/blocks/blocks/parallel.ts @@ -1,12 +1,13 @@ import { ParallelIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { ToolResponse } from '@/tools/types' export const ParallelBlock: BlockConfig = { type: 'parallel_ai', name: 'Parallel AI', description: 'Search with Parallel AI', - longDescription: 'Integrate Parallel AI into the workflow. Can search the web. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Parallel AI into the workflow. Can search the web.', docsLink: 'https://docs.parallel.ai/search-api/search-quickstart', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/perplexity.ts b/apps/sim/blocks/blocks/perplexity.ts index 15801c3340..fecae112be 100644 --- a/apps/sim/blocks/blocks/perplexity.ts +++ b/apps/sim/blocks/blocks/perplexity.ts @@ -1,5 +1,5 @@ import { PerplexityIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { PerplexityChatResponse } from '@/tools/perplexity/types' export const PerplexityBlock: BlockConfig = { @@ -7,7 +7,8 @@ export const PerplexityBlock: BlockConfig = { name: 'Perplexity', description: 'Use Perplexity AI chat models', longDescription: - 'Integrate Perplexity into the workflow. Can generate completions using Perplexity AI chat models. Requires API Key.', + 'Integrate Perplexity into the workflow. Can generate completions using Perplexity AI chat models.', + authMode: AuthMode.ApiKey, docsLink: 'https://docs.sim.ai/tools/perplexity', category: 'tools', bgColor: '#20808D', // Perplexity turquoise color diff --git a/apps/sim/blocks/blocks/pinecone.ts b/apps/sim/blocks/blocks/pinecone.ts index 52c27b9ff1..36a6c5ca57 100644 --- a/apps/sim/blocks/blocks/pinecone.ts +++ b/apps/sim/blocks/blocks/pinecone.ts @@ -1,13 +1,15 @@ import { PineconeIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { PineconeResponse } from '@/tools/pinecone/types' export const PineconeBlock: BlockConfig = { type: 'pinecone', name: 'Pinecone', description: 'Use Pinecone vector database', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors. Requires API Key.', + 'Integrate Pinecone into the workflow. Can generate embeddings, upsert text, search with text, fetch vectors, and search with vectors.', docsLink: 'https://docs.sim.ai/tools/pinecone', category: 'tools', bgColor: '#0D1117', diff --git a/apps/sim/blocks/blocks/qdrant.ts b/apps/sim/blocks/blocks/qdrant.ts index 9673ce8a3f..a06743764e 100644 --- a/apps/sim/blocks/blocks/qdrant.ts +++ b/apps/sim/blocks/blocks/qdrant.ts @@ -1,13 +1,14 @@ import { QdrantIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { QdrantResponse } from '@/tools/qdrant/types' export const QdrantBlock: BlockConfig = { type: 'qdrant', name: 'Qdrant', description: 'Use Qdrant vector database', - longDescription: - 'Integrate Qdrant into the workflow. Can upsert, search, and fetch points. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Qdrant into the workflow. Can upsert, search, and fetch points.', docsLink: 'https://qdrant.tech/documentation/', category: 'tools', bgColor: '#1A223F', diff --git a/apps/sim/blocks/blocks/reddit.ts b/apps/sim/blocks/blocks/reddit.ts index 102e803a24..ffbe0509e2 100644 --- a/apps/sim/blocks/blocks/reddit.ts +++ b/apps/sim/blocks/blocks/reddit.ts @@ -1,13 +1,15 @@ import { RedditIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { RedditResponse } from '@/tools/reddit/types' export const RedditBlock: BlockConfig = { type: 'reddit', name: 'Reddit', description: 'Access Reddit data and content', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Reddit into the workflow. Can get posts and comments from a subreddit. Requires OAuth.', + 'Integrate Reddit into the workflow. Can get posts and comments from a subreddit.', docsLink: 'https://docs.sim.ai/tools/reddit', category: 'tools', bgColor: '#FF5700', diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts index b84f722a59..6c909650b3 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -1,6 +1,6 @@ import { ConnectIcon } from '@/components/icons' import { isHosted } from '@/lib/environment' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { ProviderId } from '@/providers/types' import { getAllModelProviders, @@ -108,6 +108,7 @@ export const RouterBlock: BlockConfig = { type: 'router', name: 'Router', description: 'Route workflow', + authMode: AuthMode.ApiKey, longDescription: 'This is a core workflow block. Intelligently direct workflow execution to different paths based on input analysis. Use natural language to instruct the router to route to certain blocks based on the input.', category: 'blocks', diff --git a/apps/sim/blocks/blocks/s3.ts b/apps/sim/blocks/blocks/s3.ts index c29dbabfcb..577c6cc374 100644 --- a/apps/sim/blocks/blocks/s3.ts +++ b/apps/sim/blocks/blocks/s3.ts @@ -1,11 +1,13 @@ import { S3Icon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { S3Response } from '@/tools/s3/types' export const S3Block: BlockConfig = { type: 's3', name: 'S3', description: 'View S3 files', + authMode: AuthMode.ApiKey, longDescription: 'Integrate S3 into the workflow. Can get presigned URLs for S3 objects. Requires access key and secret access key.', docsLink: 'https://docs.sim.ai/tools/s3', diff --git a/apps/sim/blocks/blocks/serper.ts b/apps/sim/blocks/blocks/serper.ts index f98ebe4a57..267adafbf4 100644 --- a/apps/sim/blocks/blocks/serper.ts +++ b/apps/sim/blocks/blocks/serper.ts @@ -1,12 +1,14 @@ import { SerperIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { SearchResponse } from '@/tools/serper/types' export const SerperBlock: BlockConfig = { type: 'serper', name: 'Serper', description: 'Search the web using Serper', - longDescription: 'Integrate Serper into the workflow. Can search the web. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Serper into the workflow. Can search the web.', docsLink: 'https://docs.sim.ai/tools/serper', category: 'tools', bgColor: '#2B3543', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 11212c9d7b..18fba7a5c2 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -1,13 +1,15 @@ import { MicrosoftSharepointIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { SharepointResponse } from '@/tools/sharepoint/types' export const SharepointBlock: BlockConfig = { type: 'sharepoint', name: 'Sharepoint', description: 'Read and create pages', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Sharepoint into the workflow. Can read and create pages, and list sites. Requires OAuth.', + 'Integrate Sharepoint into the workflow. Can read and create pages, and list sites.', docsLink: 'https://docs.sim.ai/tools/sharepoint', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 89cab78483..305bb32c29 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -1,17 +1,20 @@ import { SlackIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { SlackResponse } from '@/tools/slack/types' export const SlackBlock: BlockConfig = { type: 'slack', name: 'Slack', description: 'Send messages to Slack or trigger workflows from Slack events', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Slack into the workflow. Can send messages, create canvases, and read messages. Requires OAuth. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.', + 'Integrate Slack into the workflow. Can send messages, create canvases, and read messages. Requires Bot Token instead of OAuth in advanced mode. Can be used in trigger mode to trigger a workflow when a message is sent to a channel.', docsLink: 'https://docs.sim.ai/tools/slack', category: 'tools', bgColor: '#611f69', icon: SlackIcon, + triggerAllowed: true, subBlocks: [ { id: 'operation', diff --git a/apps/sim/blocks/blocks/stagehand.ts b/apps/sim/blocks/blocks/stagehand.ts index ac23277833..6ef63cbf69 100644 --- a/apps/sim/blocks/blocks/stagehand.ts +++ b/apps/sim/blocks/blocks/stagehand.ts @@ -1,5 +1,5 @@ import { StagehandIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { ToolResponse } from '@/tools/types' export interface StagehandExtractResponse extends ToolResponse { @@ -12,8 +12,9 @@ export const StagehandBlock: BlockConfig = { type: 'stagehand', name: 'Stagehand Extract', description: 'Extract data from websites', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Stagehand into the workflow. Can extract structured data from webpages. Requires API Key.', + 'Integrate Stagehand into the workflow. Can extract structured data from webpages.', docsLink: 'https://docs.sim.ai/tools/stagehand', category: 'tools', bgColor: '#FFC83C', diff --git a/apps/sim/blocks/blocks/stagehand_agent.ts b/apps/sim/blocks/blocks/stagehand_agent.ts index c2dc79cc65..f6225bb847 100644 --- a/apps/sim/blocks/blocks/stagehand_agent.ts +++ b/apps/sim/blocks/blocks/stagehand_agent.ts @@ -1,13 +1,14 @@ import { StagehandIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { StagehandAgentResponse } from '@/tools/stagehand/types' export const StagehandAgentBlock: BlockConfig = { type: 'stagehand_agent', name: 'Stagehand Agent', description: 'Autonomous web browsing agent', + authMode: AuthMode.ApiKey, longDescription: - 'Integrate Stagehand Agent into the workflow. Can navigate the web and perform tasks. Requires API Key.', + 'Integrate Stagehand Agent into the workflow. Can navigate the web and perform tasks.', docsLink: 'https://docs.sim.ai/tools/stagehand_agent', category: 'tools', bgColor: '#FFC83C', diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index 09b6f01bb4..2bd6c57d95 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -1,6 +1,6 @@ import { SupabaseIcon } from '@/components/icons' import { createLogger } from '@/lib/logs/console/logger' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import type { SupabaseResponse } from '@/tools/supabase/types' const logger = createLogger('SupabaseBlock') @@ -9,6 +9,7 @@ export const SupabaseBlock: BlockConfig = { type: 'supabase', name: 'Supabase', description: 'Use Supabase database', + authMode: AuthMode.ApiKey, longDescription: 'Integrate Supabase into the workflow. Can get many rows, get, create, update, delete, and upsert a row.', docsLink: 'https://docs.sim.ai/tools/supabase', diff --git a/apps/sim/blocks/blocks/tavily.ts b/apps/sim/blocks/blocks/tavily.ts index 5e8196c867..a75013df9f 100644 --- a/apps/sim/blocks/blocks/tavily.ts +++ b/apps/sim/blocks/blocks/tavily.ts @@ -1,11 +1,13 @@ import { TavilyIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { TavilyResponse } from '@/tools/tavily/types' export const TavilyBlock: BlockConfig = { type: 'tavily', name: 'Tavily', description: 'Search and extract information', + authMode: AuthMode.ApiKey, longDescription: 'Integrate Tavily into the workflow. Can search the web and extract content from specific URLs. Requires API Key.', category: 'tools', diff --git a/apps/sim/blocks/blocks/telegram.ts b/apps/sim/blocks/blocks/telegram.ts index 66d5b8dde1..3f247c525a 100644 --- a/apps/sim/blocks/blocks/telegram.ts +++ b/apps/sim/blocks/blocks/telegram.ts @@ -1,17 +1,20 @@ import { TelegramIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { TelegramMessageResponse } from '@/tools/telegram/types' export const TelegramBlock: BlockConfig = { type: 'telegram', name: 'Telegram', description: 'Send messages through Telegram or trigger workflows from Telegram events', + authMode: AuthMode.BotToken, longDescription: 'Integrate Telegram into the workflow. Can send messages. Can be used in trigger mode to trigger a workflow when a message is sent to a chat.', docsLink: 'https://docs.sim.ai/tools/telegram', category: 'tools', bgColor: '#E0E0E0', icon: TelegramIcon, + triggerAllowed: true, subBlocks: [ { id: 'botToken', diff --git a/apps/sim/blocks/blocks/translate.ts b/apps/sim/blocks/blocks/translate.ts index 360221edeb..a4e4f5c199 100644 --- a/apps/sim/blocks/blocks/translate.ts +++ b/apps/sim/blocks/blocks/translate.ts @@ -1,6 +1,6 @@ import { TranslateIcon } from '@/components/icons' import { isHosted } from '@/lib/environment' -import type { BlockConfig } from '@/blocks/types' +import { AuthMode, type BlockConfig } from '@/blocks/types' import { getAllModelProviders, getBaseModelProviders, @@ -29,6 +29,7 @@ export const TranslateBlock: BlockConfig = { type: 'translate', name: 'Translate', description: 'Translate text to any language', + authMode: AuthMode.ApiKey, longDescription: 'Integrate Translate into the workflow. Can translate text to any language.', docsLink: 'https://docs.sim.ai/tools/translate', category: 'tools', diff --git a/apps/sim/blocks/blocks/twilio.ts b/apps/sim/blocks/blocks/twilio.ts index 6a51b901f2..f997f0dd86 100644 --- a/apps/sim/blocks/blocks/twilio.ts +++ b/apps/sim/blocks/blocks/twilio.ts @@ -1,11 +1,13 @@ import { TwilioIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { TwilioSMSBlockOutput } from '@/tools/twilio/types' export const TwilioSMSBlock: BlockConfig = { type: 'twilio_sms', name: 'Twilio SMS', description: 'Send SMS messages', + authMode: AuthMode.ApiKey, longDescription: 'Integrate Twilio into the workflow. Can send SMS messages.', category: 'tools', bgColor: '#F22F46', // Twilio brand color diff --git a/apps/sim/blocks/blocks/typeform.ts b/apps/sim/blocks/blocks/typeform.ts index f98f1a87fd..c63334e71e 100644 --- a/apps/sim/blocks/blocks/typeform.ts +++ b/apps/sim/blocks/blocks/typeform.ts @@ -1,11 +1,13 @@ import { TypeformIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { TypeformResponse } from '@/tools/typeform/types' export const TypeformBlock: BlockConfig = { type: 'typeform', name: 'Typeform', description: 'Interact with Typeform', + authMode: AuthMode.ApiKey, longDescription: 'Integrate Typeform into the workflow. Can retrieve responses, download files, and get form insights. Requires API Key.', docsLink: 'https://docs.sim.ai/tools/typeform', diff --git a/apps/sim/blocks/blocks/vision.ts b/apps/sim/blocks/blocks/vision.ts index dfab793e1e..5d56ad1b1d 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -1,13 +1,14 @@ import { EyeIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { VisionResponse } from '@/tools/vision/types' export const VisionBlock: BlockConfig = { type: 'vision', name: 'Vision', description: 'Analyze images with vision models', - longDescription: - 'Integrate Vision into the workflow. Can analyze images with vision models. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate Vision into the workflow. Can analyze images with vision models.', docsLink: 'https://docs.sim.ai/tools/vision', category: 'tools', bgColor: '#4D5FFF', diff --git a/apps/sim/blocks/blocks/wealthbox.ts b/apps/sim/blocks/blocks/wealthbox.ts index 7e0e59c6e7..7022cc8342 100644 --- a/apps/sim/blocks/blocks/wealthbox.ts +++ b/apps/sim/blocks/blocks/wealthbox.ts @@ -1,13 +1,15 @@ import { WealthboxIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { WealthboxResponse } from '@/tools/wealthbox/types' export const WealthboxBlock: BlockConfig = { type: 'wealthbox', name: 'Wealthbox', description: 'Interact with Wealthbox', + authMode: AuthMode.OAuth, longDescription: - 'Integrate Wealthbox into the workflow. Can read and write notes, read and write contacts, and read and write tasks. Requires OAuth.', + 'Integrate Wealthbox into the workflow. Can read and write notes, read and write contacts, and read and write tasks.', docsLink: 'https://docs.sim.ai/tools/wealthbox', category: 'tools', bgColor: '#E0E0E0', diff --git a/apps/sim/blocks/blocks/webhook.ts b/apps/sim/blocks/blocks/webhook.ts index d5b870fa00..27874f33a2 100644 --- a/apps/sim/blocks/blocks/webhook.ts +++ b/apps/sim/blocks/blocks/webhook.ts @@ -13,6 +13,7 @@ import { WhatsAppIcon, } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' const getWebhookProviderIcon = (provider: string) => { const iconMap: Record> = { @@ -36,9 +37,11 @@ export const WebhookBlock: BlockConfig = { type: 'webhook', name: 'Webhook', description: 'Trigger workflow execution from external webhooks', + authMode: AuthMode.OAuth, category: 'triggers', icon: WebhookIcon, bgColor: '#10B981', // Green color for triggers + triggerAllowed: true, hideFromToolbar: true, // Hidden for backwards compatibility - use generic webhook trigger instead subBlocks: [ diff --git a/apps/sim/blocks/blocks/whatsapp.ts b/apps/sim/blocks/blocks/whatsapp.ts index 4899f3e5ad..77801d1ad8 100644 --- a/apps/sim/blocks/blocks/whatsapp.ts +++ b/apps/sim/blocks/blocks/whatsapp.ts @@ -1,16 +1,19 @@ import { WhatsAppIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { WhatsAppResponse } from '@/tools/whatsapp/types' export const WhatsAppBlock: BlockConfig = { type: 'whatsapp', name: 'WhatsApp', description: 'Send WhatsApp messages', + authMode: AuthMode.ApiKey, longDescription: 'Integrate WhatsApp into the workflow. Can send messages.', docsLink: 'https://docs.sim.ai/tools/whatsapp', category: 'tools', bgColor: '#25D366', icon: WhatsAppIcon, + triggerAllowed: true, subBlocks: [ { id: 'phoneNumber', diff --git a/apps/sim/blocks/blocks/x.ts b/apps/sim/blocks/blocks/x.ts index a47fa65031..2b675925fe 100644 --- a/apps/sim/blocks/blocks/x.ts +++ b/apps/sim/blocks/blocks/x.ts @@ -1,13 +1,15 @@ import { xIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { XResponse } from '@/tools/x/types' export const XBlock: BlockConfig = { type: 'x', name: 'X', description: 'Interact with X', + authMode: AuthMode.OAuth, longDescription: - 'Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile. Requires OAuth.', + 'Integrate X into the workflow. Can post a new tweet, get tweet details, search tweets, and get user profile.', docsLink: 'https://docs.sim.ai/tools/x', category: 'tools', bgColor: '#000000', // X's black color diff --git a/apps/sim/blocks/blocks/youtube.ts b/apps/sim/blocks/blocks/youtube.ts index b5c53deafa..982f924c0b 100644 --- a/apps/sim/blocks/blocks/youtube.ts +++ b/apps/sim/blocks/blocks/youtube.ts @@ -1,16 +1,20 @@ import { YouTubeIcon } from '@/components/icons' -import type { BlockConfig } from '@/blocks/types' +import type { BlockConfig, BlockIcon } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import type { YouTubeSearchResponse } from '@/tools/youtube/types' +const YouTubeBlockIcon: BlockIcon = (props) => YouTubeIcon(props as any) + export const YouTubeBlock: BlockConfig = { type: 'youtube', name: 'YouTube', description: 'Search for videos on YouTube', - longDescription: 'Integrate YouTube into the workflow. Can search for videos. Requires API Key.', + authMode: AuthMode.ApiKey, + longDescription: 'Integrate YouTube into the workflow. Can search for videos.', docsLink: 'https://docs.sim.ai/tools/youtube', category: 'tools', bgColor: '#FF0000', - icon: YouTubeIcon, + icon: YouTubeBlockIcon, subBlocks: [ { id: 'query', diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 55158b6d89..77d2bfe0da 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -7,6 +7,13 @@ export type PrimitiveValueType = 'string' | 'number' | 'boolean' | 'json' | 'arr export type BlockCategory = 'blocks' | 'tools' | 'triggers' +// Authentication modes for sub-blocks and summaries +export enum AuthMode { + OAuth = 'oauth', + ApiKey = 'api_key', + BotToken = 'bot_token', +} + export type GenerationType = | 'javascript-function-body' | 'typescript-function-body' @@ -186,6 +193,8 @@ export interface BlockConfig { bgColor: string icon: BlockIcon subBlocks: SubBlockConfig[] + triggerAllowed?: boolean + authMode?: AuthMode tools: { access: string[] config?: { diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts index 34aa738c38..af50f30b2b 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts @@ -61,6 +61,7 @@ export class GetBlocksMetadataClientTool extends BaseClientTool { this.setState(ClientToolCallState.success) } catch (error: any) { const message = error instanceof Error ? error.message : String(error) + logger.error('Execute failed', { message }) await this.markToolComplete(500, message) this.setState(ClientToolCallState.error) } diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts index a44bade416..8859100c13 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts @@ -5,7 +5,7 @@ import { } from '@/lib/copilot/tools/shared/schemas' import { createLogger } from '@/lib/logs/console/logger' import { registry as blockRegistry } from '@/blocks/registry' -import { tools as toolsRegistry } from '@/tools/registry' +import type { BlockConfig } from '@/blocks/types' export const getBlocksAndToolsServerTool: BaseServerTool< ReturnType, @@ -23,23 +23,35 @@ export const getBlocksAndToolsServerTool: BaseServerTool< if ((blockConfig as any).hideFromToolbar) return false return true }) - .forEach(([blockType, blockConfig]: any) => { - blocks.push({ id: blockType, type: blockType, name: blockConfig.name || blockType }) + .forEach(([blockType, blockConfig]: [string, BlockConfig]) => { + blocks.push({ + type: blockType, + name: blockConfig.name, + triggerAllowed: !!blockConfig.triggerAllowed, + }) }) - const specialBlocks = { loop: { name: 'Loop' }, parallel: { name: 'Parallel' } } + const specialBlocks = { + loop: { + name: 'Loop', + longDescription: + 'Control flow block for iterating over collections or repeating actions in a loop', + }, + parallel: { + name: 'Parallel', + longDescription: 'Control flow block for executing multiple branches simultaneously', + }, + } Object.entries(specialBlocks).forEach(([blockType, info]) => { - if (!blocks.some((b) => b.id === blockType)) { - blocks.push({ id: blockType, type: blockType, name: (info as any).name }) + if (!blocks.some((b) => b.type === blockType)) { + blocks.push({ + type: blockType, + name: (info as any).name, + longDescription: (info as any).longDescription, + }) } }) - const tools: any[] = Object.entries(toolsRegistry).map(([toolId, toolConfig]: any) => ({ - id: toolId, - type: toolId, - name: toolConfig?.name || toolId, - })) - - return GetBlocksAndToolsResult.parse({ blocks, tools }) + return GetBlocksAndToolsResult.parse({ blocks }) }, } diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index 9103bed30d..bc3f20833a 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -7,7 +7,45 @@ import { } from '@/lib/copilot/tools/shared/schemas' import { createLogger } from '@/lib/logs/console/logger' import { registry as blockRegistry } from '@/blocks/registry' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' import { tools as toolsRegistry } from '@/tools/registry' +import { TRIGGER_REGISTRY } from '@/triggers' + +export interface CopilotSubblockMetadata { + id: string + type: string + title?: string + required?: boolean + description?: string +} + +export interface CopilotToolMetadata { + id: string + name: string + description?: string + inputs?: any + outputs?: any +} + +export interface CopilotTriggerMetadata { + id: string + outputs?: any +} + +export interface CopilotBlockMetadata { + id: string + name: string + description: string + inputs: Record + outputs: Record + triggerAllowed?: boolean + authType?: 'OAuth' | 'API Key' | 'Bot Token' + tools: CopilotToolMetadata[] + triggers: CopilotTriggerMetadata[] + parameters: CopilotSubblockMetadata[] + yamlDocumentation?: string +} export const getBlocksMetadataServerTool: BaseServerTool< ReturnType, @@ -22,35 +60,68 @@ export const getBlocksMetadataServerTool: BaseServerTool< const logger = createLogger('GetBlocksMetadataServerTool') logger.debug('Executing get_blocks_metadata', { count: blockIds?.length }) - const result: Record = {} + const result: Record = {} for (const blockId of blockIds || []) { - let metadata: any = {} + let metadata: any if (SPECIAL_BLOCKS_METADATA[blockId]) { - metadata = { ...SPECIAL_BLOCKS_METADATA[blockId] } - metadata.tools = metadata.tools?.access || [] + const specialBlock = SPECIAL_BLOCKS_METADATA[blockId] + metadata = { + ...specialBlock, + tools: [], + triggers: [], + parameters: specialBlock.subBlocks ? specialBlock.subBlocks.map(simplifySubBlock) : [], + }(metadata as any).subBlocks = undefined } else { - const blockConfig: any = (blockRegistry as any)[blockId] + const blockConfig: BlockConfig | undefined = (blockRegistry as any)[blockId] if (!blockConfig) { logger.debug('Block not found in registry', { blockId }) continue } + + if (blockConfig.hideFromToolbar) { + logger.debug('Skipping block hidden from toolbar', { blockId }) + continue + } + const tools: CopilotToolMetadata[] = Array.isArray(blockConfig.tools?.access) + ? blockConfig.tools!.access.map((toolId) => { + const tool: any = (toolsRegistry as any)[toolId] + if (!tool) return { id: toolId, name: toolId } + return { + id: toolId, + name: tool.name || toolId, + description: tool.description || '', + inputs: tool.params || {}, + outputs: tool.outputs || {}, + } + }) + : [] + + const triggers: CopilotTriggerMetadata[] = [] + const availableTriggerIds = blockConfig.triggers?.available || [] + for (const tid of availableTriggerIds) { + const trig = TRIGGER_REGISTRY[tid] + triggers.push({ + id: tid, + outputs: trig?.outputs || {}, + }) + } + + const parameters: CopilotSubblockMetadata[] = Array.isArray(blockConfig.subBlocks) + ? blockConfig.subBlocks.map(simplifySubBlock) + : [] + metadata = { id: blockId, name: blockConfig.name || blockId, - description: blockConfig.description || '', - longDescription: blockConfig.longDescription, - category: blockConfig.category, - bgColor: blockConfig.bgColor, + description: blockConfig.longDescription || blockConfig.description || '', inputs: blockConfig.inputs || {}, outputs: blockConfig.outputs || {}, - tools: blockConfig.tools?.access || [], - hideFromToolbar: blockConfig.hideFromToolbar, - } - if (blockConfig.subBlocks && Array.isArray(blockConfig.subBlocks)) { - metadata.subBlocks = processSubBlocks(blockConfig.subBlocks) - } else { - metadata.subBlocks = [] + triggerAllowed: !!blockConfig.triggerAllowed, + authType: resolveAuthType(blockConfig.authMode), + tools, + triggers, + parameters, } } @@ -73,87 +144,41 @@ export const getBlocksMetadataServerTool: BaseServerTool< } } catch {} - if (Array.isArray(metadata.tools) && metadata.tools.length > 0) { - metadata.toolDetails = {} - for (const toolId of metadata.tools) { - const tool = (toolsRegistry as any)[toolId] - if (tool) { - metadata.toolDetails[toolId] = { name: tool.name, description: tool.description } - } - } + if (metadata) { + result[blockId] = metadata as CopilotBlockMetadata } - - result[blockId] = metadata } return GetBlocksMetadataResult.parse({ metadata: result }) }, } -function resolveSubBlockOptions(options: any): any[] { - try { - if (typeof options === 'function') { - const resolved = options() - return Array.isArray(resolved) ? resolved : [] - } - return Array.isArray(options) ? options : [] - } catch { - return [] +function simplifySubBlock(sb: any): CopilotSubblockMetadata { + const simplified: CopilotSubblockMetadata = { + id: sb.id, + type: sb.type, } + if (sb.title) simplified.title = sb.title + if (sb.required) simplified.required = sb.required + if (sb.description) simplified.description = sb.description + return simplified } -function processSubBlocks(subBlocks: any[]): any[] { - if (!Array.isArray(subBlocks)) return [] - return subBlocks.map((subBlock) => { - const processed: any = { - id: subBlock.id, - title: subBlock.title, - type: subBlock.type, - layout: subBlock.layout, - mode: subBlock.mode, - required: subBlock.required, - placeholder: subBlock.placeholder, - description: subBlock.description, - hidden: subBlock.hidden, - condition: subBlock.condition, - min: subBlock.min, - max: subBlock.max, - step: subBlock.step, - integer: subBlock.integer, - rows: subBlock.rows, - password: subBlock.password, - multiSelect: subBlock.multiSelect, - language: subBlock.language, - generationType: subBlock.generationType, - provider: subBlock.provider, - serviceId: subBlock.serviceId, - requiredScopes: subBlock.requiredScopes, - mimeType: subBlock.mimeType, - acceptedTypes: subBlock.acceptedTypes, - multiple: subBlock.multiple, - maxSize: subBlock.maxSize, - connectionDroppable: subBlock.connectionDroppable, - columns: subBlock.columns, - value: typeof subBlock.value === 'function' ? 'function' : undefined, - wandConfig: subBlock.wandConfig, - } - if (subBlock.options) { - const resolvedOptions = resolveSubBlockOptions(subBlock.options) - processed.options = resolvedOptions.map((option: any) => ({ - label: option.label, - id: option.id, - hasIcon: !!option.icon, - })) - } - return Object.fromEntries(Object.entries(processed).filter(([_, v]) => v !== undefined)) - }) +function resolveAuthType( + authMode: AuthMode | undefined +): 'OAuth' | 'API Key' | 'Bot Token' | undefined { + if (!authMode) return undefined + if (authMode === AuthMode.OAuth) return 'OAuth' + if (authMode === AuthMode.ApiKey) return 'API Key' + if (authMode === AuthMode.BotToken) return 'Bot Token' + return undefined } const DOCS_FILE_MAPPING: Record = {} const SPECIAL_BLOCKS_METADATA: Record = { loop: { - type: 'loop', + id: 'loop', name: 'Loop', description: 'Control flow block for iterating over collections or repeating actions', inputs: { @@ -168,7 +193,6 @@ const SPECIAL_BLOCKS_METADATA: Record = { currentItem: 'any', totalIterations: 'number', }, - tools: { access: [] }, subBlocks: [ { id: 'loopType', @@ -208,7 +232,7 @@ const SPECIAL_BLOCKS_METADATA: Record = { ], }, parallel: { - type: 'parallel', + id: 'parallel', name: 'Parallel', description: 'Control flow block for executing multiple branches simultaneously', inputs: { @@ -218,7 +242,6 @@ const SPECIAL_BLOCKS_METADATA: Record = { maxConcurrency: { type: 'number', required: false, default: 10, minimum: 1, maximum: 50 }, }, outputs: { results: 'array', branchId: 'number', branchItem: 'any', totalBranches: 'number' }, - tools: { access: [] }, subBlocks: [ { id: 'parallelType', diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts index 2ee5bd07a0..8564eebfa4 100644 --- a/apps/sim/lib/copilot/tools/shared/schemas.ts +++ b/apps/sim/lib/copilot/tools/shared/schemas.ts @@ -10,8 +10,16 @@ export type ExecuteResponseSuccess = z.infer From b7ad4ec92a92ae14a36ba792e68f2c24423d500a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 12:40:37 -0700 Subject: [PATCH 10/43] forgot long description --- apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts index 8859100c13..ab55fa952c 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts @@ -27,6 +27,7 @@ export const getBlocksAndToolsServerTool: BaseServerTool< blocks.push({ type: blockType, name: blockConfig.name, + description: blockConfig.longDescription, triggerAllowed: !!blockConfig.triggerAllowed, }) }) From e528b4c3bf8e05d0d5d0631f65ae647581fc814a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Tue, 16 Sep 2025 16:17:47 -0700 Subject: [PATCH 11/43] Update metadata params --- .../server/blocks/get-blocks-metadata-tool.ts | 203 ++++++++++++++++-- 1 file changed, 191 insertions(+), 12 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index bc3f20833a..b685472c75 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -37,13 +37,23 @@ export interface CopilotBlockMetadata { id: string name: string description: string - inputs: Record - outputs: Record + commonParameters: Record triggerAllowed?: boolean authType?: 'OAuth' | 'API Key' | 'Bot Token' tools: CopilotToolMetadata[] triggers: CopilotTriggerMetadata[] - parameters: CopilotSubblockMetadata[] + operationParameters: Record + operations?: Record< + string, + { + toolId?: string + toolName?: string + description?: string + inputs?: Record + outputs?: Record + parameters?: CopilotSubblockMetadata[] + } + > yamlDocumentation?: string } @@ -66,12 +76,20 @@ export const getBlocksMetadataServerTool: BaseServerTool< if (SPECIAL_BLOCKS_METADATA[blockId]) { const specialBlock = SPECIAL_BLOCKS_METADATA[blockId] + const { operationParameters } = splitParametersByOperation( + specialBlock.subBlocks || [], + specialBlock.inputs || {} + ) metadata = { - ...specialBlock, + id: specialBlock.id, + name: specialBlock.name, + description: specialBlock.description || '', + commonParameters: specialBlock.inputs || {}, tools: [], triggers: [], - parameters: specialBlock.subBlocks ? specialBlock.subBlocks.map(simplifySubBlock) : [], - }(metadata as any).subBlocks = undefined + operationParameters, + } + ;(metadata as any).subBlocks = undefined } else { const blockConfig: BlockConfig | undefined = (blockRegistry as any)[blockId] if (!blockConfig) { @@ -107,21 +125,45 @@ export const getBlocksMetadataServerTool: BaseServerTool< }) } - const parameters: CopilotSubblockMetadata[] = Array.isArray(blockConfig.subBlocks) - ? blockConfig.subBlocks.map(simplifySubBlock) - : [] + const blockInputs = computeBlockLevelInputs(blockConfig) + const { operationParameters } = splitParametersByOperation( + Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : [], + blockInputs + ) + + const operationInputs = computeOperationLevelInputs(blockConfig) + const operationIds = resolveOperationIds(blockConfig, operationParameters) + const operations: Record = {} + for (const opId of operationIds) { + const resolvedToolId = resolveToolIdForOperation(blockConfig, opId) + const toolCfg: any = resolvedToolId ? (toolsRegistry as any)[resolvedToolId] : undefined + const toolParams: Record = toolCfg?.params || {} + const toolOutputs: Record = toolCfg?.outputs || {} + const filteredToolParams: Record = {} + for (const [k, v] of Object.entries(toolParams)) { + if (!(k in blockInputs)) filteredToolParams[k] = v + } + operations[opId] = { + toolId: resolvedToolId, + toolName: toolCfg?.name || resolvedToolId, + description: toolCfg?.description || undefined, + inputs: { ...filteredToolParams, ...(operationInputs[opId] || {}) }, + outputs: toolOutputs, + parameters: operationParameters[opId] || [], + } + } metadata = { id: blockId, name: blockConfig.name || blockId, description: blockConfig.longDescription || blockConfig.description || '', - inputs: blockConfig.inputs || {}, - outputs: blockConfig.outputs || {}, + commonParameters: blockInputs, triggerAllowed: !!blockConfig.triggerAllowed, authType: resolveAuthType(blockConfig.authMode), tools, triggers, - parameters, + operationParameters, + operations, } } @@ -174,6 +216,143 @@ function resolveAuthType( return undefined } +function normalizeCondition(condition: any): any | undefined { + try { + if (!condition) return undefined + if (typeof condition === 'function') { + return condition() + } + return condition + } catch { + return undefined + } +} + +function splitParametersByOperation( + subBlocks: any[], + blockInputsForDescriptions?: Record +): { + commonParameters: CopilotSubblockMetadata[] + operationParameters: Record +} { + const commonParameters: CopilotSubblockMetadata[] = [] + const operationParameters: Record = {} + + for (const sb of subBlocks || []) { + const cond = normalizeCondition(sb.condition) + const simplified = simplifySubBlock(sb) + + if (cond && cond.field === 'operation' && !cond.not && cond.value !== undefined) { + const values: any[] = Array.isArray(cond.value) ? cond.value : [cond.value] + for (const v of values) { + const key = String(v) + if (!operationParameters[key]) operationParameters[key] = [] + operationParameters[key].push(simplified) + } + } else { + // Override description from blockInputs if available (by id or canonicalParamId) + if (blockInputsForDescriptions) { + const candidates = [sb.id, sb.canonicalParamId].filter(Boolean) + for (const key of candidates) { + const bi = (blockInputsForDescriptions as any)[key as string] + if (bi && typeof bi.description === 'string') { + simplified.description = bi.description + break + } + } + } + commonParameters.push(simplified) + } + } + + return { commonParameters, operationParameters } +} + +function computeBlockLevelInputs(blockConfig: BlockConfig): Record { + const inputs = blockConfig.inputs || {} + const subBlocks: any[] = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : [] + + // Build quick lookup of subBlocks by id and canonicalParamId + const byParamKey: Record = {} + for (const sb of subBlocks) { + if (sb.id) { + byParamKey[sb.id] = byParamKey[sb.id] || [] + byParamKey[sb.id].push(sb) + } + if (sb.canonicalParamId) { + byParamKey[sb.canonicalParamId] = byParamKey[sb.canonicalParamId] || [] + byParamKey[sb.canonicalParamId].push(sb) + } + } + + const blockInputs: Record = {} + for (const key of Object.keys(inputs)) { + const sbs = byParamKey[key] || [] + // If any related subBlock is gated by operation, treat as operation-level and exclude + const isOperationGated = sbs.some((sb) => { + const cond = normalizeCondition(sb.condition) + return cond && cond.field === 'operation' && !cond.not && cond.value !== undefined + }) + if (!isOperationGated) { + blockInputs[key] = inputs[key] + } + } + + return blockInputs +} + +function computeOperationLevelInputs(blockConfig: BlockConfig): Record> { + const inputs = blockConfig.inputs || {} + const subBlocks: any[] = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : [] + + const opInputs: Record> = {} + + // Map subblocks to inputs keys via id or canonicalParamId and collect by operation + for (const sb of subBlocks) { + const cond = normalizeCondition((sb as any).condition) + if (!cond || cond.field !== 'operation' || cond.not) continue + const keys: string[] = [] + if ((sb as any).canonicalParamId) keys.push((sb as any).canonicalParamId) + if ((sb as any).id) keys.push((sb as any).id) + const values: any[] = Array.isArray(cond.value) ? cond.value : [cond.value] + for (const key of keys) { + if (!(key in inputs)) continue + for (const v of values) { + const op = String(v) + if (!opInputs[op]) opInputs[op] = {} + opInputs[op][key] = inputs[key] + } + } + } + + return opInputs +} + +function resolveOperationIds( + blockConfig: BlockConfig, + operationParameters: Record +): string[] { + // Prefer explicit operation subblock options if present + const opBlock: any = (blockConfig.subBlocks || []).find((sb: any) => sb.id === 'operation') + if (opBlock && Array.isArray(opBlock.options)) { + const ids = opBlock.options.map((o: any) => o.id).filter(Boolean) + if (ids.length > 0) return ids + } + // Fallback: keys from operationParameters + return Object.keys(operationParameters) +} + +function resolveToolIdForOperation(blockConfig: BlockConfig, opId: string): string | undefined { + try { + const toolSelector = (blockConfig.tools as any)?.config?.tool + if (typeof toolSelector === 'function') { + const maybeToolId = toolSelector({ operation: opId }) + if (typeof maybeToolId === 'string') return maybeToolId + } + } catch {} + return undefined +} + const DOCS_FILE_MAPPING: Record = {} const SPECIAL_BLOCKS_METADATA: Record = { From cd4cb2cc77cc6a6f6e2cf19a58e40981615b81e2 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 17:22:27 -0700 Subject: [PATCH 12/43] progress --- .../app/api/workflows/[id]/execute/route.ts | 4 +- .../components/sub-block/components/index.ts | 1 + .../input-mapping/input-mapping.tsx | 294 ++++++++++++++++++ .../components/sub-block/sub-block.tsx | 12 + .../hooks/use-workflow-execution.ts | 28 +- .../[workspaceId]/w/[workflowId]/workflow.tsx | 12 +- apps/sim/blocks/blocks/input_trigger.ts | 37 +++ apps/sim/blocks/blocks/manual_trigger.ts | 26 -- apps/sim/blocks/blocks/workflow.ts | 1 + apps/sim/blocks/blocks/workflow_input.ts | 58 ++++ apps/sim/blocks/registry.ts | 6 +- apps/sim/blocks/types.ts | 1 + .../handlers/workflow/workflow-handler.ts | 44 ++- apps/sim/executor/index.ts | 71 ++++- apps/sim/executor/resolver/resolver.ts | 2 +- apps/sim/lib/workflows/block-outputs.ts | 8 +- apps/sim/lib/workflows/triggers.ts | 37 ++- 17 files changed, 561 insertions(+), 81 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx create mode 100644 apps/sim/blocks/blocks/input_trigger.ts delete mode 100644 apps/sim/blocks/blocks/manual_trigger.ts create mode 100644 apps/sim/blocks/blocks/workflow_input.ts diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index f3a2937e0c..d77ba91b27 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -274,8 +274,8 @@ async function executeWorkflow( ) // Determine API trigger start block - // API execution ONLY works with API trigger blocks (or legacy starter in api/run mode) - const startBlock = TriggerUtils.findStartBlock(mergedStates, 'api') + // Direct API execution ONLY works with API trigger blocks (or legacy starter in api/run mode) + const startBlock = TriggerUtils.findStartBlock(mergedStates, 'api', false) // isChildWorkflow = false if (!startBlock) { logger.error(`[${requestId}] No API trigger configured for this workflow`) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts index d132f528e0..2d16bb19ef 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/index.ts @@ -10,6 +10,7 @@ export { EvalInput } from './eval-input' export { FileSelectorInput } from './file-selector/file-selector-input' export { FileUpload } from './file-upload' export { FolderSelectorInput } from './folder-selector/components/folder-selector-input' +export { InputMapping } from './input-mapping/input-mapping' export { KnowledgeBaseSelector } from './knowledge-base-selector/knowledge-base-selector' export { LongInput } from './long-input' export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx new file mode 100644 index 0000000000..014df270f2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx @@ -0,0 +1,294 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { formatDisplayText } from '@/components/ui/formatted-text' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown' +import { cn } from '@/lib/utils' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +interface InputMappingProps { + blockId: string + subBlockId: string + isPreview?: boolean + previewValue?: any + disabled?: boolean +} + +// Simple mapping UI: for each field in child Input Trigger's inputFormat, render an input with TagDropdown support +export function InputMapping({ + blockId, + subBlockId, + isPreview = false, + previewValue, + disabled = false, +}: InputMappingProps) { + const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId) + const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId') + + const { workflows } = useWorkflowRegistry.getState() + + // Fetch child workflow state via registry API endpoint, using cached metadata when possible + // Here we rely on live store; the serializer/executor will resolve at runtime too. + // We only need the inputFormat from an Input Trigger in the selected child workflow state. + const [childInputFields, setChildInputFields] = useState>( + [] + ) + + useEffect(() => { + let isMounted = true + const controller = new AbortController() + async function fetchChildSchema() { + try { + if (!selectedWorkflowId) { + if (isMounted) setChildInputFields([]) + return + } + const res = await fetch(`/api/workflows/${selectedWorkflowId}`, { + signal: controller.signal, + }) + if (!res.ok) { + if (isMounted) setChildInputFields([]) + return + } + const { data } = await res.json() + const blocks = data?.state?.blocks || {} + const triggerEntry = Object.entries(blocks).find( + ([, b]: any) => b?.type === 'input_trigger' + ) + if (!triggerEntry) { + if (isMounted) setChildInputFields([]) + return + } + const triggerBlock = triggerEntry[1] as any + const inputFormat = triggerBlock?.subBlocks?.inputFormat?.value + if (Array.isArray(inputFormat)) { + const fields = inputFormat + .filter((f: any) => f && typeof f.name === 'string' && f.name.trim() !== '') + .map((f: any) => ({ name: f.name as string, type: f.type as string | undefined })) + if (isMounted) setChildInputFields(fields) + } else { + if (isMounted) setChildInputFields([]) + } + } catch { + if (isMounted) setChildInputFields([]) + } + } + fetchChildSchema() + return () => { + isMounted = false + controller.abort() + } + }, [selectedWorkflowId]) + + const valueObj: Record = useMemo(() => { + if (isPreview && previewValue && typeof previewValue === 'object') return previewValue + if (mapping && typeof mapping === 'object') return mapping as Record + try { + if (typeof mapping === 'string') return JSON.parse(mapping) + } catch {} + return {} + }, [mapping, isPreview, previewValue]) + + const update = (field: string, value: string) => { + if (disabled) return + const updated = { ...valueObj, [field]: value } + setMapping(updated) + } + + if (!selectedWorkflowId) { + return ( +
+ Select a workflow first. +
+ ) + } + + if (!childInputFields || childInputFields.length === 0) { + return ( +
+ The selected workflow must have an Input Trigger with a defined input format to show fields. +
+ ) + } + + return ( +
+ {childInputFields.map((field) => { + return ( + update(field.name, value)} + blockId={blockId} + subBlockId={subBlockId} + disabled={isPreview || disabled} + /> + ) + })} +
+ ) +} + +// Individual field component with TagDropdown support +function InputMappingField({ + fieldName, + fieldType, + value, + onChange, + blockId, + subBlockId, + disabled, +}: { + fieldName: string + fieldType?: string + value: string + onChange: (value: string) => void + blockId: string + subBlockId: string + disabled: boolean +}) { + const [showTags, setShowTags] = useState(false) + const [cursorPosition, setCursorPosition] = useState(0) + const [activeSourceBlockId, setActiveSourceBlockId] = useState(null) + const inputRef = useRef(null) + const overlayRef = useRef(null) + + const handleChange = (e: React.ChangeEvent) => { + if (disabled) { + e.preventDefault() + return + } + + const newValue = e.target.value + const newCursorPosition = e.target.selectionStart ?? 0 + + onChange(newValue) + setCursorPosition(newCursorPosition) + + // Check for tag trigger + const tagTrigger = checkTagTrigger(newValue, newCursorPosition) + setShowTags(tagTrigger.show) + } + + // Sync scroll position between input and overlay + const handleScroll = (e: React.UIEvent) => { + if (overlayRef.current) { + overlayRef.current.scrollLeft = e.currentTarget.scrollLeft + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setShowTags(false) + } + } + + const handleTagSelect = (newValue: string) => { + onChange(newValue) + // Don't emit tag selection here - onChange already updates the parent which handles the state update + // emitTagSelection was overwriting the entire inputMapping object with just a string value + } + + // Drag and Drop handlers + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + + try { + const data = JSON.parse(e.dataTransfer.getData('application/json')) + if (data.type !== 'connectionBlock') return + + // Get current cursor position or append to end + const dropPosition = inputRef.current?.selectionStart ?? value.length ?? 0 + + // Insert '<' at drop position to trigger the dropdown + const newValue = `${value.slice(0, dropPosition)}<${value.slice(dropPosition)}` + + // Focus the input first + inputRef.current?.focus() + + // Update all state in a single batch + Promise.resolve().then(() => { + onChange(newValue) + setCursorPosition(dropPosition + 1) + setShowTags(true) + + // Pass the source block ID from the dropped connection + if (data.connectionData?.sourceBlockId) { + setActiveSourceBlockId(data.connectionData.sourceBlockId) + } + + // Set cursor position after state updates + setTimeout(() => { + if (inputRef.current) { + inputRef.current.selectionStart = dropPosition + 1 + inputRef.current.selectionEnd = dropPosition + 1 + } + }, 0) + }) + } catch (error) { + console.error('Failed to parse drop data:', error) + } + } + + return ( +
+ +
+ { + setShowTags(false) + }} + onBlur={() => { + setShowTags(false) + }} + onDrop={handleDrop} + onDragOver={handleDragOver} + onScroll={handleScroll} + onKeyDown={handleKeyDown} + autoComplete='off' + style={{ overflowX: 'auto' }} + disabled={disabled} + /> +
+
+ {formatDisplayText(value, true)} +
+
+ + { + setShowTags(false) + setActiveSourceBlockId(null) + }} + /> +
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx index c8a49a5d89..4511803a25 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/sub-block.tsx @@ -18,6 +18,7 @@ import { FileUpload, FolderSelectorInput, InputFormat, + InputMapping, KnowledgeBaseSelector, LongInput, McpDynamicArgs, @@ -450,6 +451,17 @@ export function SubBlock({ /> ) } + case 'input-mapping': { + return ( + + ) + } case 'response-format': return ( 1) { - const error = new Error('Multiple Manual Trigger blocks found. Keep only one.') - logger.error('Multiple manual triggers found') + const error = new Error('Multiple Input Trigger blocks found. Keep only one.') + logger.error('Multiple input triggers found') setIsExecuting(false) throw error } else { - const error = new Error('Manual run requires a Manual Trigger or API Trigger block') - logger.error('No manual or API triggers found for manual run') - setIsExecuting(false) - throw error + // Fallback: Check for legacy starter block + const starterBlock = Object.values(filteredStates).find((block) => block.type === 'starter') + if (starterBlock) { + // Found a legacy starter block, use it as a manual trigger + const blockEntry = Object.entries(filteredStates).find( + ([, block]) => block === starterBlock + ) + if (blockEntry) { + selectedBlockId = blockEntry[0] + selectedTrigger = starterBlock + logger.info('Using legacy starter block for manual run') + } + } + + if (!selectedBlockId || !selectedTrigger) { + const error = new Error('Manual run requires an Input Trigger or API Trigger block') + logger.error('No input or API triggers found for manual run') + setIsExecuting(false) + throw error + } } if (selectedBlockId && selectedTrigger) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 036886dea9..eacc8694ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -625,13 +625,13 @@ const WorkflowContent = React.memo(() => { return } } - // Enforce only one Manual trigger for manual run UX - if (type === 'manual_trigger') { - const existingManualTriggers = Object.values(blocks).filter( - (b) => b.type === 'manual_trigger' + // Enforce only one Input trigger (single entry point for manual run UX) + if (type === 'input_trigger') { + const existingInputTriggers = Object.values(blocks).filter( + (b) => b.type === 'input_trigger' ) - if (existingManualTriggers.length >= 1) { - logger.warn('Only one Manual trigger is recommended; manual run uses a single trigger') + if (existingInputTriggers.length >= 1) { + logger.warn('Only one Input trigger is recommended; manual run uses a single trigger') return } } diff --git a/apps/sim/blocks/blocks/input_trigger.ts b/apps/sim/blocks/blocks/input_trigger.ts new file mode 100644 index 0000000000..b30e06c08a --- /dev/null +++ b/apps/sim/blocks/blocks/input_trigger.ts @@ -0,0 +1,37 @@ +import type { SVGProps } from 'react' +import { createElement } from 'react' +import { Play } from 'lucide-react' +import type { BlockConfig } from '@/blocks/types' + +const InputTriggerIcon = (props: SVGProps) => createElement(Play, props) + +export const InputTriggerBlock: BlockConfig = { + type: 'input_trigger', + name: 'Input Trigger', + description: 'Start workflow manually with a defined input schema', + longDescription: + 'Manually trigger the workflow from the editor with a structured input schema. This enables typed inputs for parent workflows to map into.', + category: 'triggers', + bgColor: '#3B82F6', + icon: InputTriggerIcon, + subBlocks: [ + { + id: 'inputFormat', + title: 'Input Format', + type: 'input-format', + layout: 'full', + description: 'Define the JSON input schema for this workflow when run manually.', + }, + ], + tools: { + access: [], + }, + inputs: {}, + outputs: { + // Dynamic outputs will be derived from inputFormat + }, + triggers: { + enabled: true, + available: ['manual'], + }, +} diff --git a/apps/sim/blocks/blocks/manual_trigger.ts b/apps/sim/blocks/blocks/manual_trigger.ts deleted file mode 100644 index 3fd3d33e5d..0000000000 --- a/apps/sim/blocks/blocks/manual_trigger.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { SVGProps } from 'react' -import { createElement } from 'react' -import { Play } from 'lucide-react' -import type { BlockConfig } from '@/blocks/types' - -const ManualTriggerIcon = (props: SVGProps) => createElement(Play, props) - -export const ManualTriggerBlock: BlockConfig = { - type: 'manual_trigger', - name: 'Manual Trigger', - description: 'Run workflow manually from the editor', - longDescription: 'Manual trigger to start the workflow during test runs.', - category: 'triggers', - bgColor: '#3B82F6', - icon: ManualTriggerIcon, - subBlocks: [], - tools: { - access: [], - }, - inputs: {}, - outputs: {}, - triggers: { - enabled: true, - available: ['manual'], - }, -} diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index 69eda686af..734913dbb1 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -80,4 +80,5 @@ export const WorkflowBlock: BlockConfig = { result: { type: 'json', description: 'Workflow execution result' }, error: { type: 'string', description: 'Error message' }, }, + hideFromToolbar: true, } diff --git a/apps/sim/blocks/blocks/workflow_input.ts b/apps/sim/blocks/blocks/workflow_input.ts new file mode 100644 index 0000000000..acf1f05bfd --- /dev/null +++ b/apps/sim/blocks/blocks/workflow_input.ts @@ -0,0 +1,58 @@ +import { WorkflowIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +// Helper: list workflows excluding self +const getAvailableWorkflows = (): Array<{ label: string; id: string }> => { + try { + const { workflows, activeWorkflowId } = useWorkflowRegistry.getState() + return Object.entries(workflows) + .filter(([id]) => id !== activeWorkflowId) + .map(([id, w]) => ({ label: w.name || `Workflow ${id.slice(0, 8)}`, id })) + .sort((a, b) => a.label.localeCompare(b.label)) + } catch { + return [] + } +} + +// New workflow block variant that visualizes child Input Trigger schema for mapping +export const WorkflowInputBlock: BlockConfig = { + type: 'workflow_input', + name: 'Workflow', + description: 'Execute another workflow and map variables to its Input Trigger schema.', + category: 'blocks', + bgColor: '#705335', + icon: WorkflowIcon, + subBlocks: [ + { + id: 'workflowId', + title: 'Select Workflow', + type: 'dropdown', + options: getAvailableWorkflows, + required: true, + }, + // Renders dynamic mapping UI based on selected child workflow's Input Trigger inputFormat + { + id: 'inputMapping', + title: 'Input Mapping', + type: 'input-mapping', + layout: 'full', + description: + "Map fields defined in the child workflow's Input Trigger to variables/values in this workflow.", + dependsOn: ['workflowId'], + }, + ], + tools: { + access: ['workflow_executor'], + }, + inputs: { + workflowId: { type: 'string', description: 'ID of the child workflow' }, + inputMapping: { type: 'json', description: 'Mapping of input fields to values' }, + }, + outputs: { + success: { type: 'boolean', description: 'Execution success status' }, + childWorkflowName: { type: 'string', description: 'Child workflow name' }, + result: { type: 'json', description: 'Workflow execution result' }, + error: { type: 'string', description: 'Error message' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 7029bee3ed..dcecbab0a3 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -31,12 +31,12 @@ import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets' import { HuggingFaceBlock } from '@/blocks/blocks/huggingface' import { HunterBlock } from '@/blocks/blocks/hunter' import { ImageGeneratorBlock } from '@/blocks/blocks/image_generator' +import { InputTriggerBlock } from '@/blocks/blocks/input_trigger' import { JinaBlock } from '@/blocks/blocks/jina' import { JiraBlock } from '@/blocks/blocks/jira' import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LinearBlock } from '@/blocks/blocks/linear' import { LinkupBlock } from '@/blocks/blocks/linkup' -import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger' import { McpBlock } from '@/blocks/blocks/mcp' import { Mem0Block } from '@/blocks/blocks/mem0' import { MemoryBlock } from '@/blocks/blocks/memory' @@ -79,6 +79,7 @@ import { WebhookBlock } from '@/blocks/blocks/webhook' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WikipediaBlock } from '@/blocks/blocks/wikipedia' import { WorkflowBlock } from '@/blocks/blocks/workflow' +import { WorkflowInputBlock } from '@/blocks/blocks/workflow_input' import { XBlock } from '@/blocks/blocks/x' import { YouTubeBlock } from '@/blocks/blocks/youtube' import type { BlockConfig } from '@/blocks/types' @@ -145,7 +146,7 @@ export const registry: Record = { stagehand_agent: StagehandAgentBlock, slack: SlackBlock, starter: StarterBlock, - manual_trigger: ManualTriggerBlock, + input_trigger: InputTriggerBlock, chat_trigger: ChatTriggerBlock, api_trigger: ApiTriggerBlock, supabase: SupabaseBlock, @@ -161,6 +162,7 @@ export const registry: Record = { whatsapp: WhatsAppBlock, wikipedia: WikipediaBlock, workflow: WorkflowBlock, + workflow_input: WorkflowInputBlock, x: XBlock, youtube: YouTubeBlock, } diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 55158b6d89..2108e78bf4 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -54,6 +54,7 @@ export type SubBlockType = | 'input-format' // Input structure format | 'response-format' // Response structure format | 'file-upload' // File uploader + | 'input-mapping' // Map parent variables to child workflow input schema export type SubBlockLayout = 'full' | 'half' diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index fc3603417d..d4c8e311bf 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -22,8 +22,20 @@ const MAX_WORKFLOW_DEPTH = 10 export class WorkflowBlockHandler implements BlockHandler { private serializer = new Serializer() + // Tolerant JSON parser for mapping values + // Keeps handler self-contained without introducing utilities + private safeParse(input: unknown): unknown { + if (typeof input !== 'string') return input + try { + return JSON.parse(input) + } catch { + return input + } + } + canHandle(block: SerializedBlock): boolean { - return block.metadata?.id === BlockType.WORKFLOW + const id = block.metadata?.id + return id === BlockType.WORKFLOW || id === 'workflow_input' } async execute( @@ -63,13 +75,22 @@ export class WorkflowBlockHandler implements BlockHandler { ) // Prepare the input for the child workflow - // The input from this block should be passed as start.input to the child workflow - let childWorkflowInput = {} - - if (inputs.input !== undefined) { - // If input is provided, use it directly + // Prefer structured mapping if provided; otherwise fall back to legacy 'input' passthrough + let childWorkflowInput: Record = {} + + if (inputs.inputMapping !== undefined && inputs.inputMapping !== null) { + // Handle inputMapping - could be object or stringified JSON + const raw = inputs.inputMapping + const normalized = this.safeParse(raw) + + if (normalized && typeof normalized === 'object' && !Array.isArray(normalized)) { + childWorkflowInput = normalized as Record + } else { + childWorkflowInput = {} + } + } else if (inputs.input !== undefined) { + // Legacy behavior: pass under start.input childWorkflowInput = inputs.input - logger.info(`Passing input to child workflow: ${JSON.stringify(childWorkflowInput)}`) } // Remove the workflowId from the input to avoid confusion @@ -308,10 +329,11 @@ export class WorkflowBlockHandler implements BlockHandler { } return failure as Record } - let result = childResult - if (childResult?.output) { - result = childResult.output - } + + // childResult is an ExecutionResult with structure { success, output, metadata, logs } + // We want the actual output from the execution + const result = childResult.output || {} + return { success: true, childWorkflowName, diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index ab94e27089..5fe05510d3 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -766,10 +766,34 @@ export class Executor { // Starting from a specific block (webhook trigger, schedule trigger, or new trigger blocks) initBlock = this.actualWorkflow.blocks.find((block) => block.id === startBlockId) } else { - // Default to starter block (legacy) + // Default to starter block (legacy) or find any trigger block initBlock = this.actualWorkflow.blocks.find( (block) => block.metadata?.id === BlockType.STARTER ) + + // If no starter block, look for appropriate trigger block based on context + if (!initBlock) { + if (this.isChildExecution) { + // Child workflows only use Input Trigger blocks + const inputTriggerBlocks = this.actualWorkflow.blocks.filter( + (block) => block.metadata?.id === 'input_trigger' + ) + if (inputTriggerBlocks.length > 0) { + initBlock = inputTriggerBlocks[0] + } + } else { + // Parent workflows can use any trigger block + const triggerBlocks = this.actualWorkflow.blocks.filter( + (block) => + block.metadata?.id === 'input_trigger' || + block.metadata?.id === 'api_trigger' || + block.metadata?.id === 'chat_trigger' + ) + if (triggerBlocks.length > 0) { + initBlock = triggerBlocks[0] + } + } + } } if (initBlock) { @@ -852,9 +876,21 @@ export class Executor { // Initialize the starting block with structured input let blockOutput: any - // For API triggers, fields should be at root level without 'input' field - if (initBlock.metadata?.id === 'api_trigger') { - blockOutput = { ...finalInput } + // For API/Input triggers, normalize primitives and mirror objects under input + if ( + initBlock.metadata?.id === 'api_trigger' || + initBlock.metadata?.id === 'input_trigger' + ) { + const isObject = + finalInput !== null && typeof finalInput === 'object' && !Array.isArray(finalInput) + if (isObject) { + blockOutput = { ...finalInput } + // Provide a mirrored input object for universal references + blockOutput.input = { ...finalInput } + } else { + // Primitive input: only expose under input + blockOutput = { input: finalInput } + } } else { // For legacy starter blocks, keep the old behavior blockOutput = { @@ -893,12 +929,27 @@ export class Executor { if (this.workflowInput?.files && Array.isArray(this.workflowInput.files)) { starterOutput.files = this.workflowInput.files } - } else if (initBlock.metadata?.id === 'manual_trigger') { - // Manual trigger: no outputs - starterOutput = {} - } else if (initBlock.metadata?.id === 'api_trigger') { - // API trigger without inputFormat: spread the raw input directly - starterOutput = { ...(this.workflowInput || {}) } + } else if ( + initBlock.metadata?.id === 'api_trigger' || + initBlock.metadata?.id === 'input_trigger' + ) { + // API/Input trigger without inputFormat: normalize primitives and mirror objects under input + const rawCandidate = + this.workflowInput?.input !== undefined + ? this.workflowInput.input + : this.workflowInput + const isObject = + rawCandidate !== null && + typeof rawCandidate === 'object' && + !Array.isArray(rawCandidate) + if (isObject) { + starterOutput = { + ...(rawCandidate as Record), + input: { ...(rawCandidate as Record) }, + } + } else { + starterOutput = { input: rawCandidate } + } } else { // Legacy starter block handling if (this.workflowInput && typeof this.workflowInput === 'object') { diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index 257efa151b..01b0ae3e98 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -526,7 +526,7 @@ export class InputResolver { start: 'starter', api: 'api_trigger', chat: 'chat_trigger', - manual: 'manual_trigger', + manual: 'input_trigger', } const triggerType = triggerTypeMap[blockRefLower] diff --git a/apps/sim/lib/workflows/block-outputs.ts b/apps/sim/lib/workflows/block-outputs.ts index 2ef2fff71a..f7184c4a71 100644 --- a/apps/sim/lib/workflows/block-outputs.ts +++ b/apps/sim/lib/workflows/block-outputs.ts @@ -55,8 +55,8 @@ export function getBlockOutputs( const inputFormatValue = subBlocks.inputFormat.value if (Array.isArray(inputFormatValue)) { - // For API trigger, only use inputFormat fields - if (blockType === 'api_trigger') { + // For API and Input triggers, only use inputFormat fields + if (blockType === 'api_trigger' || blockType === 'input_trigger') { outputs = {} // Clear all default outputs // Add each field from inputFormat as an output at root level @@ -69,8 +69,8 @@ export function getBlockOutputs( } }) } - } else if (blockType === 'api_trigger') { - // If no inputFormat defined, API trigger has no outputs + } else if (blockType === 'api_trigger' || blockType === 'input_trigger') { + // If no inputFormat defined, API/Input trigger has no outputs outputs = {} } } diff --git a/apps/sim/lib/workflows/triggers.ts b/apps/sim/lib/workflows/triggers.ts index a62bdee1ef..ead2fc09f9 100644 --- a/apps/sim/lib/workflows/triggers.ts +++ b/apps/sim/lib/workflows/triggers.ts @@ -4,7 +4,7 @@ import { getBlock } from '@/blocks' * Unified trigger type definitions */ export const TRIGGER_TYPES = { - MANUAL: 'manual_trigger', + INPUT: 'input_trigger', CHAT: 'chat_trigger', API: 'api_trigger', WEBHOOK: 'webhook', @@ -46,7 +46,7 @@ export class TriggerUtils { */ static isAnyTriggerType(type: string): boolean { return ( - type === TRIGGER_TYPES.MANUAL || + type === TRIGGER_TYPES.INPUT || type === TRIGGER_TYPES.CHAT || type === TRIGGER_TYPES.API || type === TRIGGER_TYPES.WEBHOOK || @@ -74,13 +74,15 @@ export class TriggerUtils { * Check if a block is a manual-compatible trigger */ static isManualTrigger(block: { type: string; subBlocks?: any }): boolean { - if (block.type === TRIGGER_TYPES.MANUAL) { + if (block.type === TRIGGER_TYPES.INPUT) { return true } - // Legacy: starter block in manual mode + // Legacy: starter block in manual mode or without explicit mode (default to manual) if (block.type === TRIGGER_TYPES.STARTER) { - return block.subBlocks?.startWorkflow?.value === 'manual' + // If startWorkflow is not set or is set to 'manual', treat as manual trigger + const startWorkflowValue = block.subBlocks?.startWorkflow?.value + return startWorkflowValue === 'manual' || startWorkflowValue === undefined } return false @@ -88,8 +90,15 @@ export class TriggerUtils { /** * Check if a block is an API-compatible trigger + * @param block - Block to check + * @param isChildWorkflow - Whether this is being called from a child workflow context */ - static isApiTrigger(block: { type: string; subBlocks?: any }): boolean { + static isApiTrigger(block: { type: string; subBlocks?: any }, isChildWorkflow = false): boolean { + if (isChildWorkflow) { + // Child workflows (workflow-in-workflow) only work with input_trigger + return block.type === TRIGGER_TYPES.INPUT + } + // Direct API calls only work with api_trigger if (block.type === TRIGGER_TYPES.API) { return true } @@ -121,8 +130,8 @@ export class TriggerUtils { switch (triggerType) { case TRIGGER_TYPES.CHAT: return 'Chat' - case TRIGGER_TYPES.MANUAL: - return 'Manual' + case TRIGGER_TYPES.INPUT: + return 'Input Trigger' case TRIGGER_TYPES.API: return 'API' case TRIGGER_TYPES.WEBHOOK: @@ -139,7 +148,8 @@ export class TriggerUtils { */ static findTriggersByType( blocks: T[] | Record, - triggerType: 'chat' | 'manual' | 'api' + triggerType: 'chat' | 'manual' | 'api', + isChildWorkflow = false ): T[] { const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks) @@ -149,7 +159,7 @@ export class TriggerUtils { case 'manual': return blockArray.filter((block) => TriggerUtils.isManualTrigger(block)) case 'api': - return blockArray.filter((block) => TriggerUtils.isApiTrigger(block)) + return blockArray.filter((block) => TriggerUtils.isApiTrigger(block, isChildWorkflow)) default: return [] } @@ -160,12 +170,13 @@ export class TriggerUtils { */ static findStartBlock( blocks: Record, - executionType: 'chat' | 'manual' | 'api' + executionType: 'chat' | 'manual' | 'api', + isChildWorkflow = false ): { blockId: string; block: T } | null { const entries = Object.entries(blocks) // Look for new trigger blocks first - const triggers = TriggerUtils.findTriggersByType(blocks, executionType) + const triggers = TriggerUtils.findTriggersByType(blocks, executionType, isChildWorkflow) if (triggers.length > 0) { const blockId = entries.find(([, b]) => b === triggers[0])?.[0] if (blockId) { @@ -200,7 +211,7 @@ export class TriggerUtils { static requiresSingleInstance(triggerType: string): boolean { return ( triggerType === TRIGGER_TYPES.API || - triggerType === TRIGGER_TYPES.MANUAL || + triggerType === TRIGGER_TYPES.INPUT || triggerType === TRIGGER_TYPES.CHAT ) } From 68cb52eb3523ca99ddf24e817924a8ff827f033f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 17:35:24 -0700 Subject: [PATCH 13/43] add better workflow ux --- .../input-mapping/input-mapping.tsx | 111 ++++++++---------- apps/sim/blocks/blocks/workflow_input.ts | 2 +- 2 files changed, 51 insertions(+), 62 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx index 014df270f2..43a8151596 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx @@ -98,22 +98,54 @@ export function InputMapping({ if (!selectedWorkflowId) { return ( -
- Select a workflow first. +
+ + + +

No workflow selected

+

+ Select a workflow above to configure inputs +

) } if (!childInputFields || childInputFields.length === 0) { return ( -
- The selected workflow must have an Input Trigger with a defined input format to show fields. +
+ + + +

No input fields defined

+

+ The selected workflow needs an Input Trigger with defined fields +

) } return ( -
+
{childInputFields.map((field) => { return ( (null) const inputRef = useRef(null) const overlayRef = useRef(null) @@ -188,63 +219,24 @@ function InputMappingField({ const handleTagSelect = (newValue: string) => { onChange(newValue) - // Don't emit tag selection here - onChange already updates the parent which handles the state update - // emitTagSelection was overwriting the entire inputMapping object with just a string value - } - - // Drag and Drop handlers - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - } - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault() - - try { - const data = JSON.parse(e.dataTransfer.getData('application/json')) - if (data.type !== 'connectionBlock') return - - // Get current cursor position or append to end - const dropPosition = inputRef.current?.selectionStart ?? value.length ?? 0 - - // Insert '<' at drop position to trigger the dropdown - const newValue = `${value.slice(0, dropPosition)}<${value.slice(dropPosition)}` - - // Focus the input first - inputRef.current?.focus() - - // Update all state in a single batch - Promise.resolve().then(() => { - onChange(newValue) - setCursorPosition(dropPosition + 1) - setShowTags(true) - - // Pass the source block ID from the dropped connection - if (data.connectionData?.sourceBlockId) { - setActiveSourceBlockId(data.connectionData.sourceBlockId) - } - - // Set cursor position after state updates - setTimeout(() => { - if (inputRef.current) { - inputRef.current.selectionStart = dropPosition + 1 - inputRef.current.selectionEnd = dropPosition + 1 - } - }, 0) - }) - } catch (error) { - console.error('Failed to parse drop data:', error) - } } return ( -
- -
+
+
+ + {fieldType && ( + + {fieldType} + + )} +
+
{ setShowTags(false) }} - onDrop={handleDrop} - onDragOver={handleDragOver} onScroll={handleScroll} onKeyDown={handleKeyDown} autoComplete='off' @@ -280,12 +270,11 @@ function InputMappingField({ visible={showTags} onSelect={handleTagSelect} blockId={blockId} - activeSourceBlockId={activeSourceBlockId} + activeSourceBlockId={null} inputValue={value} cursorPosition={cursorPosition} onClose={() => { setShowTags(false) - setActiveSourceBlockId(null) }} />
diff --git a/apps/sim/blocks/blocks/workflow_input.ts b/apps/sim/blocks/blocks/workflow_input.ts index acf1f05bfd..a8c9d313cf 100644 --- a/apps/sim/blocks/blocks/workflow_input.ts +++ b/apps/sim/blocks/blocks/workflow_input.ts @@ -21,7 +21,7 @@ export const WorkflowInputBlock: BlockConfig = { name: 'Workflow', description: 'Execute another workflow and map variables to its Input Trigger schema.', category: 'blocks', - bgColor: '#705335', + bgColor: '#6366F1', // Indigo - modern and professional icon: WorkflowIcon, subBlocks: [ { From dde230a9364b4dd648229be3b0c59cabff52a672 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 18:11:13 -0700 Subject: [PATCH 14/43] progress --- apps/sim/app/api/workspaces/route.ts | 53 ++++------------- .../[workspaceId]/w/[workflowId]/workflow.tsx | 6 +- apps/sim/lib/workflows/triggers.ts | 58 +++++++++++++++++-- 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index ddb4246e74..0fc285eca4 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -110,8 +110,8 @@ async function createWorkspace(userId: string, name: string) { updatedAt: now, }) - // Create initial workflow for the workspace with start block - const starterId = crypto.randomUUID() + // Create initial workflow for the workspace with Input Trigger + const triggerId = crypto.randomUUID() // Create the workflow await tx.insert(workflow).values({ @@ -133,12 +133,12 @@ async function createWorkspace(userId: string, name: string) { marketplaceData: null, }) - // Insert the start block into workflow_blocks table + // Insert the Input Trigger block into workflow_blocks table await tx.insert(workflowBlocks).values({ - id: starterId, + id: triggerId, workflowId: workflowId, - type: 'starter', - name: 'Start', + type: 'input_trigger', + name: 'Input Trigger', positionX: '100', positionY: '100', enabled: true, @@ -147,44 +147,13 @@ async function createWorkspace(userId: string, name: string) { advancedMode: false, height: '95', subBlocks: { - startWorkflow: { - id: 'startWorkflow', - type: 'dropdown', - value: 'manual', - }, - webhookPath: { - id: 'webhookPath', - type: 'short-input', - value: '', - }, - webhookSecret: { - id: 'webhookSecret', - type: 'short-input', - value: '', - }, - scheduleType: { - id: 'scheduleType', - type: 'dropdown', - value: 'daily', - }, - minutesInterval: { - id: 'minutesInterval', - type: 'short-input', - value: '', - }, - minutesStartingAt: { - id: 'minutesStartingAt', - type: 'short-input', - value: '', - }, - }, - outputs: { - response: { - type: { - input: 'any', - }, + inputFormat: { + id: 'inputFormat', + type: 'input-format', + value: [], }, }, + outputs: {}, createdAt: now, updatedAt: now, }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index eacc8694ae..70660db1fd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -510,7 +510,7 @@ const WorkflowContent = React.memo(() => { open: true, triggerName: 'new trigger', message: - 'Cannot add new trigger blocks when a legacy Start block exists. Please remove the Start block first.', + 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.', }) } else { const triggerName = TriggerUtils.getDefaultTriggerName(type) || 'trigger' @@ -715,7 +715,7 @@ const WorkflowContent = React.memo(() => { open: true, triggerName: 'new trigger', message: - 'Cannot add new trigger blocks when a legacy Start block exists. Please remove the Start block first.', + 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.', }) } else { const triggerName = TriggerUtils.getDefaultTriggerName(data.type) || 'trigger' @@ -918,7 +918,7 @@ const WorkflowContent = React.memo(() => { open: true, triggerName: 'new trigger', message: - 'Cannot add new trigger blocks when a legacy Start block exists. Please remove the Start block first.', + 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.', }) } else { const triggerName = TriggerUtils.getDefaultTriggerName(data.type) || data.type diff --git a/apps/sim/lib/workflows/triggers.ts b/apps/sim/lib/workflows/triggers.ts index ead2fc09f9..a441fc38c6 100644 --- a/apps/sim/lib/workflows/triggers.ts +++ b/apps/sim/lib/workflows/triggers.ts @@ -209,6 +209,9 @@ export class TriggerUtils { * Check if a trigger type requires single instance constraint */ static requiresSingleInstance(triggerType: string): boolean { + // API and Input triggers cannot coexist with each other + // Chat trigger must be unique + // Schedules and webhooks can coexist with API/Input triggers return ( triggerType === TRIGGER_TYPES.API || triggerType === TRIGGER_TYPES.INPUT || @@ -232,13 +235,60 @@ export class TriggerUtils { triggerType: string ): boolean { const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks) + const hasLegacyStarter = TriggerUtils.hasLegacyStarter(blocks) + + // Legacy starter block can't coexist with Chat, Input, or API triggers + if (hasLegacyStarter) { + if ( + triggerType === TRIGGER_TYPES.CHAT || + triggerType === TRIGGER_TYPES.INPUT || + triggerType === TRIGGER_TYPES.API + ) { + return true + } + // Legacy starter CAN coexist with schedules and webhooks + } - // Can't add new triggers if legacy starter block exists - if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(triggerType)) { - return true + // Can't add legacy starter if Chat, Input, or API triggers exist + if (triggerType === TRIGGER_TYPES.STARTER) { + const hasModernTriggers = blockArray.some( + (block) => + block.type === TRIGGER_TYPES.CHAT || + block.type === TRIGGER_TYPES.INPUT || + block.type === TRIGGER_TYPES.API + ) + if (hasModernTriggers) { + return true + } + } + + // Multiple schedules are allowed + // Schedules can coexist with anything (except the constraint above with legacy starter) + if (triggerType === TRIGGER_TYPES.SCHEDULE) { + return false // Always allow schedules + } + + // Webhooks can coexist with other triggers (multiple webhooks allowed) + if (triggerType === TRIGGER_TYPES.WEBHOOK) { + return false // Always allow webhooks + } + + // Only one Input trigger allowed + if (triggerType === TRIGGER_TYPES.INPUT) { + return blockArray.some((block) => block.type === TRIGGER_TYPES.INPUT) + } + + // Only one API trigger allowed + if (triggerType === TRIGGER_TYPES.API) { + return blockArray.some((block) => block.type === TRIGGER_TYPES.API) + } + + // Chat trigger must be unique + if (triggerType === TRIGGER_TYPES.CHAT) { + return blockArray.some((block) => block.type === TRIGGER_TYPES.CHAT) } - // Check single-instance rules + // For other trigger types, check single-instance rules if (!TriggerUtils.requiresSingleInstance(triggerType)) { return false } From ce61391c24161b550ccbb639251a5b62d71a2606 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 18:12:50 -0700 Subject: [PATCH 15/43] highlighting works --- .../components/trigger-selector/trigger-placeholder.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx index 229beaf339..1cd39b4f2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx @@ -24,7 +24,7 @@ export function TriggerPlaceholder({ onClick, className }: TriggerPlaceholderPro 'border-2 border-dashed border-muted-foreground/30', 'bg-background/50 backdrop-blur-sm', 'transition-all duration-300 ease-out', - 'hover:border-primary/50 hover:bg-background/80', + 'hover:border-foreground/50 hover:bg-background/80', 'hover:shadow-lg hover:scale-[1.02]', 'active:scale-[0.98]', 'cursor-pointer' @@ -32,8 +32,8 @@ export function TriggerPlaceholder({ onClick, className }: TriggerPlaceholderPro >
- - + +

From fc40a7b35fe8f9b45c672872dd514ace585b9dc1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 18:32:52 -0700 Subject: [PATCH 16/43] trigger card --- .../app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 70660db1fd..5687cd6ca5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -1854,8 +1854,8 @@ const WorkflowContent = React.memo(() => { message={triggerWarning.message} /> - {/* Trigger selector for empty workflows */} - {isWorkflowEmpty && effectivePermissions.canEdit && ( + {/* Trigger selector for empty workflows - only show after workflow has loaded */} + {isWorkflowReady && isWorkflowEmpty && effectivePermissions.canEdit && ( setShowTriggerSelector(true)} /> )} From 17ef831feb3f891692fe97750151b05f13e7b5bb Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 18:40:37 -0700 Subject: [PATCH 17/43] default agent workflow change --- apps/sim/app/api/workspaces/route.ts | 31 +++------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index 0fc285eca4..434032fdbb 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -3,7 +3,7 @@ import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' -import { permissions, workflow, workflowBlocks, workspace } from '@/db/schema' +import { permissions, workflow, workspace } from '@/db/schema' const logger = createLogger('Workspaces') @@ -110,9 +110,7 @@ async function createWorkspace(userId: string, name: string) { updatedAt: now, }) - // Create initial workflow for the workspace with Input Trigger - const triggerId = crypto.randomUUID() - + // Create initial workflow for the workspace (empty canvas) // Create the workflow await tx.insert(workflow).values({ id: workflowId, @@ -133,30 +131,7 @@ async function createWorkspace(userId: string, name: string) { marketplaceData: null, }) - // Insert the Input Trigger block into workflow_blocks table - await tx.insert(workflowBlocks).values({ - id: triggerId, - workflowId: workflowId, - type: 'input_trigger', - name: 'Input Trigger', - positionX: '100', - positionY: '100', - enabled: true, - horizontalHandles: true, - isWide: false, - advancedMode: false, - height: '95', - subBlocks: { - inputFormat: { - id: 'inputFormat', - type: 'input-format', - value: [], - }, - }, - outputs: {}, - createdAt: now, - updatedAt: now, - }) + // No blocks are inserted - empty canvas logger.info( `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` From 18459fe6d1788b566b5571b30b91ef8dcf00e18e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 18:44:29 -0700 Subject: [PATCH 18/43] fix build error --- .../copilot/tools/server/blocks/get-blocks-metadata-tool.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index b685472c75..a1ac05aa2d 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -301,7 +301,9 @@ function computeBlockLevelInputs(blockConfig: BlockConfig): Record return blockInputs } -function computeOperationLevelInputs(blockConfig: BlockConfig): Record> { +function computeOperationLevelInputs( + blockConfig: BlockConfig +): Record> { const inputs = blockConfig.inputs || {} const subBlocks: any[] = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : [] From b5eaa85db078c6cdb40a43d02f7a3d0048df688e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 18:47:22 -0700 Subject: [PATCH 19/43] remove any casts --- .../server/blocks/get-blocks-metadata-tool.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index a1ac05aa2d..a91189de46 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -91,7 +91,7 @@ export const getBlocksMetadataServerTool: BaseServerTool< } ;(metadata as any).subBlocks = undefined } else { - const blockConfig: BlockConfig | undefined = (blockRegistry as any)[blockId] + const blockConfig: BlockConfig | undefined = blockRegistry[blockId] if (!blockConfig) { logger.debug('Block not found in registry', { blockId }) continue @@ -103,7 +103,7 @@ export const getBlocksMetadataServerTool: BaseServerTool< } const tools: CopilotToolMetadata[] = Array.isArray(blockConfig.tools?.access) ? blockConfig.tools!.access.map((toolId) => { - const tool: any = (toolsRegistry as any)[toolId] + const tool = toolsRegistry[toolId] if (!tool) return { id: toolId, name: toolId } return { id: toolId, @@ -136,7 +136,7 @@ export const getBlocksMetadataServerTool: BaseServerTool< const operations: Record = {} for (const opId of operationIds) { const resolvedToolId = resolveToolIdForOperation(blockConfig, opId) - const toolCfg: any = resolvedToolId ? (toolsRegistry as any)[resolvedToolId] : undefined + const toolCfg = resolvedToolId ? toolsRegistry[resolvedToolId] : undefined const toolParams: Record = toolCfg?.params || {} const toolOutputs: Record = toolCfg?.outputs || {} const filteredToolParams: Record = {} @@ -305,18 +305,18 @@ function computeOperationLevelInputs( blockConfig: BlockConfig ): Record> { const inputs = blockConfig.inputs || {} - const subBlocks: any[] = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : [] + const subBlocks = Array.isArray(blockConfig.subBlocks) ? blockConfig.subBlocks : [] const opInputs: Record> = {} // Map subblocks to inputs keys via id or canonicalParamId and collect by operation for (const sb of subBlocks) { - const cond = normalizeCondition((sb as any).condition) + const cond = normalizeCondition(sb.condition) if (!cond || cond.field !== 'operation' || cond.not) continue const keys: string[] = [] - if ((sb as any).canonicalParamId) keys.push((sb as any).canonicalParamId) - if ((sb as any).id) keys.push((sb as any).id) - const values: any[] = Array.isArray(cond.value) ? cond.value : [cond.value] + if (sb.canonicalParamId) keys.push(sb.canonicalParamId) + if (sb.id) keys.push(sb.id) + const values = Array.isArray(cond.value) ? cond.value : [cond.value] for (const key of keys) { if (!(key in inputs)) continue for (const v of values) { @@ -335,9 +335,9 @@ function resolveOperationIds( operationParameters: Record ): string[] { // Prefer explicit operation subblock options if present - const opBlock: any = (blockConfig.subBlocks || []).find((sb: any) => sb.id === 'operation') + const opBlock = (blockConfig.subBlocks || []).find((sb) => sb.id === 'operation') if (opBlock && Array.isArray(opBlock.options)) { - const ids = opBlock.options.map((o: any) => o.id).filter(Boolean) + const ids = opBlock.options.map((o) => o.id).filter(Boolean) if (ids.length > 0) return ids } // Fallback: keys from operationParameters @@ -346,7 +346,7 @@ function resolveOperationIds( function resolveToolIdForOperation(blockConfig: BlockConfig, opId: string): string | undefined { try { - const toolSelector = (blockConfig.tools as any)?.config?.tool + const toolSelector = blockConfig.tools?.config?.tool if (typeof toolSelector === 'function') { const maybeToolId = toolSelector({ operation: opId }) if (typeof maybeToolId === 'string') return maybeToolId From 68a59c9d0a3f4c697a4b581d15fd84b612cf8b1b Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Tue, 16 Sep 2025 19:38:38 -0700 Subject: [PATCH 20/43] address greptile comments --- apps/sim/app/api/chat/utils.ts | 18 +- .../components/panel/components/chat/chat.tsx | 46 +++-- .../trigger-selector/trigger-placeholder.tsx | 18 +- .../trigger-selector-modal.tsx | 160 +++++++----------- .../components/trigger-warning-dialog.tsx | 35 +++- .../input-mapping/input-mapping.tsx | 59 +++++-- .../hooks/use-workflow-execution.ts | 31 +++- .../[workspaceId]/w/[workflowId]/workflow.tsx | 86 +++++----- .../sidebar/components/toolbar/toolbar.tsx | 8 +- apps/sim/executor/index.ts | 5 +- apps/sim/executor/resolver/resolver.ts | 11 +- apps/sim/lib/workflows/triggers.ts | 44 ++++- apps/sim/stores/workflows/yaml/importer.ts | 10 +- 13 files changed, 301 insertions(+), 230 deletions(-) diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index d6304dd517..2e12204959 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -616,7 +616,23 @@ export async function executeWorkflowForChat( // Determine the start block for chat execution const startBlock = TriggerUtils.findStartBlock(mergedStates, 'chat') - const startBlockId = startBlock?.blockId + + if (!startBlock) { + const errorMessage = + 'No Chat trigger configured for this workflow. Add a Chat Trigger block to enable chat execution.' + logger.error(`[${requestId}] ${errorMessage}`) + await loggingSession.safeCompleteWithError({ + endedAt: new Date().toISOString(), + totalDurationMs: 0, + error: { + message: errorMessage, + stackTrace: undefined, + }, + }) + throw new Error(errorMessage) + } + + const startBlockId = startBlock.blockId let result try { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx index cdad8823ad..de576b808c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/chat/chat.tsx @@ -330,6 +330,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) { if (event === 'final' && data) { const result = data as ExecutionResult + + // If final result is a failure, surface error and stop + if ('success' in result && !result.success) { + addMessage({ + content: `Error: ${result.error || 'Workflow execution failed'}`, + workflowId: activeWorkflowId, + type: 'workflow', + }) + + // Clear any existing message streams + for (const msgId of messageIdMap.values()) { + finalizeMessageStream(msgId) + } + messageIdMap.clear() + + // Stop processing + return + } + const nonStreamingLogs = result.logs?.filter((log) => !messageIdMap.has(log.blockId)) || [] @@ -343,34 +362,25 @@ export function Chat({ chatMessage, setChatMessage }: ChatProps) { const blockIdForOutput = extractBlockIdFromOutputId(outputId) const path = extractPathFromOutputId(outputId, blockIdForOutput) const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput) - if (log) { - let outputValue: any = log.output - + let output = log.output if (path) { - // Parse JSON content safely - outputValue = parseOutputContentSafely(outputValue) - + output = parseOutputContentSafely(output) const pathParts = path.split('.') + let current = output for (const part of pathParts) { - if ( - outputValue && - typeof outputValue === 'object' && - part in outputValue - ) { - outputValue = outputValue[part] + if (current && typeof current === 'object' && part in current) { + current = current[part] } else { - outputValue = undefined + current = undefined break } } + output = current } - if (outputValue !== undefined) { + if (output !== undefined) { addMessage({ - content: - typeof outputValue === 'string' - ? outputValue - : `\`\`\`json\n${JSON.stringify(outputValue, null, 2)}\n\`\`\``, + content: typeof output === 'string' ? output : JSON.stringify(output), workflowId: activeWorkflowId, type: 'workflow', }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx index 1cd39b4f2f..9051476ec8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx @@ -12,31 +12,31 @@ export function TriggerPlaceholder({ onClick, className }: TriggerPlaceholderPro return (

+ ) + } + return ( !open && onClose()}> - + - + How do you want to trigger this workflow? -

+

Choose how your workflow will be started. You can add more triggers later from the sidebar.

@@ -56,7 +98,7 @@ export function TriggerSelectorModal({ open, onClose, onSelect }: TriggerSelecto
{/* Search Input */} -
+
-
+
{/* Core Triggers Section */} {coreOptions.length > 0 && ( <> -

Core Triggers

-
- {coreOptions.map((option) => { - const Icon = option.icon - const isHovered = hoveredId === option.id - - return ( - - ) - })} +

Core Triggers

+
+ {coreOptions.map((option) => ( + + ))}
)} @@ -130,62 +129,19 @@ export function TriggerSelectorModal({ open, onClose, onSelect }: TriggerSelecto {/* Integration Triggers Section */} {integrationOptions.length > 0 && ( <> -

+

Integration Triggers

-
- {integrationOptions.map((option) => { - const Icon = option.icon - const isHovered = hoveredId === option.id - - return ( - - ) - })} +
+ {integrationOptions.map((option) => ( + + ))}
)} {filteredOptions.length === 0 && ( -
+
No triggers found matching "{searchQuery}"
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx index aa44160df0..2b8c2a55ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog.tsx @@ -8,31 +8,48 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog' +export enum TriggerWarningType { + DUPLICATE_TRIGGER = 'duplicate_trigger', + LEGACY_INCOMPATIBILITY = 'legacy_incompatibility', +} + interface TriggerWarningDialogProps { open: boolean onOpenChange: (open: boolean) => void triggerName: string - message?: string + type: TriggerWarningType } export function TriggerWarningDialog({ open, onOpenChange, triggerName, - message, + type, }: TriggerWarningDialogProps) { - const defaultMessage = `A workflow can only have one ${triggerName} trigger block. Please remove the existing one before adding a new one.` + const getTitle = () => { + switch (type) { + case TriggerWarningType.LEGACY_INCOMPATIBILITY: + return 'Cannot mix trigger types' + case TriggerWarningType.DUPLICATE_TRIGGER: + return `Only one ${triggerName} trigger allowed` + } + } + + const getDescription = () => { + switch (type) { + case TriggerWarningType.LEGACY_INCOMPATIBILITY: + return 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.' + case TriggerWarningType.DUPLICATE_TRIGGER: + return `A workflow can only have one ${triggerName} trigger block. Please remove the existing one before adding a new one.` + } + } return ( - - {message && message.includes('legacy') - ? 'Cannot mix trigger types' - : `Only one ${triggerName} trigger allowed`} - - {message || defaultMessage} + {getTitle()} + {getDescription()} onOpenChange(false)}>Got it diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx index 43a8151596..a2d1f536df 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx @@ -7,6 +7,33 @@ import { cn } from '@/lib/utils' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +interface InputFormatField { + name: string + type?: string +} + +interface InputTriggerBlock { + type: 'input_trigger' + subBlocks?: { + inputFormat?: { value?: InputFormatField[] } + } +} + +function isInputTriggerBlock(value: unknown): value is InputTriggerBlock { + return ( + !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'input_trigger' + ) +} + +function isInputFormatField(value: unknown): value is InputFormatField { + if (typeof value !== 'object' || value === null) return false + if (!('name' in value)) return false + const { name, type } = value as { name: unknown; type?: unknown } + if (typeof name !== 'string' || name.trim() === '') return false + if (type !== undefined && typeof type !== 'string') return false + return true +} + interface InputMappingProps { blockId: string subBlockId: string @@ -52,20 +79,22 @@ export function InputMapping({ return } const { data } = await res.json() - const blocks = data?.state?.blocks || {} - const triggerEntry = Object.entries(blocks).find( - ([, b]: any) => b?.type === 'input_trigger' - ) + const blocks = (data?.state?.blocks as Record) || {} + const triggerEntry = Object.entries(blocks).find(([, b]) => isInputTriggerBlock(b)) if (!triggerEntry) { if (isMounted) setChildInputFields([]) return } - const triggerBlock = triggerEntry[1] as any - const inputFormat = triggerBlock?.subBlocks?.inputFormat?.value + const triggerBlock = triggerEntry[1] + if (!isInputTriggerBlock(triggerBlock)) { + if (isMounted) setChildInputFields([]) + return + } + const inputFormat = triggerBlock.subBlocks?.inputFormat?.value if (Array.isArray(inputFormat)) { - const fields = inputFormat - .filter((f: any) => f && typeof f.name === 'string' && f.name.trim() !== '') - .map((f: any) => ({ name: f.name as string, type: f.type as string | undefined })) + const fields = (inputFormat as unknown[]) + .filter(isInputFormatField) + .map((f) => ({ name: f.name, type: f.type })) if (isMounted) setChildInputFields(fields) } else { if (isMounted) setChildInputFields([]) @@ -112,8 +141,8 @@ export function InputMapping({ d='M13 10V3L4 14h7v7l9-11h-7z' /> -

No workflow selected

-

+

No workflow selected

+

Select a workflow above to configure inputs

@@ -136,8 +165,8 @@ export function InputMapping({ d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' /> -

No input fields defined

-

+

No input fields defined

+

The selected workflow needs an Input Trigger with defined fields

@@ -224,9 +253,9 @@ function InputMappingField({ return (
- + {fieldType && ( - + {fieldType} )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index d6a45ad326..e5ca24009d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -447,7 +447,30 @@ export function useWorkflowExecution() { ) } } catch (error: any) { - controller.error(error) + // Create a proper error result for logging + const errorResult = { + success: false, + error: error.message || 'Workflow execution failed', + output: {}, + logs: [], + metadata: { + duration: 0, + startTime: new Date().toISOString(), + source: 'chat' as const, + }, + } + + // Send the error as final event so downstream handlers can treat it uniformly + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ event: 'final', data: errorResult })}\n\n`) + ) + + // Persist the error to logs so it shows up in the logs page + persistLogs(executionId, errorResult).catch((err) => + logger.error('Error persisting error logs:', err) + ) + + // Do not error the controller to allow consumers to process the final event } finally { controller.close() setIsExecuting(false) @@ -661,12 +684,6 @@ export function useWorkflowExecution() { } startBlockId = startBlock.blockId - - // Check if the chat trigger has any outgoing connections - const outgoingConnections = workflowEdges.filter((edge) => edge.source === startBlockId) - if (outgoingConnections.length === 0) { - throw new Error('Chat Trigger block must be connected to other blocks to execute') - } } else { // For manual editor runs: look for Manual trigger OR API trigger const entries = Object.entries(filteredStates) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 5687cd6ca5..797b950ac0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -22,7 +22,10 @@ import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/p import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' import { TriggerPlaceholder } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder' import { TriggerSelectorModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal' -import { TriggerWarningDialog } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog' +import { + TriggerWarningDialog, + TriggerWarningType, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-warning-dialog' import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' @@ -83,10 +86,11 @@ const WorkflowContent = React.memo(() => { const [triggerWarning, setTriggerWarning] = useState<{ open: boolean triggerName: string - message?: string + type: TriggerWarningType }>({ open: false, triggerName: '', + type: TriggerWarningType.DUPLICATE_TRIGGER, }) // State for trigger selector modal @@ -509,12 +513,11 @@ const WorkflowContent = React.memo(() => { setTriggerWarning({ open: true, triggerName: 'new trigger', - message: - 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.', + type: TriggerWarningType.LEGACY_INCOMPATIBILITY, }) } else { const triggerName = TriggerUtils.getDefaultTriggerName(type) || 'trigger' - setTriggerWarning({ open: true, triggerName }) + setTriggerWarning({ open: true, triggerName, type: TriggerWarningType.DUPLICATE_TRIGGER }) } return } @@ -616,24 +619,23 @@ const WorkflowContent = React.memo(() => { } } - // Enforce only one API trigger - if (type === 'api_trigger') { - const existingApiTriggers = Object.values(blocks).filter((b) => b.type === 'api_trigger') - if (existingApiTriggers.length >= 1) { - // Surface a clean UI indication; for now, log and skip add - logger.warn('Only one API trigger is allowed per workflow') - return - } - } - // Enforce only one Input trigger (single entry point for manual run UX) - if (type === 'input_trigger') { - const existingInputTriggers = Object.values(blocks).filter( - (b) => b.type === 'input_trigger' - ) - if (existingInputTriggers.length >= 1) { - logger.warn('Only one Input trigger is recommended; manual run uses a single trigger') - return + // Centralized trigger constraints + const additionIssue = TriggerUtils.getTriggerAdditionIssue(blocks, type) + if (additionIssue) { + if (additionIssue.issue === 'legacy') { + setTriggerWarning({ + open: true, + triggerName: additionIssue.triggerName, + type: TriggerWarningType.LEGACY_INCOMPATIBILITY, + }) + } else { + setTriggerWarning({ + open: true, + triggerName: additionIssue.triggerName, + type: TriggerWarningType.DUPLICATE_TRIGGER, + }) } + return } // Add the block to the workflow with auto-connect edge @@ -714,12 +716,15 @@ const WorkflowContent = React.memo(() => { setTriggerWarning({ open: true, triggerName: 'new trigger', - message: - 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.', + type: TriggerWarningType.LEGACY_INCOMPATIBILITY, }) } else { const triggerName = TriggerUtils.getDefaultTriggerName(data.type) || 'trigger' - setTriggerWarning({ open: true, triggerName }) + setTriggerWarning({ + open: true, + triggerName, + type: TriggerWarningType.DUPLICATE_TRIGGER, + }) } return } @@ -910,24 +915,17 @@ const WorkflowContent = React.memo(() => { } } } else { - // Check if adding this trigger would violate constraints - if (TriggerUtils.wouldViolateSingleInstance(blocks, data.type)) { - // Check if it's because of a legacy starter block - if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(data.type)) { - setTriggerWarning({ - open: true, - triggerName: 'new trigger', - message: - 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.', - }) - } else { - const triggerName = TriggerUtils.getDefaultTriggerName(data.type) || data.type - setTriggerWarning({ - open: true, - triggerName, - message: `Only one ${triggerName} trigger allowed`, - }) - } + // Centralized trigger constraints + const dropIssue = TriggerUtils.getTriggerAdditionIssue(blocks, data.type) + if (dropIssue) { + setTriggerWarning({ + open: true, + triggerName: dropIssue.triggerName, + type: + dropIssue.issue === 'legacy' + ? TriggerWarningType.LEGACY_INCOMPATIBILITY + : TriggerWarningType.DUPLICATE_TRIGGER, + }) return } @@ -1851,7 +1849,7 @@ const WorkflowContent = React.memo(() => { open={triggerWarning.open} onOpenChange={(open) => setTriggerWarning({ ...triggerWarning, open })} triggerName={triggerWarning.triggerName} - message={triggerWarning.message} + type={triggerWarning.type} /> {/* Trigger selector for empty workflows - only show after workflow has loaded */} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx index 745ccc1056..3d87f32565 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx @@ -114,7 +114,7 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: {/* Tabs */}
- + Blocks @@ -144,7 +144,7 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
{/* Blocks Tab Content */} - +
{/* Regular Blocks */} @@ -178,7 +178,7 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: {/* Triggers Tab Content */} - +
{triggers.length > 0 ? ( @@ -191,7 +191,7 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: /> )) ) : ( -
+
{searchQuery ? 'No triggers found' : 'Add triggers from the workflow canvas'}
)} diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index 5fe05510d3..aad90f6a9e 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -774,12 +774,13 @@ export class Executor { // If no starter block, look for appropriate trigger block based on context if (!initBlock) { if (this.isChildExecution) { - // Child workflows only use Input Trigger blocks const inputTriggerBlocks = this.actualWorkflow.blocks.filter( (block) => block.metadata?.id === 'input_trigger' ) - if (inputTriggerBlocks.length > 0) { + if (inputTriggerBlocks.length === 1) { initBlock = inputTriggerBlocks[0] + } else if (inputTriggerBlocks.length > 1) { + throw new Error('Child workflow has multiple Input Trigger blocks. Keep only one.') } } else { // Parent workflows can use any trigger block diff --git a/apps/sim/executor/resolver/resolver.ts b/apps/sim/executor/resolver/resolver.ts index 01b0ae3e98..bdb8bc57a8 100644 --- a/apps/sim/executor/resolver/resolver.ts +++ b/apps/sim/executor/resolver/resolver.ts @@ -1,6 +1,7 @@ import { BlockPathCalculator } from '@/lib/block-path-calculator' import { createLogger } from '@/lib/logs/console/logger' import { VariableManager } from '@/lib/variables/variable-manager' +import { TRIGGER_REFERENCE_ALIAS_MAP } from '@/lib/workflows/triggers' import { getBlock } from '@/blocks/index' import type { LoopManager } from '@/executor/loops/loops' import type { ExecutionContext } from '@/executor/types' @@ -522,14 +523,8 @@ export class InputResolver { // Special case for trigger block references (start, api, chat, manual) const blockRefLower = blockRef.toLowerCase() - const triggerTypeMap: Record = { - start: 'starter', - api: 'api_trigger', - chat: 'chat_trigger', - manual: 'input_trigger', - } - - const triggerType = triggerTypeMap[blockRefLower] + const triggerType = + TRIGGER_REFERENCE_ALIAS_MAP[blockRefLower as keyof typeof TRIGGER_REFERENCE_ALIAS_MAP] if (triggerType) { const triggerBlock = this.workflow.blocks.find( (block) => block.metadata?.id === triggerType diff --git a/apps/sim/lib/workflows/triggers.ts b/apps/sim/lib/workflows/triggers.ts index a441fc38c6..9acf44e5e8 100644 --- a/apps/sim/lib/workflows/triggers.ts +++ b/apps/sim/lib/workflows/triggers.ts @@ -14,6 +14,19 @@ export const TRIGGER_TYPES = { export type TriggerType = (typeof TRIGGER_TYPES)[keyof typeof TRIGGER_TYPES] +/** + * Mapping from reference alias (used in inline refs like , , etc.) + * to concrete trigger block type identifiers used across the system. + */ +export const TRIGGER_REFERENCE_ALIAS_MAP = { + start: TRIGGER_TYPES.STARTER, + api: TRIGGER_TYPES.API, + chat: TRIGGER_TYPES.CHAT, + manual: TRIGGER_TYPES.INPUT, +} as const + +export type TriggerReferenceAlias = keyof typeof TRIGGER_REFERENCE_ALIAS_MAP + /** * Trigger classification and utilities */ @@ -45,13 +58,7 @@ export class TriggerUtils { * Check if a type string is any trigger type */ static isAnyTriggerType(type: string): boolean { - return ( - type === TRIGGER_TYPES.INPUT || - type === TRIGGER_TYPES.CHAT || - type === TRIGGER_TYPES.API || - type === TRIGGER_TYPES.WEBHOOK || - type === TRIGGER_TYPES.SCHEDULE - ) + return Object.values(TRIGGER_TYPES).includes(type as TriggerType) } /** @@ -296,6 +303,29 @@ export class TriggerUtils { return blockArray.some((block) => block.type === triggerType) } + /** + * Evaluate whether adding a trigger of the given type is allowed and, if not, why. + * Returns null if allowed; otherwise returns an object describing the violation. + * This avoids duplicating UI logic across toolbar/drop handlers. + */ + static getTriggerAdditionIssue( + blocks: T[] | Record, + triggerType: string + ): { issue: 'legacy' | 'duplicate'; triggerName: string } | null { + if (!TriggerUtils.wouldViolateSingleInstance(blocks, triggerType)) { + return null + } + + // Legacy starter present + adding modern trigger → legacy incompatibility + if (TriggerUtils.hasLegacyStarter(blocks) && TriggerUtils.isAnyTriggerType(triggerType)) { + return { issue: 'legacy', triggerName: 'new trigger' } + } + + // Otherwise treat as duplicate of a single-instance trigger + const triggerName = TriggerUtils.getDefaultTriggerName(triggerType) || 'trigger' + return { issue: 'duplicate', triggerName } + } + /** * Get trigger validation message */ diff --git a/apps/sim/stores/workflows/yaml/importer.ts b/apps/sim/stores/workflows/yaml/importer.ts index 3a01662efc..3f6bdb6d37 100644 --- a/apps/sim/stores/workflows/yaml/importer.ts +++ b/apps/sim/stores/workflows/yaml/importer.ts @@ -123,6 +123,11 @@ function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[]; war const errors: string[] = [] const warnings: string[] = [] + // Precompute counts that are used in validations to avoid O(n^2) checks + const apiTriggerCount = Object.values(yamlWorkflow.blocks).filter( + (b) => b.type === 'api_trigger' + ).length + Object.entries(yamlWorkflow.blocks).forEach(([blockId, block]) => { // Use shared structure validation const { errors: structureErrors, warnings: structureWarnings } = validateBlockStructure( @@ -159,10 +164,7 @@ function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[]; war } // Enforce only one API trigger in YAML if (block.type === 'api_trigger') { - const apiCount = Object.values(yamlWorkflow.blocks).filter( - (b) => b.type === 'api_trigger' - ).length - if (apiCount > 1) { + if (apiTriggerCount > 1) { errors.push('Only one API trigger is allowed per workflow (YAML contains multiple).') } } From 84fb4e7d3d29c2510011fc5ab1e059717acc1f99 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 17 Sep 2025 11:00:59 -0700 Subject: [PATCH 21/43] Diff input format --- apps/sim/app/api/workflows/[id]/yaml/route.ts | 57 ++++++++++++++++++ apps/sim/app/api/yaml/diff/create/route.ts | 58 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index 54e33237fa..122eaf48f7 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -240,6 +240,58 @@ async function upsertCustomToolsFromBlocks( } } +/** + * Convert blocks with 'inputs' field to standard 'subBlocks' structure + * This handles trigger blocks that may come from YAML/copilot with legacy format + */ +function normalizeBlockStructure(blocks: Record): Record { + const normalizedBlocks: Record = {} + + for (const [blockId, block] of Object.entries(blocks)) { + const normalizedBlock = { ...block } + + // Check if this is a trigger block with 'inputs' field + if (block.inputs && ( + block.type === 'api_trigger' || + block.type === 'input_trigger' || + block.type === 'starter' || + block.type === 'chat_trigger' || + block.type === 'generic_webhook' + )) { + // Convert inputs.inputFormat to subBlocks.inputFormat + if (block.inputs.inputFormat) { + if (!normalizedBlock.subBlocks) { + normalizedBlock.subBlocks = {} + } + + normalizedBlock.subBlocks.inputFormat = { + id: 'inputFormat', + type: 'input-format', + value: block.inputs.inputFormat + } + } + + // Copy any other inputs fields to subBlocks + for (const [inputKey, inputValue] of Object.entries(block.inputs)) { + if (inputKey !== 'inputFormat' && !normalizedBlock.subBlocks[inputKey]) { + normalizedBlock.subBlocks[inputKey] = { + id: inputKey, + type: 'short-input', // Default type, may need adjustment based on actual field + value: inputValue + } + } + } + + // Remove the inputs field after conversion + delete normalizedBlock.inputs + } + + normalizedBlocks[blockId] = normalizedBlock + } + + return normalizedBlocks +} + /** * PUT /api/workflows/[id]/yaml * Consolidated YAML workflow saving endpoint @@ -344,6 +396,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ }) } + // Normalize blocks that use 'inputs' field to standard 'subBlocks' structure + if (workflowState.blocks) { + workflowState.blocks = normalizeBlockStructure(workflowState.blocks) + } + // Ensure all blocks have required fields Object.values(workflowState.blocks).forEach((block: any) => { if (block.enabled === undefined) { diff --git a/apps/sim/app/api/yaml/diff/create/route.ts b/apps/sim/app/api/yaml/diff/create/route.ts index 627d539e13..e7ec007fce 100644 --- a/apps/sim/app/api/yaml/diff/create/route.ts +++ b/apps/sim/app/api/yaml/diff/create/route.ts @@ -61,6 +61,58 @@ const CreateDiffRequestSchema = z.object({ .optional(), }) +/** + * Convert blocks with 'inputs' field to standard 'subBlocks' structure + * This handles trigger blocks that may come from YAML/copilot with legacy format + */ +function normalizeBlockStructure(blocks: Record): Record { + const normalizedBlocks: Record = {} + + for (const [blockId, block] of Object.entries(blocks)) { + const normalizedBlock = { ...block } + + // Check if this is a trigger block with 'inputs' field + if (block.inputs && ( + block.type === 'api_trigger' || + block.type === 'input_trigger' || + block.type === 'starter' || + block.type === 'chat_trigger' || + block.type === 'generic_webhook' + )) { + // Convert inputs.inputFormat to subBlocks.inputFormat + if (block.inputs.inputFormat) { + if (!normalizedBlock.subBlocks) { + normalizedBlock.subBlocks = {} + } + + normalizedBlock.subBlocks.inputFormat = { + id: 'inputFormat', + type: 'input-format', + value: block.inputs.inputFormat + } + } + + // Copy any other inputs fields to subBlocks + for (const [inputKey, inputValue] of Object.entries(block.inputs)) { + if (inputKey !== 'inputFormat' && !normalizedBlock.subBlocks[inputKey]) { + normalizedBlock.subBlocks[inputKey] = { + id: inputKey, + type: 'short-input', // Default type, may need adjustment based on actual field + value: inputValue + } + } + } + + // Remove the inputs field after conversion + delete normalizedBlock.inputs + } + + normalizedBlocks[blockId] = normalizedBlock + } + + return normalizedBlocks +} + export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -202,6 +254,9 @@ export async function POST(request: NextRequest) { const finalResult = result if (result.success && result.diff?.proposedState) { + // Normalize blocks that use 'inputs' field to standard 'subBlocks' structure + result.diff.proposedState.blocks = normalizeBlockStructure(result.diff.proposedState.blocks) + // First, fix parent-child relationships based on edges const blocks = result.diff.proposedState.blocks const edges = result.diff.proposedState.edges || [] @@ -271,6 +326,9 @@ export async function POST(request: NextRequest) { if (result.success && result.blocks && !result.diff) { logger.info(`[${requestId}] Transforming sim agent blocks response to diff format`) + // Normalize blocks that use 'inputs' field to standard 'subBlocks' structure + result.blocks = normalizeBlockStructure(result.blocks) + // First, fix parent-child relationships based on edges const blocks = result.blocks const edges = result.edges || [] From a9b8fa88acbc56fc00e4ac8759178c4e3b43e542 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 17 Sep 2025 11:24:54 -0700 Subject: [PATCH 22/43] address greptile comments --- .../trigger-selector-modal.tsx | 1 - .../[workspaceId]/w/[workflowId]/workflow.tsx | 36 ------------------- .../server/blocks/get-blocks-and-tools.ts | 23 ++++++------ apps/sim/lib/workflows/triggers.ts | 15 +------- apps/sim/stores/workflows/yaml/importer.ts | 11 +++--- 5 files changed, 19 insertions(+), 67 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx index ba9374f04c..b5c6c34701 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx @@ -47,7 +47,6 @@ export function TriggerSelectorModal({ open, onClose, onSelect }: TriggerSelecto const isHovered = hoveredId === option.id return ( + + +

{trigger.description}

+
+ +
+ ) + } + + return ( +
+ {!showList ? ( + /* Initial Button State */ + + ) : ( + /* Trigger List View */ +
+ {/* Search - matching search modal exactly */} +
+ + setSearchQuery(e.target.value)} + className='!font-[350] border-0 bg-transparent font-sans text-muted-foreground leading-10 tracking-normal placeholder:text-muted-foreground focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0' + autoFocus + /> +
+ + {/* Close button */} + + + {/* Trigger List */} +
+
+ {/* Core Triggers Section */} + {coreOptions.length > 0 && ( +
+

+ Core Triggers +

+
+ {/* Group core triggers in pairs of 2 vertically */} + {Array.from({ length: Math.ceil(coreOptions.length / 2) }).map( + (_, groupIndex) => { + const firstIndex = groupIndex * 2 + const secondIndex = firstIndex + 1 + const firstTrigger = coreOptions[firstIndex] + const secondTrigger = coreOptions[secondIndex] + + return ( +
+ + {secondTrigger && } +
+ ) + } + )} +
+
+ )} + + {/* Integration Triggers Section */} + {integrationOptions.length > 0 && ( +
+

+ Integration Triggers +

+
+ {/* Group integration triggers in pairs of 2 vertically */} + {Array.from({ length: Math.ceil(integrationOptions.length / 2) }).map( + (_, groupIndex) => { + const firstIndex = groupIndex * 2 + const secondIndex = firstIndex + 1 + const firstTrigger = integrationOptions[firstIndex] + const secondTrigger = integrationOptions[secondIndex] + + return ( +
+ + {secondTrigger && } +
+ ) + } + )} +
+
+ )} + + {filteredOptions.length === 0 && ( +
+

No results found for "{searchQuery}"

+
+ )} +
+
+
+ )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx deleted file mode 100644 index 9051476ec8..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client' - -import { Plus, Zap } from 'lucide-react' -import { cn } from '@/lib/utils' - -interface TriggerPlaceholderProps { - onClick: () => void - className?: string -} - -export function TriggerPlaceholder({ onClick, className }: TriggerPlaceholderProps) { - return ( -
- -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx deleted file mode 100644 index b5c6c34701..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal.tsx +++ /dev/null @@ -1,151 +0,0 @@ -'use client' - -import { useMemo, useState } from 'react' -import { Search } from 'lucide-react' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Input } from '@/components/ui/input' -import { cn } from '@/lib/utils' -import type { TriggerInfo } from '@/lib/workflows/trigger-utils' -import { getAllTriggerBlocks, getTriggerDisplayName } from '@/lib/workflows/trigger-utils' - -interface TriggerSelectorModalProps { - open: boolean - onClose: () => void - onSelect: (triggerId: string, enableTriggerMode?: boolean) => void -} - -export function TriggerSelectorModal({ open, onClose, onSelect }: TriggerSelectorModalProps) { - const [hoveredId, setHoveredId] = useState(null) - const [searchQuery, setSearchQuery] = useState('') - - // Get all trigger options from the centralized source - const triggerOptions = useMemo(() => getAllTriggerBlocks(), []) - - const filteredOptions = useMemo(() => { - if (!searchQuery.trim()) return triggerOptions - - const query = searchQuery.toLowerCase() - return triggerOptions.filter( - (option) => - option.name.toLowerCase().includes(query) || - option.description.toLowerCase().includes(query) - ) - }, [searchQuery, triggerOptions]) - - const coreOptions = useMemo( - () => filteredOptions.filter((opt) => opt.category === 'core'), - [filteredOptions] - ) - - const integrationOptions = useMemo( - () => filteredOptions.filter((opt) => opt.category === 'integration'), - [filteredOptions] - ) - - const TriggerOptionCard = ({ option }: { option: TriggerInfo }) => { - const Icon = option.icon - const isHovered = hoveredId === option.id - return ( - - ) - } - - return ( - !open && onClose()}> - - - - How do you want to trigger this workflow? - -

- Choose how your workflow will be started. You can add more triggers later from the - sidebar. -

-
- -
- {/* Search Input */} -
- - setSearchQuery(e.target.value)} - className='h-6 flex-1 border-0 bg-transparent px-0 text-muted-foreground text-sm leading-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0' - autoComplete='off' - autoCorrect='off' - autoCapitalize='off' - spellCheck='false' - /> -
-
- -
- {/* Core Triggers Section */} - {coreOptions.length > 0 && ( - <> -

Core Triggers

-
- {coreOptions.map((option) => ( - - ))} -
- - )} - - {/* Integration Triggers Section */} - {integrationOptions.length > 0 && ( - <> -

- Integration Triggers -

-
- {integrationOptions.map((option) => ( - - ))} -
- - )} - - {filteredOptions.length === 0 && ( -
- No triggers found matching "{searchQuery}" -
- )} -
-
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 8d645522ea..3800801b69 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -660,7 +660,10 @@ export function WorkflowBlock({ id, data }: NodeProps) { {/* Block Header */}
0 && 'border-b' + )} onMouseDown={(e) => { e.stopPropagation() }} @@ -929,90 +932,92 @@ export function WorkflowBlock({ id, data }: NodeProps) { ) )} - - - - - - {!userPermissions.canEdit && !currentWorkflow.isDiffMode - ? userPermissions.isOfflineMode - ? 'Connection lost - please refresh' - : 'Read-only mode' - : displayIsWide - ? 'Narrow Block' - : 'Expand Block'} - - + {subBlockRows.length > 0 && ( + + + + + + {!userPermissions.canEdit && !currentWorkflow.isDiffMode + ? userPermissions.isOfflineMode + ? 'Connection lost - please refresh' + : 'Read-only mode' + : displayIsWide + ? 'Narrow Block' + : 'Expand Block'} + + + )}
- {/* Block Content */} -
{ - e.stopPropagation() - }} - > - {subBlockRows.length > 0 - ? subBlockRows.map((row, rowIndex) => ( -
- {row.map((subBlock, blockIndex) => ( -
- -
- ))} -
- )) - : null} -
+ {/* Block Content - Only render if there are subblocks */} + {subBlockRows.length > 0 && ( +
{ + e.stopPropagation() + }} + > + {subBlockRows.map((row, rowIndex) => ( +
+ {row.map((subBlock, blockIndex) => ( +
+ +
+ ))} +
+ ))} +
+ )} {/* Output Handle */} {type !== 'condition' && type !== 'response' && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 46153e6cb3..2192abd29d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -20,8 +20,7 @@ import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/compo import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index' import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel' import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node' -import { TriggerPlaceholder } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-placeholder' -import { TriggerSelectorModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-selector/trigger-selector-modal' +import { TriggerList } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-list/trigger-list' import { TriggerWarningDialog, TriggerWarningType, @@ -93,9 +92,6 @@ const WorkflowContent = React.memo(() => { type: TriggerWarningType.DUPLICATE_TRIGGER, }) - // State for trigger selector modal - const [showTriggerSelector, setShowTriggerSelector] = useState(false) - // Hooks const params = useParams() const router = useRouter() @@ -656,11 +652,9 @@ const WorkflowContent = React.memo(() => { setTriggerWarning, ]) - // Handler for trigger selection from modal + // Handler for trigger selection from list const handleTriggerSelect = useCallback( (triggerId: string, enableTriggerMode?: boolean) => { - setShowTriggerSelector(false) - // Get the trigger name const triggerName = TriggerUtils.getDefaultTriggerName(triggerId) || triggerId @@ -1816,16 +1810,10 @@ const WorkflowContent = React.memo(() => { type={triggerWarning.type} /> - {/* Trigger selector for empty workflows - only show after workflow has loaded */} + {/* Trigger list for empty workflows - only show after workflow has loaded */} {isWorkflowReady && isWorkflowEmpty && effectivePermissions.canEdit && ( - setShowTriggerSelector(true)} /> + )} - - setShowTriggerSelector(false)} - onSelect={handleTriggerSelect} - />
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx index 3d87f32565..0fc50df9d6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/toolbar.tsx @@ -1,10 +1,10 @@ 'use client' import { useMemo, useState } from 'react' -import { Blocks, Search, Zap } from 'lucide-react' +import { Search } from 'lucide-react' import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Tabs, TabsContent } from '@/components/ui/tabs' import { getBlocksForSidebar, getTriggersForSidebar, @@ -114,16 +114,24 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }: {/* Tabs */}
- - - +
+ + +
{/* Search */} diff --git a/apps/sim/blocks/blocks/api_trigger.ts b/apps/sim/blocks/blocks/api_trigger.ts index 5ef45eeb1d..7451283729 100644 --- a/apps/sim/blocks/blocks/api_trigger.ts +++ b/apps/sim/blocks/blocks/api_trigger.ts @@ -3,12 +3,12 @@ import type { BlockConfig } from '@/blocks/types' export const ApiTriggerBlock: BlockConfig = { type: 'api_trigger', - name: 'API Trigger', + name: 'API', description: 'Expose as HTTP API endpoint', longDescription: 'API trigger to start the workflow via authenticated HTTP calls with structured input.', category: 'triggers', - bgColor: '#10B981', // Emerald for API + bgColor: '#2F55FF', icon: ApiIcon, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts index 7566a2dfc1..33723562b7 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -7,11 +7,11 @@ const ChatTriggerIcon = (props: SVGProps) => createElement(Messag export const ChatTriggerBlock: BlockConfig = { type: 'chat_trigger', - name: 'Chat Trigger', + name: 'Chat', description: 'Start workflow from a chat deployment', longDescription: 'Chat trigger to run the workflow via deployed chat interfaces.', category: 'triggers', - bgColor: '#8B5CF6', + bgColor: '#6F3DFA', icon: ChatTriggerIcon, subBlocks: [], tools: { diff --git a/apps/sim/blocks/blocks/input_trigger.ts b/apps/sim/blocks/blocks/input_trigger.ts index b30e06c08a..1ada9e07de 100644 --- a/apps/sim/blocks/blocks/input_trigger.ts +++ b/apps/sim/blocks/blocks/input_trigger.ts @@ -7,7 +7,7 @@ const InputTriggerIcon = (props: SVGProps) => createElement(Play, export const InputTriggerBlock: BlockConfig = { type: 'input_trigger', - name: 'Input Trigger', + name: 'Input Form', description: 'Start workflow manually with a defined input schema', longDescription: 'Manually trigger the workflow from the editor with a structured input schema. This enables typed inputs for parent workflows to map into.', From ed545264b7a862638429a1ed5562569a2f447a15 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Mon, 22 Sep 2025 10:48:36 -0700 Subject: [PATCH 24/43] improvement: changed to vertical scrolling --- .../components/trigger-list/trigger-list.tsx | 57 +++++-------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-list/trigger-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-list/trigger-list.tsx index abb77713a3..a98e7c3159 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-list/trigger-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/trigger-list/trigger-list.tsx @@ -204,29 +204,13 @@ export function TriggerList({ onSelect, className }: TriggerListProps) {

Core Triggers

-
- {/* Group core triggers in pairs of 2 vertically */} - {Array.from({ length: Math.ceil(coreOptions.length / 2) }).map( - (_, groupIndex) => { - const firstIndex = groupIndex * 2 - const secondIndex = firstIndex + 1 - const firstTrigger = coreOptions[firstIndex] - const secondTrigger = coreOptions[secondIndex] - - return ( -
- - {secondTrigger && } -
- ) - } - )} +
+ {/* Display triggers in a 3-column grid */} +
+ {coreOptions.map((trigger) => ( + + ))} +
)} @@ -238,28 +222,15 @@ export function TriggerList({ onSelect, className }: TriggerListProps) { Integration Triggers
- {/* Group integration triggers in pairs of 2 vertically */} - {Array.from({ length: Math.ceil(integrationOptions.length / 2) }).map( - (_, groupIndex) => { - const firstIndex = groupIndex * 2 - const secondIndex = firstIndex + 1 - const firstTrigger = integrationOptions[firstIndex] - const secondTrigger = integrationOptions[secondIndex] - - return ( -
- - {secondTrigger && } -
- ) - } - )} + {/* Display triggers in a 3-column grid */} +
+ {integrationOptions.map((trigger) => ( + + ))} +
)} From a831cd4bbc5576dd7b0c1ab851c3f9e87d1c16a6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 16:47:14 -0700 Subject: [PATCH 25/43] fix(workflow): ensure new blocks from sidebar click/drag use getUniqueBlockName (with semantic trigger base when applicable) --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 5ac9d3017d..d001ec5f80 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -659,11 +659,10 @@ const WorkflowContent = React.memo(() => { // Create a new block with a unique ID const id = crypto.randomUUID() - // Prefer semantic default names for triggers to support , , references + // Prefer semantic default names for triggers; then ensure unique numbering centrally const defaultTriggerName = TriggerUtils.getDefaultTriggerName(type) - const name = - defaultTriggerName || - `${blockConfig.name} ${Object.values(blocks).filter((b) => b.type === type).length + 1}` + const baseName = defaultTriggerName || blockConfig.name + const name = getUniqueBlockName(baseName, blocks) // Auto-connect logic const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled @@ -867,15 +866,15 @@ const WorkflowContent = React.memo(() => { // Generate id and name here so they're available in all code paths const id = crypto.randomUUID() - // Prefer semantic default names for triggers to support , , references + // Prefer semantic default names for triggers; then ensure unique numbering centrally const defaultTriggerNameDrop = TriggerUtils.getDefaultTriggerName(data.type) - const name = + const baseName = data.type === 'loop' - ? `Loop ${Object.values(blocks).filter((b) => b.type === 'loop').length + 1}` + ? 'Loop' : data.type === 'parallel' - ? `Parallel ${Object.values(blocks).filter((b) => b.type === 'parallel').length + 1}` - : defaultTriggerNameDrop || - `${blockConfig!.name} ${Object.values(blocks).filter((b) => b.type === data.type).length + 1}` + ? 'Parallel' + : defaultTriggerNameDrop || blockConfig!.name + const name = getUniqueBlockName(baseName, blocks) if (containerInfo) { // Calculate position relative to the container node From 5f9a6c72b6c0897a5bcc8c5875145099c4f078a1 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 17:05:28 -0700 Subject: [PATCH 26/43] Validation + build/edit mark complete --- apps/sim/app/api/copilot/chat/route.ts | 7 +- .../execute-copilot-server-tool/route.ts | 3 +- apps/sim/app/api/workflows/[id]/yaml/route.ts | 63 +++-- apps/sim/app/api/yaml/diff/create/route.ts | 40 ++- .../components/tool-input/tool-input.tsx | 56 ++++- apps/sim/lib/copilot/registry.ts | 49 ++-- .../tools/client/blocks/get-trigger-blocks.ts | 64 +++++ .../tools/client/examples/get-examples-rag.ts | 32 +++ .../client/examples/get-trigger-examples.ts | 32 +++ .../tools/client/workflow/build-workflow.ts | 8 +- .../tools/client/workflow/edit-workflow.ts | 8 +- .../server/blocks/get-blocks-and-tools.ts | 2 +- .../tools/server/blocks/get-trigger-blocks.ts | 48 ++++ apps/sim/lib/copilot/tools/server/router.ts | 10 + .../tools/server/workflow/build-workflow.ts | 116 +++++---- .../tools/server/workflow/edit-workflow.ts | 47 ++++ apps/sim/lib/copilot/tools/shared/schemas.ts | 7 + apps/sim/lib/sim-agent/constants.ts | 1 + apps/sim/lib/sim-agent/index.ts | 2 +- apps/sim/lib/workflows/validation.ts | 163 +++++++++++++ apps/sim/stores/copilot/store.ts | 15 ++ apps/sim/stores/workflow-diff/store.ts | 43 +++- apps/sim/tools/params.ts | 230 +++++++++--------- 23 files changed, 837 insertions(+), 209 deletions(-) create mode 100644 apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts create mode 100644 apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts create mode 100644 apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts create mode 100644 apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 8bf4b676f2..ace360ac49 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -15,7 +15,7 @@ import { getCopilotModel } from '@/lib/copilot/config' import type { CopilotProviderConfig } from '@/lib/copilot/types' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' -import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent' +import { SIM_AGENT_API_URL_DEFAULT, SIM_AGENT_VERSION } from '@/lib/sim-agent' import { generateChatTitle } from '@/lib/sim-agent/utils' import { createFileContent, isSupportedFileType } from '@/lib/uploads/file-utils' import { S3_COPILOT_CONFIG } from '@/lib/uploads/setup' @@ -366,12 +366,14 @@ export async function POST(req: NextRequest) { const requestPayload = { messages: messagesForAgent, + chatMessages: messages, // Full unfiltered messages array workflowId, userId: authenticatedUserId, stream: stream, streamToolCalls: true, mode: mode, messageId: userMessageIdToUse, + version: SIM_AGENT_VERSION, ...(providerConfig ? { provider: providerConfig } : {}), ...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}), ...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}), @@ -384,6 +386,9 @@ export async function POST(req: NextRequest) { try { logger.info(`[${tracker.requestId}] About to call Sim Agent with context`, { context: (requestPayload as any).context, + messagesCount: messagesForAgent.length, + chatMessagesCount: messages.length, + hasConversationId: !!effectiveConversationId, }) } catch {} diff --git a/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts b/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts index f7ead3e522..d356162857 100644 --- a/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts +++ b/apps/sim/app/api/copilot/execute-copilot-server-tool/route.ts @@ -48,6 +48,7 @@ export async function POST(req: NextRequest) { return createBadRequestResponse('Invalid request body for execute-copilot-server-tool') } logger.error(`[${tracker.requestId}] Failed to execute server tool:`, error) - return createInternalServerErrorResponse('Failed to execute server tool') + const errorMessage = error instanceof Error ? error.message : 'Failed to execute server tool' + return createInternalServerErrorResponse(errorMessage) } } diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index f15d75c23b..675d55b3f1 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -13,6 +13,7 @@ import { saveWorkflowToNormalizedTables, } from '@/lib/workflows/db-helpers' import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation' +import { validateWorkflowState } from '@/lib/workflows/validation' import { getUserId } from '@/app/api/auth/oauth/utils' import { getAllBlocks, getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' @@ -402,29 +403,59 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ workflowState.blocks = normalizeBlockStructure(workflowState.blocks) } + // Validate the workflow state before persisting + const validation = validateWorkflowState(workflowState, { sanitize: true }) + + if (!validation.valid) { + logger.error(`[${requestId}] Workflow validation failed`, { + errors: validation.errors, + warnings: validation.warnings, + }) + return NextResponse.json({ + success: false, + message: 'Invalid workflow structure', + errors: validation.errors, + warnings: validation.warnings || [], + }) + } + + // Use sanitized state if available + const finalWorkflowState = validation.sanitizedState || workflowState + + if (validation.warnings.length > 0) { + logger.warn(`[${requestId}] Workflow validation warnings`, { + warnings: validation.warnings, + }) + } + // Ensure all blocks have required fields - Object.values(workflowState.blocks).forEach((block: any) => { - if (block.enabled === undefined) { - block.enabled = true + Object.entries(finalWorkflowState.blocks).forEach(([blockId, block]) => { + const blockData = block as any + if (!blockData.id) blockData.id = blockId + if (!blockData.position) { + blockData.position = { x: 0, y: 0 } + } + if (blockData.enabled === undefined) { + blockData.enabled = true } - if (block.horizontalHandles === undefined) { - block.horizontalHandles = true + if (blockData.horizontalHandles === undefined) { + blockData.horizontalHandles = true } - if (block.isWide === undefined) { - block.isWide = false + if (blockData.isWide === undefined) { + blockData.isWide = false } - if (block.height === undefined) { - block.height = 0 + if (blockData.height === undefined) { + blockData.height = 0 } - if (!block.subBlocks) { - block.subBlocks = {} + if (!blockData.subBlocks) { + blockData.subBlocks = {} } - if (!block.outputs) { - block.outputs = {} + if (!blockData.outputs) { + blockData.outputs = {} } }) - const blocks = Object.values(workflowState.blocks) as Array<{ + const blocks = Object.values(finalWorkflowState.blocks) as Array<{ id: string type: string name: string @@ -434,7 +465,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ parentId?: string extent?: string }> - const edges = workflowState.edges + const edges = finalWorkflowState.edges const warnings = conversionResult.warnings || [] // Create workflow state @@ -796,4 +827,4 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ { status: 500 } ) } -} +} \ No newline at end of file diff --git a/apps/sim/app/api/yaml/diff/create/route.ts b/apps/sim/app/api/yaml/diff/create/route.ts index 53a8692041..beea8b1948 100644 --- a/apps/sim/app/api/yaml/diff/create/route.ts +++ b/apps/sim/app/api/yaml/diff/create/route.ts @@ -15,6 +15,7 @@ import { generateLoopBlocks, generateParallelBlocks, } from '@/stores/workflows/workflow/utils' +import { validateWorkflowState } from '@/lib/workflows/validation' const logger = createLogger('YamlDiffCreateAPI') @@ -256,7 +257,44 @@ export async function POST(request: NextRequest) { if (result.success && result.diff?.proposedState) { // Normalize blocks that use 'inputs' field to standard 'subBlocks' structure - result.diff.proposedState.blocks = normalizeBlockStructure(result.diff.proposedState.blocks) + if (result.diff.proposedState.blocks) { + result.diff.proposedState.blocks = normalizeBlockStructure(result.diff.proposedState.blocks) + } + + // Validate the proposed workflow state + const validation = validateWorkflowState(result.diff.proposedState, { sanitize: true }) + + if (!validation.valid) { + logger.error(`[${requestId}] Proposed workflow state validation failed`, { + errors: validation.errors, + warnings: validation.warnings, + }) + return NextResponse.json( + { + success: false, + errors: validation.errors, + }, + { status: 400 } + ) + } + + // Use sanitized state if available + if (validation.sanitizedState) { + result.diff.proposedState = validation.sanitizedState + } + + if (validation.warnings.length > 0) { + logger.warn(`[${requestId}] Proposed workflow validation warnings`, { + warnings: validation.warnings, + }) + // Include warnings in the response + if (!result.warnings) { + result.warnings = [] + } + result.warnings.push(...validation.warnings) + } + + logger.info(`[${requestId}] Successfully created diff with normalized and validated blocks`) // First, fix parent-child relationships based on edges const blocks = result.diff.proposedState.blocks diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx index dcc48297e1..5ec6850f7f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx @@ -51,6 +51,8 @@ import { isPasswordParameter, type ToolParameterConfig, } from '@/tools/params' +import { AlertCircle } from 'lucide-react' +import React from 'react' const logger = createLogger('ToolInput') @@ -374,6 +376,44 @@ function FileUploadSyncWrapper({ ) } +// Error boundary component for tool input +class ToolInputErrorBoundary extends React.Component< + { children: React.ReactNode; blockName?: string }, + { hasError: boolean; error?: Error } +> { + constructor(props: any) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(error: Error) { + return { hasError: true, error } + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('ToolInput error:', error, info) + } + + render() { + if (this.state.hasError) { + return ( +
+
+ + Tool Configuration Error +
+

+ {this.props.blockName ? `Block "${this.props.blockName}": ` : ''} + Invalid tool reference. Please check the workflow configuration. +

+
+ ) + } + + return this.props.children + } +} + export function ToolInput({ blockId, subBlockId, @@ -475,10 +515,18 @@ export function ToolInput({ // Fallback: create options from tools.access return block.tools.access.map((toolId) => { - const toolParams = getToolParametersConfig(toolId) - return { - id: toolId, - label: toolParams?.toolConfig?.name || toolId, + try { + const toolParams = getToolParametersConfig(toolId) + return { + id: toolId, + label: toolParams?.toolConfig?.name || toolId, + } + } catch (error) { + console.error(`Error getting tool config for ${toolId}:`, error) + return { + id: toolId, + label: toolId, + } } }) } diff --git a/apps/sim/lib/copilot/registry.ts b/apps/sim/lib/copilot/registry.ts index 23caa59561..b2ce028755 100644 --- a/apps/sim/lib/copilot/registry.ts +++ b/apps/sim/lib/copilot/registry.ts @@ -9,9 +9,8 @@ export const ToolIds = z.enum([ 'get_workflow_console', 'get_blocks_and_tools', 'get_blocks_metadata', - 'get_block_best_practices', - 'get_build_workflow_examples', - 'get_edit_workflow_examples', + 'get_trigger_examples', + 'get_examples_rag', 'search_documentation', 'search_online', 'make_api_request', @@ -30,6 +29,7 @@ export const ToolIds = z.enum([ 'set_global_workflow_variables', // New 'oauth_request_access', + 'get_trigger_blocks', ]) export type ToolId = z.infer @@ -102,6 +102,8 @@ export const ToolArgSchemas = { blockIds: StringArray.min(1), }), + get_trigger_blocks: z.object({}), + get_block_best_practices: z.object({ blockIds: StringArray.min(1), }), @@ -114,6 +116,12 @@ export const ToolArgSchemas = { exampleIds: StringArray.min(1), }), + get_trigger_examples: z.object({}), + + get_examples_rag: z.object({ + query: z.string(), + }), + search_documentation: z.object({ query: z.string(), topK: NumberOptional, @@ -198,18 +206,10 @@ export const ToolSSESchemas = { get_workflow_console: toolCallSSEFor('get_workflow_console', ToolArgSchemas.get_workflow_console), get_blocks_and_tools: toolCallSSEFor('get_blocks_and_tools', ToolArgSchemas.get_blocks_and_tools), get_blocks_metadata: toolCallSSEFor('get_blocks_metadata', ToolArgSchemas.get_blocks_metadata), - get_block_best_practices: toolCallSSEFor( - 'get_block_best_practices', - ToolArgSchemas.get_block_best_practices - ), - get_build_workflow_examples: toolCallSSEFor( - 'get_build_workflow_examples', - ToolArgSchemas.get_build_workflow_examples - ), - get_edit_workflow_examples: toolCallSSEFor( - 'get_edit_workflow_examples', - ToolArgSchemas.get_edit_workflow_examples - ), + get_trigger_blocks: toolCallSSEFor('get_trigger_blocks', ToolArgSchemas.get_trigger_blocks), + + get_trigger_examples: toolCallSSEFor('get_trigger_examples', ToolArgSchemas.get_trigger_examples), + get_examples_rag: toolCallSSEFor('get_examples_rag', ToolArgSchemas.get_examples_rag), search_documentation: toolCallSSEFor('search_documentation', ToolArgSchemas.search_documentation), search_online: toolCallSSEFor('search_online', ToolArgSchemas.search_online), make_api_request: toolCallSSEFor('make_api_request', ToolArgSchemas.make_api_request), @@ -296,6 +296,7 @@ export const ToolResultSchemas = { get_workflow_console: z.object({ entries: z.array(ExecutionEntry) }), get_blocks_and_tools: z.object({ blocks: z.array(z.any()), tools: z.array(z.any()) }), get_blocks_metadata: z.object({ metadata: z.record(z.any()) }), + get_trigger_blocks: z.object({ triggerBlockIds: z.array(z.string()) }), get_block_best_practices: z.object({ bestPractices: z.array(z.any()) }), get_build_workflow_examples: z.object({ examples: z.array( @@ -311,6 +312,24 @@ export const ToolResultSchemas = { }) ), }), + get_trigger_examples: z.object({ + examples: z.array( + z.object({ + id: z.string(), + title: z.string().optional(), + operations: z.array(z.any()).optional(), + }) + ), + }), + get_examples_rag: z.object({ + examples: z.array( + z.object({ + id: z.string(), + title: z.string().optional(), + operations: z.array(z.any()).optional(), + }) + ), + }), search_documentation: z.object({ results: z.array(z.any()) }), search_online: z.object({ results: z.array(z.any()) }), make_api_request: z.object({ diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts new file mode 100644 index 0000000000..3e9e8434b1 --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts @@ -0,0 +1,64 @@ +import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' +import { + ExecuteResponseSuccessSchema, + GetTriggerBlocksResult, +} from '@/lib/copilot/tools/shared/schemas' +import { createLogger } from '@/lib/logs/console/logger' + +export class GetTriggerBlocksClientTool extends BaseClientTool { + static readonly id = 'get_trigger_blocks' + + constructor(toolCallId: string) { + super(toolCallId, GetTriggerBlocksClientTool.id, GetTriggerBlocksClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Found trigger blocks', icon: ListFilter }, + [ClientToolCallState.error]: { text: 'Failed to find trigger blocks', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted finding trigger blocks', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped finding trigger blocks', icon: MinusCircle }, + }, + interrupt: undefined, + } + + async execute(): Promise { + const logger = createLogger('GetTriggerBlocksClientTool') + try { + this.setState(ClientToolCallState.executing) + + const res = await fetch('/api/copilot/execute-copilot-server-tool', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ toolName: 'get_trigger_blocks', payload: {} }), + }) + if (!res.ok) { + const errorText = await res.text().catch(() => '') + try { + const errorJson = JSON.parse(errorText) + throw new Error(errorJson.error || errorText || `Server error (${res.status})`) + } catch { + throw new Error(errorText || `Server error (${res.status})`) + } + } + const json = await res.json() + const parsed = ExecuteResponseSuccessSchema.parse(json) + const result = GetTriggerBlocksResult.parse(parsed.result) + + await this.markToolComplete(200, 'Successfully retrieved trigger blocks', result) + this.setState(ClientToolCallState.success) + } catch (error: any) { + const message = error instanceof Error ? error.message : String(error) + await this.markToolComplete(500, message) + this.setState(ClientToolCallState.error) + } + } +} \ No newline at end of file diff --git a/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts b/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts new file mode 100644 index 0000000000..26ed518f0a --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts @@ -0,0 +1,32 @@ +import { Blocks, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' + + +export class GetExamplesRagClientTool extends BaseClientTool { + static readonly id = 'get_examples_rag' + + constructor(toolCallId: string) { + super(toolCallId, GetExamplesRagClientTool.id, GetExamplesRagClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting examples', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting examples', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting examples', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Got examples', icon: Blocks }, + [ClientToolCallState.error]: { text: 'Failed to get examples', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting examples', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting examples', icon: MinusCircle }, + }, + interrupt: undefined, + } + + async execute(): Promise { + return + } +} diff --git a/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts b/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts new file mode 100644 index 0000000000..1fde2c12a3 --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts @@ -0,0 +1,32 @@ +import { Blocks, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' + + +export class GetTriggerExamplesClientTool extends BaseClientTool { + static readonly id = 'get_trigger_examples' + + constructor(toolCallId: string) { + super(toolCallId, GetTriggerExamplesClientTool.id, GetTriggerExamplesClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting trigger examples', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting trigger examples', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting trigger examples', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Got trigger examples', icon: Blocks }, + [ClientToolCallState.error]: { text: 'Failed to get trigger examples', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting trigger examples', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting trigger examples', icon: MinusCircle }, + }, + interrupt: undefined, + } + + async execute(): Promise { + return + } +} diff --git a/apps/sim/lib/copilot/tools/client/workflow/build-workflow.ts b/apps/sim/lib/copilot/tools/client/workflow/build-workflow.ts index 6c36ccd6cc..9adaa06bbc 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/build-workflow.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/build-workflow.ts @@ -79,7 +79,12 @@ export class BuildWorkflowClientTool extends BaseClientTool { }) if (!res.ok) { const errorText = await res.text().catch(() => '') - throw new Error(errorText || `Server error (${res.status})`) + try { + const errorJson = JSON.parse(errorText) + throw new Error(errorJson.error || errorText || `Server error (${res.status})`) + } catch { + throw new Error(errorText || `Server error (${res.status})`) + } } const json = await res.json() @@ -111,6 +116,7 @@ export class BuildWorkflowClientTool extends BaseClientTool { } catch (error: any) { const message = error instanceof Error ? error.message : String(error) logger.error('execute error', { message }) + await this.markToolComplete(500, message) this.setState(ClientToolCallState.error) } } diff --git a/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts index 50d3a5d6fc..1bc3fa84a4 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts @@ -135,7 +135,12 @@ export class EditWorkflowClientTool extends BaseClientTool { }) if (!res.ok) { const errorText = await res.text().catch(() => '') - throw new Error(errorText || `Server error (${res.status})`) + try { + const errorJson = JSON.parse(errorText) + throw new Error(errorJson.error || errorText || `Server error (${res.status})`) + } catch { + throw new Error(errorText || `Server error (${res.status})`) + } } const json = await res.json() @@ -169,6 +174,7 @@ export class EditWorkflowClientTool extends BaseClientTool { } catch (error: any) { const message = error instanceof Error ? error.message : String(error) logger.error('execute error', { message }) + await this.markToolComplete(500, message) this.setState(ClientToolCallState.error) } } diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts index 6ed908d346..9609f70aec 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts @@ -31,7 +31,7 @@ export const getBlocksAndToolsServerTool: BaseServerTool< type: blockType, name: blockConfig.name, description: blockConfig.longDescription, - triggerAllowed: !!blockConfig.triggerAllowed, + triggerAllowed: 'triggerAllowed' in blockConfig ? !!blockConfig.triggerAllowed : false, }) }) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts new file mode 100644 index 0000000000..d9acd4ee8d --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts @@ -0,0 +1,48 @@ +import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { registry as blockRegistry } from '@/blocks/registry' +import type { BlockConfig } from '@/blocks/types' + +// Define input and result schemas +export const GetTriggerBlocksInput = z.object({}) +export const GetTriggerBlocksResult = z.object({ + triggerBlockIds: z.array(z.string()), +}) + +export const getTriggerBlocksServerTool: BaseServerTool< + ReturnType, + ReturnType +> = { + name: 'get_trigger_blocks', + async execute() { + const logger = createLogger('GetTriggerBlocksServerTool') + logger.debug('Executing get_trigger_blocks') + + const triggerBlockIds: string[] = [] + + Object.entries(blockRegistry).forEach(([blockType, blockConfig]: [string, BlockConfig]) => { + // Skip hidden blocks + if (blockConfig.hideFromToolbar) return + + // Check if it's a trigger block (category: 'triggers') + if (blockConfig.category === 'triggers') { + triggerBlockIds.push(blockType) + } + // Check if it's a tool with trigger capability (triggerAllowed: true) + else if ('triggerAllowed' in blockConfig && blockConfig.triggerAllowed === true) { + triggerBlockIds.push(blockType) + } + // Check if it has a trigger-config subblock + else if (blockConfig.subBlocks?.some((subBlock) => subBlock.type === 'trigger-config')) { + triggerBlockIds.push(blockType) + } + }) + + // Sort alphabetically for consistency + triggerBlockIds.sort() + + logger.debug(`Found ${triggerBlockIds.length} trigger blocks`) + return GetTriggerBlocksResult.parse({ triggerBlockIds }) + }, +} \ No newline at end of file diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index 14bc621876..ab59fc9313 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -1,6 +1,7 @@ import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { getBlocksAndToolsServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-and-tools' import { getBlocksMetadataServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-metadata-tool' +import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/get-trigger-blocks' import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation' import { listGDriveFilesServerTool } from '@/lib/copilot/tools/server/gdrive/list-files' import { readGDriveFileServerTool } from '@/lib/copilot/tools/server/gdrive/read-file' @@ -20,6 +21,8 @@ import { GetBlocksAndToolsResult, GetBlocksMetadataInput, GetBlocksMetadataResult, + GetTriggerBlocksInput, + GetTriggerBlocksResult, } from '@/lib/copilot/tools/shared/schemas' import { createLogger } from '@/lib/logs/console/logger' @@ -34,6 +37,7 @@ const logger = createLogger('ServerToolRouter') // Register tools serverToolRegistry[getBlocksAndToolsServerTool.name] = getBlocksAndToolsServerTool serverToolRegistry[getBlocksMetadataServerTool.name] = getBlocksMetadataServerTool +serverToolRegistry[getTriggerBlocksServerTool.name] = getTriggerBlocksServerTool serverToolRegistry[buildWorkflowServerTool.name] = buildWorkflowServerTool serverToolRegistry[editWorkflowServerTool.name] = editWorkflowServerTool serverToolRegistry[getWorkflowConsoleServerTool.name] = getWorkflowConsoleServerTool @@ -70,6 +74,9 @@ export async function routeExecution(toolName: string, payload: unknown): Promis if (toolName === 'get_blocks_metadata') { args = GetBlocksMetadataInput.parse(args) } + if (toolName === 'get_trigger_blocks') { + args = GetTriggerBlocksInput.parse(args) + } if (toolName === 'build_workflow') { args = BuildWorkflowInput.parse(args) } @@ -82,6 +89,9 @@ export async function routeExecution(toolName: string, payload: unknown): Promis if (toolName === 'get_blocks_metadata') { return GetBlocksMetadataResult.parse(result) } + if (toolName === 'get_trigger_blocks') { + return GetTriggerBlocksResult.parse(result) + } if (toolName === 'build_workflow') { return BuildWorkflowResult.parse(result) } diff --git a/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts index 7b31b67cf4..8fda7bce6f 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts @@ -1,5 +1,3 @@ -import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' -import { type BuildWorkflowInput, BuildWorkflowResult } from '@/lib/copilot/tools/shared/schemas' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent' @@ -7,6 +5,9 @@ import { getAllBlocks } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { resolveOutputType } from '@/blocks/utils' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' +import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import { BuildWorkflowInput, BuildWorkflowResult } from '@/lib/copilot/tools/shared/schemas' +import { validateWorkflowState } from '@/lib/workflows/validation' const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT @@ -70,70 +71,81 @@ export const buildWorkflowServerTool: BaseServerTool< errors: conversionResult.errors, warnings: conversionResult.warnings, }) - return BuildWorkflowResult.parse({ - success: false, - message: `Failed to convert YAML workflow: ${Array.isArray(conversionResult.errors) ? conversionResult.errors.join(', ') : 'Unknown errors'}`, - yamlContent, - description, - }) + throw new Error( + conversionResult.errors?.join(', ') || 'Failed to convert YAML to workflow' + ) } - const { workflowState } = conversionResult + const workflowState = conversionResult.workflowState - const previewWorkflowState = { - blocks: {} as Record, - edges: [] as any[], - loops: {} as Record, - parallels: {} as Record, - lastSaved: Date.now(), - isDeployed: false, + // Validate the workflow state before returning + const validation = validateWorkflowState(workflowState, { sanitize: true }) + + if (!validation.valid) { + logger.error('Generated workflow state is invalid', { + errors: validation.errors, + warnings: validation.warnings, + }) + throw new Error(`Invalid workflow: ${validation.errors.join('; ')}`) } - const blockIdMapping = new Map() - Object.keys(workflowState.blocks).forEach((blockId: string) => { - const previewId = `preview-${Date.now()}-${Math.random().toString(36).substring(2, 7)}` - blockIdMapping.set(blockId, previewId) - }) - - for (const [originalId, block] of Object.entries(workflowState.blocks)) { - const previewBlockId = blockIdMapping.get(originalId as string)! - const typedBlock = block as any - ;(previewWorkflowState.blocks as any)[previewBlockId] = { - ...typedBlock, - id: previewBlockId, - position: typedBlock.position || { x: 0, y: 0 }, - enabled: true, - } + if (validation.warnings.length > 0) { + logger.warn('Workflow validation warnings', { + warnings: validation.warnings, + }) } - ;(previewWorkflowState as any).edges = (workflowState.edges as any[]).map((edge: any) => ({ - ...edge, - id: `edge-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, - source: blockIdMapping.get(edge.source) || edge.source, - target: blockIdMapping.get(edge.target) || edge.target, - })) + // Use sanitized state if available + const finalWorkflowState = validation.sanitizedState || workflowState - const blocksCount = Object.keys((previewWorkflowState as any).blocks).length - const edgesCount = (previewWorkflowState as any).edges.length + // Apply positions using smart layout + const positionResponse = await fetch(`${SIM_AGENT_API_URL}/api/yaml/apply-layout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workflowState: finalWorkflowState, + options: { + strategy: 'smart', + direction: 'auto', + spacing: { + horizontal: 500, + vertical: 400, + layer: 700, + }, + alignment: 'center', + padding: { + x: 250, + y: 250, + }, + }, + }), + }) - logger.info('Workflow built successfully', { blocksCount, edgesCount }) + if (!positionResponse.ok) { + const errorText = await positionResponse.text().catch(() => '') + logger.warn('Failed to apply layout to workflow', { + status: positionResponse.status, + error: errorText, + }) + // Non-critical error - continue with unpositioned workflow + } else { + const layoutResult = await positionResponse.json() + if (layoutResult.success && layoutResult.workflowState) { + // Update the workflow state with positioned blocks + Object.assign(finalWorkflowState, layoutResult.workflowState) + } + } - return BuildWorkflowResult.parse({ + return { success: true, - message: `Successfully built workflow with ${blocksCount} blocks and ${edgesCount} connections`, + workflowState: finalWorkflowState, yamlContent, + message: `Successfully built workflow with ${Object.keys(finalWorkflowState.blocks).length} blocks`, description: description || 'Built workflow', - workflowState: previewWorkflowState, - data: { blocksCount, edgesCount }, - }) + } } catch (error: any) { - logger.error('Failed to build workflow:', error) - return BuildWorkflowResult.parse({ - success: false, - message: `Workflow build failed: ${error instanceof Error ? error.message : 'Unknown error'}`, - yamlContent, - description, - }) + logger.error('Error building workflow', error) + throw error } }, } diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 244d3fe4e5..2c343fbe66 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -10,6 +10,7 @@ import { getAllBlocks } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { resolveOutputType } from '@/blocks/utils' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' +import { validateWorkflowState } from '@/lib/workflows/validation' interface EditWorkflowOperation { operation_type: 'add' | 'edit' | 'delete' @@ -255,9 +256,55 @@ export const editWorkflowServerTool: BaseServerTool = { const modifiedYaml = await applyOperationsToYaml(currentYaml, operations) + // Convert the modified YAML back to workflow state for validation + const validationResponse = await fetch(`${SIM_AGENT_API_URL}/api/yaml/to-workflow`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + yamlContent: modifiedYaml, + blockRegistry, + utilities: { + generateLoopBlocks: generateLoopBlocks.toString(), + generateParallelBlocks: generateParallelBlocks.toString(), + resolveOutputType: resolveOutputType.toString(), + }, + options: { generateNewIds: false, preservePositions: true }, + }), + }) + + if (!validationResponse.ok) { + throw new Error(`Failed to validate edited workflow: ${validationResponse.statusText}`) + } + + const validationResult = await validationResponse.json() + if (!validationResult.success || !validationResult.workflowState) { + throw new Error( + validationResult.errors?.join(', ') || 'Failed to convert edited YAML to workflow' + ) + } + + // Validate the workflow state + const validation = validateWorkflowState(validationResult.workflowState, { sanitize: true }) + + if (!validation.valid) { + logger.error('Edited workflow state is invalid', { + errors: validation.errors, + warnings: validation.warnings, + }) + throw new Error(`Invalid edited workflow: ${validation.errors.join('; ')}`) + } + + if (validation.warnings.length > 0) { + logger.warn('Edited workflow validation warnings', { + warnings: validation.warnings, + }) + } + logger.info('edit_workflow generated modified YAML', { operationCount: operations.length, modifiedYamlLength: modifiedYaml.length, + validationErrors: validation.errors.length, + validationWarnings: validation.warnings.length, }) return { success: true, yamlContent: modifiedYaml } diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts index 8564eebfa4..dde822a1fc 100644 --- a/apps/sim/lib/copilot/tools/shared/schemas.ts +++ b/apps/sim/lib/copilot/tools/shared/schemas.ts @@ -28,6 +28,13 @@ export const GetBlocksMetadataInput = z.object({ blockIds: z.array(z.string()).m export const GetBlocksMetadataResult = z.object({ metadata: z.record(z.any()) }) export type GetBlocksMetadataResultType = z.infer +// get_trigger_blocks +export const GetTriggerBlocksInput = z.object({}) +export const GetTriggerBlocksResult = z.object({ + triggerBlockIds: z.array(z.string()), +}) +export type GetTriggerBlocksResultType = z.infer + // build_workflow export const BuildWorkflowInput = z.object({ yamlContent: z.string(), diff --git a/apps/sim/lib/sim-agent/constants.ts b/apps/sim/lib/sim-agent/constants.ts index 41ee3900c5..dc705dba5b 100644 --- a/apps/sim/lib/sim-agent/constants.ts +++ b/apps/sim/lib/sim-agent/constants.ts @@ -1 +1,2 @@ export const SIM_AGENT_API_URL_DEFAULT = 'https://agent.sim.ai' +export const SIM_AGENT_VERSION = '1.0.0' \ No newline at end of file diff --git a/apps/sim/lib/sim-agent/index.ts b/apps/sim/lib/sim-agent/index.ts index 20183cdaec..f2fe623f02 100644 --- a/apps/sim/lib/sim-agent/index.ts +++ b/apps/sim/lib/sim-agent/index.ts @@ -2,7 +2,7 @@ export type { SimAgentRequest, SimAgentResponse } from './client' export { SimAgentClient, simAgentClient } from './client' -export { SIM_AGENT_API_URL_DEFAULT } from './constants' +export { SIM_AGENT_API_URL_DEFAULT, SIM_AGENT_VERSION } from './constants' // Import for default export import { simAgentClient } from './client' diff --git a/apps/sim/lib/workflows/validation.ts b/apps/sim/lib/workflows/validation.ts index e09abf3aeb..818edea238 100644 --- a/apps/sim/lib/workflows/validation.ts +++ b/apps/sim/lib/workflows/validation.ts @@ -1,4 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' +import type { WorkflowState } from '@/stores/workflows/workflow/types' +import { getTool } from '@/tools/utils' +import { getBlock } from '@/blocks/registry' const logger = createLogger('WorkflowValidation') @@ -107,3 +110,163 @@ export function sanitizeAgentToolsInBlocks(blocks: Record): { return { blocks: sanitizedBlocks, warnings } } + +export interface WorkflowValidationResult { + valid: boolean + errors: string[] + warnings: string[] + sanitizedState?: WorkflowState +} + +/** + * Comprehensive workflow state validation + * Checks all tool references, block types, and required fields + */ +export function validateWorkflowState( + workflowState: WorkflowState, + options: { sanitize?: boolean } = {} +): WorkflowValidationResult { + const errors: string[] = [] + const warnings: string[] = [] + let sanitizedState = workflowState + + try { + // Basic structure validation + if (!workflowState || typeof workflowState !== 'object') { + errors.push('Invalid workflow state: must be an object') + return { valid: false, errors, warnings } + } + + if (!workflowState.blocks || typeof workflowState.blocks !== 'object') { + errors.push('Invalid workflow state: missing blocks') + return { valid: false, errors, warnings } + } + + // Validate each block + const sanitizedBlocks: Record = {} + let hasChanges = false + + for (const [blockId, block] of Object.entries(workflowState.blocks)) { + if (!block || typeof block !== 'object') { + errors.push(`Block ${blockId}: invalid block structure`) + continue + } + + // Check if block type exists + const blockConfig = getBlock(block.type) + if (!blockConfig) { + errors.push(`Block ${block.name || blockId}: unknown block type '${block.type}'`) + if (options.sanitize) { + hasChanges = true + continue // Skip this block in sanitized output + } + } + + // Validate tool references in blocks that use tools + if (block.type === 'api' || block.type === 'generic') { + // For API and generic blocks, the tool is determined by the block's tool configuration + // In the workflow state, we need to check if the block type has valid tool access + const blockConfig = getBlock(block.type) + if (blockConfig && blockConfig.tools?.access) { + // API block has static tool access + const toolIds = blockConfig.tools.access + for (const toolId of toolIds) { + const validationError = validateToolReference(toolId, block.type, block.name) + if (validationError) { + errors.push(validationError) + } + } + } + } else if (block.type === 'knowledge' || block.type === 'supabase' || block.type === 'mcp') { + // These blocks have dynamic tool selection based on operation + // The actual tool validation happens at runtime based on the operation value + // For now, just ensure the block type is valid (already checked above) + } + + // Special validation for agent blocks + if (block.type === 'agent' && block.subBlocks?.tools?.value) { + const toolsSanitization = sanitizeAgentToolsInBlocks({ [blockId]: block }) + warnings.push(...toolsSanitization.warnings) + if (toolsSanitization.warnings.length > 0) { + sanitizedBlocks[blockId] = toolsSanitization.blocks[blockId] + hasChanges = true + } else { + sanitizedBlocks[blockId] = block + } + } else { + sanitizedBlocks[blockId] = block + } + } + + // Validate edges reference existing blocks + if (workflowState.edges && Array.isArray(workflowState.edges)) { + const blockIds = new Set(Object.keys(sanitizedBlocks)) + const loopIds = new Set(Object.keys(workflowState.loops || {})) + const parallelIds = new Set(Object.keys(workflowState.parallels || {})) + + for (const edge of workflowState.edges) { + if (!edge || typeof edge !== 'object') { + errors.push('Invalid edge structure') + continue + } + + // Check if source and target exist + const sourceExists = blockIds.has(edge.source) || loopIds.has(edge.source) || parallelIds.has(edge.source) + const targetExists = blockIds.has(edge.target) || loopIds.has(edge.target) || parallelIds.has(edge.target) + + if (!sourceExists) { + errors.push(`Edge references non-existent source block '${edge.source}'`) + } + if (!targetExists) { + errors.push(`Edge references non-existent target block '${edge.target}'`) + } + } + } + + // If we made changes during sanitization, create a new state object + if (hasChanges && options.sanitize) { + sanitizedState = { + ...workflowState, + blocks: sanitizedBlocks, + } + } + + const valid = errors.length === 0 + return { + valid, + errors, + warnings, + sanitizedState: options.sanitize ? sanitizedState : undefined, + } + } catch (err) { + logger.error('Workflow validation failed with exception', err) + errors.push(`Validation failed: ${err instanceof Error ? err.message : String(err)}`) + return { valid: false, errors, warnings } + } +} + +/** + * Validate tool reference for a specific block + * Returns null if valid, error message if invalid + */ +export function validateToolReference( + toolId: string | undefined, + blockType: string, + blockName?: string +): string | null { + if (!toolId) return null + + // Check if it's a custom tool or MCP tool + const isCustomTool = toolId.startsWith('custom_') + const isMcpTool = toolId.startsWith('mcp-') + + if (!isCustomTool && !isMcpTool) { + // For built-in tools, verify they exist + const tool = getTool(toolId) + if (!tool) { + return `Block ${blockName || 'unknown'} (${blockType}): references non-existent tool '${toolId}'` + } + } + + return null +} diff --git a/apps/sim/stores/copilot/store.ts b/apps/sim/stores/copilot/store.ts index 3fc7553bf6..2c68c34a66 100644 --- a/apps/sim/stores/copilot/store.ts +++ b/apps/sim/stores/copilot/store.ts @@ -10,6 +10,9 @@ import type { import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' import { GetBlocksAndToolsClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-and-tools' import { GetBlocksMetadataClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-metadata' +import { GetTriggerBlocksClientTool } from '@/lib/copilot/tools/client/blocks/get-trigger-blocks' +import { GetTriggerExamplesClientTool } from '@/lib/copilot/tools/client/examples/get-trigger-examples' +import { GetExamplesRagClientTool } from '@/lib/copilot/tools/client/examples/get-examples-rag' import { ListGDriveFilesClientTool } from '@/lib/copilot/tools/client/gdrive/list-files' import { ReadGDriveFileClientTool } from '@/lib/copilot/tools/client/gdrive/read-file' import { GDriveRequestAccessClientTool } from '@/lib/copilot/tools/client/google/gdrive-request-access' @@ -66,6 +69,7 @@ const CLIENT_TOOL_INSTANTIATORS: Record any> = { get_workflow_console: (id) => new GetWorkflowConsoleClientTool(id), get_blocks_and_tools: (id) => new GetBlocksAndToolsClientTool(id), get_blocks_metadata: (id) => new GetBlocksMetadataClientTool(id), + get_trigger_blocks: (id) => new GetTriggerBlocksClientTool(id), search_online: (id) => new SearchOnlineClientTool(id), search_documentation: (id) => new SearchDocumentationClientTool(id), get_environment_variables: (id) => new GetEnvironmentVariablesClientTool(id), @@ -86,6 +90,8 @@ const CLIENT_TOOL_INSTANTIATORS: Record any> = { get_workflow_from_name: (id) => new GetWorkflowFromNameClientTool(id), get_global_workflow_variables: (id) => new GetGlobalWorkflowVariablesClientTool(id), set_global_workflow_variables: (id) => new SetGlobalWorkflowVariablesClientTool(id), + get_trigger_examples: (id) => new GetTriggerExamplesClientTool(id), + get_examples_rag: (id) => new GetExamplesRagClientTool(id), } // Read-only static metadata for class-based tools (no instances) @@ -94,6 +100,7 @@ export const CLASS_TOOL_METADATA: Record()( } else if (result.status === 403) { errorContent = '_Provider config not allowed for non-enterprise users. Please remove the provider config and try again_' + } else if (result.status === 426) { + errorContent = + '_Please upgrade to the latest version of the Sim platform to continue using the copilot._' + } else if (result.status === 429) { + errorContent = + '_Provider rate limit exceeded. Please try again later._' } const errorMessage = createErrorMessage(streamingMessage.id, errorContent) diff --git a/apps/sim/stores/workflow-diff/store.ts b/apps/sim/stores/workflow-diff/store.ts index 61dfc3744c..6a9e83f535 100644 --- a/apps/sim/stores/workflow-diff/store.ts +++ b/apps/sim/stores/workflow-diff/store.ts @@ -8,6 +8,7 @@ import { useWorkflowRegistry } from '../workflows/registry/store' import { useSubBlockStore } from '../workflows/subblock/store' import { useWorkflowStore } from '../workflows/workflow/store' import type { WorkflowState } from '../workflows/workflow/types' +import { validateWorkflowState } from '@/lib/workflows/validation' const logger = createLogger('WorkflowDiffStore') @@ -293,6 +294,36 @@ export const useWorkflowDiffStore = create 0) { + logger.warn('Workflow validation warnings during diff acceptance', { + warnings: validation.warnings, + }) + } + // Immediately flag diffAccepted on stats if we can (early upsert with minimal fields) try { const { useCopilotStore } = await import('@/stores/copilot/store') @@ -313,19 +344,19 @@ export const useWorkflowDiffStore = create> = {} - Object.entries(cleanState.blocks).forEach(([blockId, block]) => { + Object.entries(stateToApply.blocks).forEach(([blockId, block]) => { subblockValues[blockId] = {} Object.entries(block.subBlocks || {}).forEach(([subblockId, subblock]) => { - subblockValues[blockId][subblockId] = (subblock as any).value + subblockValues[blockId][subblockId] = subblock.value }) }) diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index 50fbc7aa55..dd3ebcdb0a 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -130,131 +130,143 @@ export function getToolParametersConfig( toolId: string, blockType?: string ): ToolWithParameters | null { - const toolConfig = getTool(toolId) - if (!toolConfig) { - return null - } + try { + const toolConfig = getTool(toolId) + if (!toolConfig) { + console.warn(`Tool not found: ${toolId}`) + return null + } - // Get block configuration for UI component information - let blockConfig: BlockConfig | null = null - if (blockType) { - const blockConfigs = getBlockConfigurations() - blockConfig = blockConfigs[blockType] || null - } + // Validate that toolConfig has required properties + if (!toolConfig.params || typeof toolConfig.params !== 'object') { + console.warn(`Tool ${toolId} has invalid params configuration`) + return null + } - // Convert tool params to our standard format with UI component info - const allParameters: ToolParameterConfig[] = Object.entries(toolConfig.params).map( - ([paramId, param]) => { - const toolParam: ToolParameterConfig = { - id: paramId, - type: param.type, - required: param.required ?? false, - visibility: param.visibility ?? (param.required ? 'user-or-llm' : 'user-only'), - description: param.description, - default: param.default, - } + // Get block configuration for UI component information + let blockConfig: BlockConfig | null = null + if (blockType) { + const blockConfigs = getBlockConfigurations() + blockConfig = blockConfigs[blockType] || null + } - // Add UI component information from block config if available - if (blockConfig) { - // For multi-operation tools, find the subblock that matches both the parameter ID - // and the current tool operation - let subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => { - if (sb.id !== paramId) return false - - // If there's a condition, check if it matches the current tool - if (sb.condition && sb.condition.field === 'operation') { - // First try exact match with full tool ID - if (sb.condition.value === toolId) return true - - // Then try extracting operation from tool ID - // For tools like 'google_calendar_quick_add', extract 'quick_add' - const parts = toolId.split('_') - if (parts.length >= 3) { - // Join everything after the provider prefix (e.g., 'google_calendar_') - const operation = parts.slice(2).join('_') - if (sb.condition.value === operation) return true - } + // Convert tool params to our standard format with UI component info + const allParameters: ToolParameterConfig[] = Object.entries(toolConfig.params).map( + ([paramId, param]) => { + const toolParam: ToolParameterConfig = { + id: paramId, + type: param.type, + required: param.required ?? false, + visibility: param.visibility ?? (param.required ? 'user-or-llm' : 'user-only'), + description: param.description, + default: param.default, + } - // Fallback to last part only - const operation = parts[parts.length - 1] - return sb.condition.value === operation - } + // Add UI component information from block config if available + if (blockConfig) { + // For multi-operation tools, find the subblock that matches both the parameter ID + // and the current tool operation + let subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => { + if (sb.id !== paramId) return false + + // If there's a condition, check if it matches the current tool + if (sb.condition && sb.condition.field === 'operation') { + // First try exact match with full tool ID + if (sb.condition.value === toolId) return true + + // Then try extracting operation from tool ID + // For tools like 'google_calendar_quick_add', extract 'quick_add' + const parts = toolId.split('_') + if (parts.length >= 3) { + // Join everything after the provider prefix (e.g., 'google_calendar_') + const operation = parts.slice(2).join('_') + if (sb.condition.value === operation) return true + } + + // Fallback to last part only + const operation = parts[parts.length - 1] + return sb.condition.value === operation + } - // If no condition, it's a global parameter (like apiKey) - return !sb.condition - }) + // If no condition, it's a global parameter (like apiKey) + return !sb.condition + }) - // Fallback: if no operation-specific match, find any matching parameter - if (!subBlock) { - subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => sb.id === paramId) - } + // Fallback: if no operation-specific match, find any matching parameter + if (!subBlock) { + subBlock = blockConfig.subBlocks?.find((sb: SubBlockConfig) => sb.id === paramId) + } - // Special case: Check if this boolean parameter is part of a checkbox-list - if (!subBlock && param.type === 'boolean' && blockConfig) { - // Look for a checkbox-list that includes this parameter as an option - const checkboxListBlock = blockConfig.subBlocks?.find( - (sb: SubBlockConfig) => - sb.type === 'checkbox-list' && - Array.isArray(sb.options) && - sb.options.some((opt: any) => opt.id === paramId) - ) - - if (checkboxListBlock) { - subBlock = checkboxListBlock + // Special case: Check if this boolean parameter is part of a checkbox-list + if (!subBlock && param.type === 'boolean' && blockConfig) { + // Look for a checkbox-list that includes this parameter as an option + const checkboxListBlock = blockConfig.subBlocks?.find( + (sb: SubBlockConfig) => + sb.type === 'checkbox-list' && + Array.isArray(sb.options) && + sb.options.some((opt: any) => opt.id === paramId) + ) + + if (checkboxListBlock) { + subBlock = checkboxListBlock + } } - } - if (subBlock) { - toolParam.uiComponent = { - type: subBlock.type, - options: subBlock.options, - placeholder: subBlock.placeholder, - password: subBlock.password, - condition: subBlock.condition, - title: subBlock.title, - layout: subBlock.layout, - value: subBlock.value, - provider: subBlock.provider, - serviceId: subBlock.serviceId, - requiredScopes: subBlock.requiredScopes, - mimeType: subBlock.mimeType, - columns: subBlock.columns, - min: subBlock.min, - max: subBlock.max, - step: subBlock.step, - integer: subBlock.integer, - language: subBlock.language, - generationType: subBlock.generationType, - acceptedTypes: subBlock.acceptedTypes, - multiple: subBlock.multiple, - maxSize: subBlock.maxSize, + if (subBlock) { + toolParam.uiComponent = { + type: subBlock.type, + options: subBlock.options, + placeholder: subBlock.placeholder, + password: subBlock.password, + condition: subBlock.condition, + title: subBlock.title, + layout: subBlock.layout, + value: subBlock.value, + provider: subBlock.provider, + serviceId: subBlock.serviceId, + requiredScopes: subBlock.requiredScopes, + mimeType: subBlock.mimeType, + columns: subBlock.columns, + min: subBlock.min, + max: subBlock.max, + step: subBlock.step, + integer: subBlock.integer, + language: subBlock.language, + generationType: subBlock.generationType, + acceptedTypes: subBlock.acceptedTypes, + multiple: subBlock.multiple, + maxSize: subBlock.maxSize, + } } } - } - return toolParam - } - ) + return toolParam + } + ) - // Parameters that should be shown to the user for input - const userInputParameters = allParameters.filter( - (param) => param.visibility === 'user-or-llm' || param.visibility === 'user-only' - ) + // Parameters that should be shown to the user for input + const userInputParameters = allParameters.filter( + (param) => param.visibility === 'user-or-llm' || param.visibility === 'user-only' + ) - // Parameters that are required (must be filled by user or LLM) - const requiredParameters = allParameters.filter((param) => param.required) + // Parameters that are required (must be filled by user or LLM) + const requiredParameters = allParameters.filter((param) => param.required) - // Parameters that are optional but can be provided by user - const optionalParameters = allParameters.filter( - (param) => param.visibility === 'user-only' && !param.required - ) + // Parameters that are optional but can be provided by user + const optionalParameters = allParameters.filter( + (param) => param.visibility === 'user-only' && !param.required + ) - return { - toolConfig, - allParameters, - userInputParameters, - requiredParameters, - optionalParameters, + return { + toolConfig, + allParameters, + userInputParameters, + requiredParameters, + optionalParameters, + } + } catch (error) { + console.error('Error getting tool parameters config:', error) + return null } } From a574675b05b0b3e6fa3b675c67831432ea0e3dce Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 17:32:53 -0700 Subject: [PATCH 27/43] fix trigger dropdown --- apps/sim/components/ui/tag-dropdown.tsx | 82 ++++++++++++++++++++----- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/apps/sim/components/ui/tag-dropdown.tsx b/apps/sim/components/ui/tag-dropdown.tsx index 97ef575310..717ff4facf 100644 --- a/apps/sim/components/ui/tag-dropdown.tsx +++ b/apps/sim/components/ui/tag-dropdown.tsx @@ -102,7 +102,8 @@ const getOutputTypeForPath = ( block: BlockState, blockConfig: BlockConfig | null, blockId: string, - outputPath: string + outputPath: string, + mergedSubBlocksOverride?: Record ): string => { if (block?.triggerMode && blockConfig?.triggers?.enabled) { const triggerId = blockConfig?.triggers?.available?.[0] @@ -126,7 +127,8 @@ const getOutputTypeForPath = ( } } else if (block?.type === 'starter') { // Handle starter block specific outputs - const startWorkflowValue = getSubBlockValue(blockId, 'startWorkflow') + const startWorkflowValue = + mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow') if (startWorkflowValue === 'chat') { // Define types for chat mode outputs @@ -138,7 +140,8 @@ const getOutputTypeForPath = ( return chatModeTypes[outputPath] || 'any' } // For API mode, check inputFormat for custom field types - const inputFormatValue = getSubBlockValue(blockId, 'inputFormat') + const inputFormatValue = + mergedSubBlocksOverride?.inputFormat?.value ?? getSubBlockValue(blockId, 'inputFormat') if (inputFormatValue && Array.isArray(inputFormatValue)) { const field = inputFormatValue.find( (f: { name?: string; type?: string }) => f.name === outputPath @@ -150,7 +153,7 @@ const getOutputTypeForPath = ( } else if (blockConfig?.category === 'triggers') { // For trigger blocks, use the dynamic output helper const blockState = useWorkflowStore.getState().blocks[blockId] - const subBlocks = blockState?.subBlocks || {} + const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {}) return getBlockOutputType(block.type, outputPath, subBlocks) } else { const operationValue = getSubBlockValue(blockId, 'operation') @@ -303,6 +306,24 @@ export const TagDropdown: React.FC = ({ const edges = useWorkflowStore((state) => state.edges) const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId) + // Subscribe to live subblock values for the active workflow to react to input format changes + const workflowSubBlockValues = useSubBlockStore((state) => + workflowId ? (state.workflowValues[workflowId] ?? {}) : {} + ) + + const getMergedSubBlocks = useCallback( + (targetBlockId: string): Record => { + const base = blocks[targetBlockId]?.subBlocks || {} + const live = workflowSubBlockValues?.[targetBlockId] || {} + const merged: Record = { ...base } + for (const [subId, liveVal] of Object.entries(live)) { + merged[subId] = { ...(base[subId] || {}), value: liveVal } + } + return merged + }, + [blocks, workflowSubBlockValues] + ) + const getVariablesByWorkflowId = useVariablesStore((state) => state.getVariablesByWorkflowId) const variables = useVariablesStore((state) => state.variables) const workflowVariables = workflowId ? getVariablesByWorkflowId(workflowId) : [] @@ -361,7 +382,8 @@ export const TagDropdown: React.FC = ({ const blockName = sourceBlock.name || sourceBlock.type const normalizedBlockName = normalizeBlockName(blockName) - const responseFormatValue = getSubBlockValue(activeSourceBlockId, 'responseFormat') + const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId) + const responseFormatValue = mergedSubBlocks?.responseFormat?.value const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId) let blockTags: string[] @@ -388,7 +410,7 @@ export const TagDropdown: React.FC = ({ } } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { if (sourceBlock.type === 'starter') { - const startWorkflowValue = getSubBlockValue(activeSourceBlockId, 'startWorkflow') + const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value if (startWorkflowValue === 'chat') { // For chat mode, provide input, conversationId, and files @@ -398,7 +420,7 @@ export const TagDropdown: React.FC = ({ `${normalizedBlockName}.files`, ] } else { - const inputFormatValue = getSubBlockValue(activeSourceBlockId, 'inputFormat') + const inputFormatValue = mergedSubBlocks?.inputFormat?.value if ( inputFormatValue && @@ -416,7 +438,17 @@ export const TagDropdown: React.FC = ({ blockTags = [normalizedBlockName] } } else { - if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) { + // For triggers and starter blocks, use dynamic outputs based on live subblock values + if (blockConfig.category === 'triggers' || sourceBlock.type === 'starter') { + const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) + if (dynamicOutputs.length > 0) { + blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) + } else if (sourceBlock.type === 'starter') { + blockTags = [normalizedBlockName] + } else { + blockTags = [] + } + } else if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) { const triggerId = blockConfig?.triggers?.available?.[0] const firstTrigger = triggerId ? getTrigger(triggerId) @@ -432,7 +464,8 @@ export const TagDropdown: React.FC = ({ } } else { // Check for tool-specific outputs first - const operationValue = getSubBlockValue(activeSourceBlockId, 'operation') + const operationValue = + mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation') const toolOutputPaths = operationValue ? generateToolOutputPaths(blockConfig, operationValue) : [] @@ -631,21 +664,21 @@ export const TagDropdown: React.FC = ({ const blockName = accessibleBlock.name || accessibleBlock.type const normalizedBlockName = normalizeBlockName(blockName) - const responseFormatValue = getSubBlockValue(accessibleBlockId, 'responseFormat') + const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId) + const responseFormatValue = mergedSubBlocks?.responseFormat?.value const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId) let blockTags: string[] // For trigger blocks, use the dynamic output helper if (blockConfig.category === 'triggers' || accessibleBlock.type === 'starter') { - const subBlocks = blocks[accessibleBlockId]?.subBlocks || {} - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, subBlocks) + const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) if (dynamicOutputs.length > 0) { blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) } else if (accessibleBlock.type === 'starter') { // Legacy starter block fallback - const startWorkflowValue = getSubBlockValue(accessibleBlockId, 'startWorkflow') + const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value if (startWorkflowValue === 'chat') { blockTags = [ `${normalizedBlockName}.input`, @@ -698,7 +731,8 @@ export const TagDropdown: React.FC = ({ } } else { // Check for tool-specific outputs first - const operationValue = getSubBlockValue(accessibleBlockId, 'operation') + const operationValue = + mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation') const toolOutputPaths = operationValue ? generateToolOutputPaths(blockConfig, operationValue) : [] @@ -747,7 +781,17 @@ export const TagDropdown: React.FC = ({ variableInfoMap, blockTagGroups: finalBlockTagGroups, } - }, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables]) + }, [ + blocks, + edges, + loops, + parallels, + blockId, + activeSourceBlockId, + workflowVariables, + workflowSubBlockValues, + getMergedSubBlocks, + ]) const filteredTags = useMemo(() => { if (!searchTerm) return tags @@ -1329,12 +1373,14 @@ export const TagDropdown: React.FC = ({ ) if (block) { const blockConfig = getBlock(block.type) + const mergedSubBlocks = getMergedSubBlocks(group.blockId) tagDescription = getOutputTypeForPath( block, blockConfig || null, group.blockId, - outputPath + outputPath, + mergedSubBlocks ) } } @@ -1469,12 +1515,14 @@ export const TagDropdown: React.FC = ({ ) if (block) { const blockConfig = getBlock(block.type) + const mergedSubBlocks = getMergedSubBlocks(group.blockId) childType = getOutputTypeForPath( block, blockConfig || null, group.blockId, - childOutputPath + childOutputPath, + mergedSubBlocks ) } From 6dc7393d3426bb75d7760c0dd261c3997de18d0e Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 19:46:14 -0700 Subject: [PATCH 28/43] Copilot stuff (lots of it) --- apps/sim/app/api/copilot/chat/route.test.ts | 4 - apps/sim/app/api/copilot/chat/route.ts | 33 ++-- apps/sim/app/api/workflows/[id]/yaml/route.ts | 7 +- apps/sim/app/api/yaml/diff/create/route.ts | 4 +- .../copilot-message/copilot-message.tsx | 82 +++++----- .../components/user-input/user-input.tsx | 152 +++++++++--------- .../components/tool-input/tool-input.tsx | 16 +- apps/sim/lib/copilot/api.ts | 10 +- .../tools/client/blocks/get-trigger-blocks.ts | 2 +- .../tools/client/examples/get-examples-rag.ts | 13 +- .../client/examples/get-trigger-examples.ts | 17 +- .../tools/server/blocks/get-trigger-blocks.ts | 4 +- .../tools/server/workflow/build-workflow.ts | 12 +- .../tools/server/workflow/edit-workflow.ts | 4 +- apps/sim/lib/sim-agent/constants.ts | 2 +- apps/sim/lib/workflows/validation.ts | 12 +- apps/sim/stores/copilot/store.ts | 25 ++- apps/sim/stores/copilot/types.ts | 14 +- apps/sim/stores/workflow-diff/store.ts | 10 +- 19 files changed, 211 insertions(+), 212 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.test.ts b/apps/sim/app/api/copilot/chat/route.test.ts index 2a57ccb687..b8e63f4b9e 100644 --- a/apps/sim/app/api/copilot/chat/route.test.ts +++ b/apps/sim/app/api/copilot/chat/route.test.ts @@ -226,7 +226,6 @@ describe('Copilot Chat API Route', () => { streamToolCalls: true, mode: 'agent', messageId: 'mock-uuid-1234-5678', - depth: 0, chatId: 'chat-123', }), }) @@ -290,7 +289,6 @@ describe('Copilot Chat API Route', () => { streamToolCalls: true, mode: 'agent', messageId: 'mock-uuid-1234-5678', - depth: 0, chatId: 'chat-123', }), }) @@ -343,7 +341,6 @@ describe('Copilot Chat API Route', () => { streamToolCalls: true, mode: 'agent', messageId: 'mock-uuid-1234-5678', - depth: 0, chatId: 'chat-123', }), }) @@ -433,7 +430,6 @@ describe('Copilot Chat API Route', () => { streamToolCalls: true, mode: 'ask', messageId: 'mock-uuid-1234-5678', - depth: 0, chatId: 'chat-123', }), }) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index ace360ac49..229bdcc354 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -38,8 +38,20 @@ const ChatMessageSchema = z.object({ userMessageId: z.string().optional(), // ID from frontend for the user message chatId: z.string().optional(), workflowId: z.string().min(1, 'Workflow ID is required'), + model: z + .enum([ + 'gpt-5-fast', + 'gpt-5', + 'gpt-5-high', + 'gpt-4o', + 'gpt-4.1', + 'o3', + 'claude-4-sonnet', + 'claude-4.1-opus', + ]) + .optional() + .default('gpt-5'), mode: z.enum(['ask', 'agent']).optional().default('agent'), - depth: z.number().int().min(0).max(3).optional().default(0), prefetch: z.boolean().optional(), createNewChat: z.boolean().optional().default(false), stream: z.boolean().optional().default(true), @@ -97,8 +109,8 @@ export async function POST(req: NextRequest) { userMessageId, chatId, workflowId, + model, mode, - depth, prefetch, createNewChat, stream, @@ -147,19 +159,6 @@ export async function POST(req: NextRequest) { } } - // Consolidation mapping: map negative depths to base depth with prefetch=true - let effectiveDepth: number | undefined = typeof depth === 'number' ? depth : undefined - let effectivePrefetch: boolean | undefined = prefetch - if (typeof effectiveDepth === 'number') { - if (effectiveDepth === -2) { - effectiveDepth = 1 - effectivePrefetch = true - } else if (effectiveDepth === -1) { - effectiveDepth = 0 - effectivePrefetch = true - } - } - // Handle chat context let currentChat: any = null let conversationHistory: any[] = [] @@ -371,13 +370,13 @@ export async function POST(req: NextRequest) { userId: authenticatedUserId, stream: stream, streamToolCalls: true, + model: model, mode: mode, messageId: userMessageIdToUse, version: SIM_AGENT_VERSION, ...(providerConfig ? { provider: providerConfig } : {}), ...(effectiveConversationId ? { conversationId: effectiveConversationId } : {}), - ...(typeof effectiveDepth === 'number' ? { depth: effectiveDepth } : {}), - ...(typeof effectivePrefetch === 'boolean' ? { prefetch: effectivePrefetch } : {}), + ...(typeof prefetch === 'boolean' ? { prefetch: prefetch } : {}), ...(session?.user?.name && { userName: session.user.name }), ...(agentContexts.length > 0 && { context: agentContexts }), ...(actualChatId ? { chatId: actualChatId } : {}), diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index 675d55b3f1..76bb8ea0e1 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -12,8 +12,7 @@ import { loadWorkflowFromNormalizedTables, saveWorkflowToNormalizedTables, } from '@/lib/workflows/db-helpers' -import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation' -import { validateWorkflowState } from '@/lib/workflows/validation' +import { sanitizeAgentToolsInBlocks, validateWorkflowState } from '@/lib/workflows/validation' import { getUserId } from '@/app/api/auth/oauth/utils' import { getAllBlocks, getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' @@ -405,7 +404,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ // Validate the workflow state before persisting const validation = validateWorkflowState(workflowState, { sanitize: true }) - + if (!validation.valid) { logger.error(`[${requestId}] Workflow validation failed`, { errors: validation.errors, @@ -827,4 +826,4 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ { status: 500 } ) } -} \ No newline at end of file +} diff --git a/apps/sim/app/api/yaml/diff/create/route.ts b/apps/sim/app/api/yaml/diff/create/route.ts index beea8b1948..81148c6520 100644 --- a/apps/sim/app/api/yaml/diff/create/route.ts +++ b/apps/sim/app/api/yaml/diff/create/route.ts @@ -4,6 +4,7 @@ import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent' import { generateRequestId } from '@/lib/utils' +import { validateWorkflowState } from '@/lib/workflows/validation' import { getAllBlocks } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { resolveOutputType } from '@/blocks/utils' @@ -15,7 +16,6 @@ import { generateLoopBlocks, generateParallelBlocks, } from '@/stores/workflows/workflow/utils' -import { validateWorkflowState } from '@/lib/workflows/validation' const logger = createLogger('YamlDiffCreateAPI') @@ -263,7 +263,7 @@ export async function POST(request: NextRequest) { // Validate the proposed workflow state const validation = validateWorkflowState(result.diff.proposedState, { sanitize: true }) - + if (!validation.valid) { logger.error(`[${requestId}] Proposed workflow state validation failed`, { errors: validation.errors, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx index 426913eb76..7d2846af81 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx @@ -456,46 +456,6 @@ const CopilotMessage: FC = memo( ) : null}
- {hasCheckpoints && ( -
- {showRestoreConfirmation ? ( -
- - -
- ) : ( - - )} -
- )}
{/* Message content in purple box */}
= memo( })()}
+ {hasCheckpoints && ( +
+ {showRestoreConfirmation ? ( +
+ Restore Checkpoint? + + +
+ ) : ( + + )} +
+ )}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index 7fb6402018..43e20b10e1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -51,7 +51,6 @@ import { import { useSession } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' -import { CopilotSlider } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/components/copilot-slider' import { useCopilotStore } from '@/stores/copilot/store' import type { ChatContext } from '@/stores/copilot/types' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -1754,55 +1753,48 @@ const UserInput = forwardRef( return 'Agent' } - // Depth toggle state comes from global store; access via useCopilotStore - const { agentDepth, agentPrefetch, setAgentDepth, setAgentPrefetch } = useCopilotStore() - - // Ensure MAX mode is off for Fast and Balanced depths - useEffect(() => { - if (agentDepth < 2 && !agentPrefetch) { - setAgentPrefetch(true) - } - }, [agentDepth, agentPrefetch, setAgentPrefetch]) - - const cycleDepth = () => { - // 8 modes: depths 0-3, each with prefetch off/on. Cycle depth, then toggle prefetch when wrapping. - const nextDepth = agentDepth === 3 ? 0 : ((agentDepth + 1) as 0 | 1 | 2 | 3) - if (nextDepth === 0 && agentDepth === 3) { - setAgentPrefetch(!agentPrefetch) - } - setAgentDepth(nextDepth) - } + // Model selection state comes from global store; access via useCopilotStore + const { selectedModel, agentPrefetch, setSelectedModel, setAgentPrefetch } = useCopilotStore() + + // Model configurations with their display names + const modelOptions = [ + { value: 'gpt-5-fast', label: 'GPT-5 Fast' }, + { value: 'gpt-5', label: 'GPT-5' }, + { value: 'gpt-5-high', label: 'GPT-5 High' }, + { value: 'gpt-4o', label: 'GPT-4o' }, + { value: 'gpt-4.1', label: 'GPT-4.1' }, + { value: 'o3', label: 'o3' }, + { value: 'claude-4-sonnet', label: 'Claude 4 Sonnet' }, + { value: 'claude-4.1-opus', label: 'Claude 4.1 Opus' }, + ] as const const getCollapsedModeLabel = () => { - const base = getDepthLabelFor(agentDepth) - return !agentPrefetch ? `${base} MAX` : base + const model = modelOptions.find((m) => m.value === selectedModel) + return model ? model.label : 'GPT-5 Default' } - const getDepthLabelFor = (value: 0 | 1 | 2 | 3) => { - return value === 0 ? 'Fast' : value === 1 ? 'Balanced' : value === 2 ? 'Advanced' : 'Behemoth' - } - - // Removed descriptive suffixes; concise labels only - const getDepthDescription = (value: 0 | 1 | 2 | 3) => { - if (value === 0) - return 'Fastest and cheapest. Good for small edits, simple workflows, and small tasks' - if (value === 1) return 'Balances speed and reasoning. Good fit for most tasks' - if (value === 2) - return 'More reasoning for larger workflows and complex edits, still balanced for speed' - return 'Maximum reasoning power. Best for complex workflow building and debugging' - } - - const getDepthIconFor = (value: 0 | 1 | 2 | 3) => { + const getModelIcon = () => { const colorClass = !agentPrefetch ? 'text-[var(--brand-primary-hover-hex)]' : 'text-muted-foreground' - if (value === 0) return - if (value === 1) return - if (value === 2) return - return - } - const getDepthIcon = () => getDepthIconFor(agentDepth) + switch (selectedModel) { + case 'gpt-5-fast': + return + case 'gpt-5': + return + case 'gpt-5-high': + case 'gpt-4o': + case 'claude-4-sonnet': + return + case 'gpt-4.1': + case 'o3': + case 'claude-4.1-opus': + return + default: + return + } + } const scrollActiveItemIntoView = (index: number) => { const container = menuListRef.current @@ -3057,7 +3049,7 @@ const UserInput = forwardRef( {getModeText()} - +
@@ -3131,14 +3123,14 @@ const UserInput = forwardRef( )} title='Choose mode' > - {getDepthIcon()} + {getModelIcon()} {getCollapsedModeLabel()} - + -
-
+
+
MAX mode @@ -3167,50 +3159,56 @@ const UserInput = forwardRef(
{ - if (agentDepth < 2) return + if (['gpt-5-fast', 'gpt-5'].includes(selectedModel)) return setAgentPrefetch(!checked) }} />
-
+
-
-
- Mode -
- {getDepthIconFor(agentDepth)} - - {getDepthLabelFor(agentDepth)} - -
+
+
+ Model
-
- - setAgentDepth((val?.[0] ?? 0) as 0 | 1 | 2 | 3) +
+ {modelOptions.map((option) => { + const getIcon = () => { + if (['gpt-5-high', 'claude-4.1-opus'].includes(option.value)) { + return + } + if (['gpt-5', 'o3', 'claude-4-sonnet'].includes(option.value)) { + return + } + if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(option.value)) { + return + } + return
} - /> -
-
-
-
+ + return ( + setSelectedModel(option.value)} + className={cn( + 'flex h-7 items-center gap-1.5 px-2 py-1 text-left text-xs', + selectedModel === option.value ? 'bg-muted/50' : '' + )} + > + {getIcon()} + {option.label} + + ) + })}
-
- {getDepthDescription(agentDepth)} -
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx index 5ec6850f7f..496d21216b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useState } from 'react' -import { PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react' +import React, { useCallback, useEffect, useState } from 'react' +import { AlertCircle, PlusIcon, Server, WrenchIcon, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' @@ -51,8 +51,6 @@ import { isPasswordParameter, type ToolParameterConfig, } from '@/tools/params' -import { AlertCircle } from 'lucide-react' -import React from 'react' const logger = createLogger('ToolInput') @@ -397,12 +395,12 @@ class ToolInputErrorBoundary extends React.Component< render() { if (this.state.hasError) { return ( -
-
- - Tool Configuration Error +
+
+ + Tool Configuration Error
-

+

{this.props.blockName ? `Block "${this.props.blockName}": ` : ''} Invalid tool reference. Please check the workflow configuration.

diff --git a/apps/sim/lib/copilot/api.ts b/apps/sim/lib/copilot/api.ts index 3b55729b6a..80f9e24ed4 100644 --- a/apps/sim/lib/copilot/api.ts +++ b/apps/sim/lib/copilot/api.ts @@ -57,7 +57,15 @@ export interface SendMessageRequest { chatId?: string workflowId?: string mode?: 'ask' | 'agent' - depth?: 0 | 1 | 2 | 3 + model?: + | 'gpt-5-fast' + | 'gpt-5' + | 'gpt-5-high' + | 'gpt-4o' + | 'gpt-4.1' + | 'o3' + | 'claude-4-sonnet' + | 'claude-4.1-opus' prefetch?: boolean createNewChat?: boolean stream?: boolean diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts index 3e9e8434b1..23f7066562 100644 --- a/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts +++ b/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts @@ -61,4 +61,4 @@ export class GetTriggerBlocksClientTool extends BaseClientTool { this.setState(ClientToolCallState.error) } } -} \ No newline at end of file +} diff --git a/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts b/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts index 26ed518f0a..ccc5db9c63 100644 --- a/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts +++ b/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts @@ -1,11 +1,10 @@ -import { Blocks, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { Loader2, MinusCircle, Search, XCircle } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' - export class GetExamplesRagClientTool extends BaseClientTool { static readonly id = 'get_examples_rag' @@ -15,11 +14,11 @@ export class GetExamplesRagClientTool extends BaseClientTool { static readonly metadata: BaseClientToolMetadata = { displayNames: { - [ClientToolCallState.generating]: { text: 'Getting examples', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting examples', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting examples', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Got examples', icon: Blocks }, - [ClientToolCallState.error]: { text: 'Failed to get examples', icon: XCircle }, + [ClientToolCallState.generating]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Fetched examples', icon: Search }, + [ClientToolCallState.error]: { text: 'Failed to fetch examples', icon: XCircle }, [ClientToolCallState.aborted]: { text: 'Aborted getting examples', icon: MinusCircle }, [ClientToolCallState.rejected]: { text: 'Skipped getting examples', icon: MinusCircle }, }, diff --git a/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts b/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts index 1fde2c12a3..f24ea48017 100644 --- a/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts +++ b/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts @@ -1,11 +1,10 @@ -import { Blocks, Loader2, MinusCircle, XCircle } from 'lucide-react' +import { Loader2, MinusCircle, XCircle, Zap } from 'lucide-react' import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState, } from '@/lib/copilot/tools/client/base-tool' - export class GetTriggerExamplesClientTool extends BaseClientTool { static readonly id = 'get_trigger_examples' @@ -15,13 +14,13 @@ export class GetTriggerExamplesClientTool extends BaseClientTool { static readonly metadata: BaseClientToolMetadata = { displayNames: { - [ClientToolCallState.generating]: { text: 'Getting trigger examples', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting trigger examples', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting trigger examples', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Got trigger examples', icon: Blocks }, - [ClientToolCallState.error]: { text: 'Failed to get trigger examples', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting trigger examples', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped getting trigger examples', icon: MinusCircle }, + [ClientToolCallState.generating]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Selected a trigger', icon: Zap }, + [ClientToolCallState.error]: { text: 'Failed to select a trigger', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted selecting a trigger', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped selecting a trigger', icon: MinusCircle }, }, interrupt: undefined, } diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts index d9acd4ee8d..a7cb0f3861 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts @@ -1,5 +1,5 @@ -import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { z } from 'zod' +import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { createLogger } from '@/lib/logs/console/logger' import { registry as blockRegistry } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' @@ -45,4 +45,4 @@ export const getTriggerBlocksServerTool: BaseServerTool< logger.debug(`Found ${triggerBlockIds.length} trigger blocks`) return GetTriggerBlocksResult.parse({ triggerBlockIds }) }, -} \ No newline at end of file +} diff --git a/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts index 8fda7bce6f..112112d55d 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/build-workflow.ts @@ -1,13 +1,13 @@ +import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import type { BuildWorkflowInput, BuildWorkflowResult } from '@/lib/copilot/tools/shared/schemas' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent' +import { validateWorkflowState } from '@/lib/workflows/validation' import { getAllBlocks } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { resolveOutputType } from '@/blocks/utils' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' -import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' -import { BuildWorkflowInput, BuildWorkflowResult } from '@/lib/copilot/tools/shared/schemas' -import { validateWorkflowState } from '@/lib/workflows/validation' const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT @@ -71,16 +71,14 @@ export const buildWorkflowServerTool: BaseServerTool< errors: conversionResult.errors, warnings: conversionResult.warnings, }) - throw new Error( - conversionResult.errors?.join(', ') || 'Failed to convert YAML to workflow' - ) + throw new Error(conversionResult.errors?.join(', ') || 'Failed to convert YAML to workflow') } const workflowState = conversionResult.workflowState // Validate the workflow state before returning const validation = validateWorkflowState(workflowState, { sanitize: true }) - + if (!validation.valid) { logger.error('Generated workflow state is invalid', { errors: validation.errors, diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 2c343fbe66..4e336c6fcb 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -6,11 +6,11 @@ import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' +import { validateWorkflowState } from '@/lib/workflows/validation' import { getAllBlocks } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { resolveOutputType } from '@/blocks/utils' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' -import { validateWorkflowState } from '@/lib/workflows/validation' interface EditWorkflowOperation { operation_type: 'add' | 'edit' | 'delete' @@ -285,7 +285,7 @@ export const editWorkflowServerTool: BaseServerTool = { // Validate the workflow state const validation = validateWorkflowState(validationResult.workflowState, { sanitize: true }) - + if (!validation.valid) { logger.error('Edited workflow state is invalid', { errors: validation.errors, diff --git a/apps/sim/lib/sim-agent/constants.ts b/apps/sim/lib/sim-agent/constants.ts index dc705dba5b..594ed64f9d 100644 --- a/apps/sim/lib/sim-agent/constants.ts +++ b/apps/sim/lib/sim-agent/constants.ts @@ -1,2 +1,2 @@ export const SIM_AGENT_API_URL_DEFAULT = 'https://agent.sim.ai' -export const SIM_AGENT_VERSION = '1.0.0' \ No newline at end of file +export const SIM_AGENT_VERSION = '1.0.0' diff --git a/apps/sim/lib/workflows/validation.ts b/apps/sim/lib/workflows/validation.ts index 818edea238..f92fc16c40 100644 --- a/apps/sim/lib/workflows/validation.ts +++ b/apps/sim/lib/workflows/validation.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console/logger' +import { getBlock } from '@/blocks/registry' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { getTool } from '@/tools/utils' -import { getBlock } from '@/blocks/registry' const logger = createLogger('WorkflowValidation') @@ -167,7 +167,7 @@ export function validateWorkflowState( // For API and generic blocks, the tool is determined by the block's tool configuration // In the workflow state, we need to check if the block type has valid tool access const blockConfig = getBlock(block.type) - if (blockConfig && blockConfig.tools?.access) { + if (blockConfig?.tools?.access) { // API block has static tool access const toolIds = blockConfig.tools.access for (const toolId of toolIds) { @@ -203,7 +203,7 @@ export function validateWorkflowState( const blockIds = new Set(Object.keys(sanitizedBlocks)) const loopIds = new Set(Object.keys(workflowState.loops || {})) const parallelIds = new Set(Object.keys(workflowState.parallels || {})) - + for (const edge of workflowState.edges) { if (!edge || typeof edge !== 'object') { errors.push('Invalid edge structure') @@ -211,8 +211,10 @@ export function validateWorkflowState( } // Check if source and target exist - const sourceExists = blockIds.has(edge.source) || loopIds.has(edge.source) || parallelIds.has(edge.source) - const targetExists = blockIds.has(edge.target) || loopIds.has(edge.target) || parallelIds.has(edge.target) + const sourceExists = + blockIds.has(edge.source) || loopIds.has(edge.source) || parallelIds.has(edge.source) + const targetExists = + blockIds.has(edge.target) || loopIds.has(edge.target) || parallelIds.has(edge.target) if (!sourceExists) { errors.push(`Edge references non-existent source block '${edge.source}'`) diff --git a/apps/sim/stores/copilot/store.ts b/apps/sim/stores/copilot/store.ts index 2c68c34a66..a3fea5c4f4 100644 --- a/apps/sim/stores/copilot/store.ts +++ b/apps/sim/stores/copilot/store.ts @@ -11,8 +11,8 @@ import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' import { GetBlocksAndToolsClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-and-tools' import { GetBlocksMetadataClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-metadata' import { GetTriggerBlocksClientTool } from '@/lib/copilot/tools/client/blocks/get-trigger-blocks' -import { GetTriggerExamplesClientTool } from '@/lib/copilot/tools/client/examples/get-trigger-examples' import { GetExamplesRagClientTool } from '@/lib/copilot/tools/client/examples/get-examples-rag' +import { GetTriggerExamplesClientTool } from '@/lib/copilot/tools/client/examples/get-trigger-examples' import { ListGDriveFilesClientTool } from '@/lib/copilot/tools/client/gdrive/list-files' import { ReadGDriveFileClientTool } from '@/lib/copilot/tools/client/gdrive/read-file' import { GDriveRequestAccessClientTool } from '@/lib/copilot/tools/client/google/gdrive-request-access' @@ -1275,8 +1275,9 @@ async function* parseSSEStream( // Initial state (subset required for UI/streaming) const initialState = { mode: 'agent' as const, - agentDepth: 1 as 0 | 1 | 2 | 3, + selectedModel: 'gpt-5' as CopilotStore['selectedModel'], agentPrefetch: true, + isCollapsed: false, currentChat: null as CopilotChat | null, chats: [] as CopilotChat[], messages: [] as CopilotMessage[], @@ -1331,7 +1332,7 @@ export const useCopilotStore = create()( ...initialState, workflowId, mode: get().mode, - agentDepth: get().agentDepth, + selectedModel: get().selectedModel, agentPrefetch: get().agentPrefetch, }) }, @@ -1548,16 +1549,9 @@ export const useCopilotStore = create()( } const isFirstMessage = get().messages.length === 0 && !currentChat?.title - // Capture send-time meta for reliable stats - const sendDepth = get().agentDepth - const sendMaxEnabled = sendDepth >= 2 && !get().agentPrefetch set((state) => ({ messages: newMessages, currentUserMessageId: userMessage.id, - messageMetaById: { - ...(state.messageMetaById || {}), - [userMessage.id]: { depth: sendDepth, maxEnabled: sendMaxEnabled }, - }, })) if (isFirstMessage) { @@ -1592,7 +1586,7 @@ export const useCopilotStore = create()( chatId: currentChat?.id, workflowId, mode: mode === 'ask' ? 'ask' : 'agent', - depth: get().agentDepth, + model: get().selectedModel, prefetch: get().agentPrefetch, createNewChat: !currentChat, stream, @@ -1629,8 +1623,7 @@ export const useCopilotStore = create()( errorContent = '_Please upgrade to the latest version of the Sim platform to continue using the copilot._' } else if (result.status === 429) { - errorContent = - '_Provider rate limit exceeded. Please try again later._' + errorContent = '_Provider rate limit exceeded. Please try again later._' } const errorMessage = createErrorMessage(streamingMessage.id, errorContent) @@ -1707,7 +1700,7 @@ export const useCopilotStore = create()( // Implicit feedback (send a continuation) - minimal sendImplicitFeedback: async (implicitFeedback: string) => { - const { workflowId, currentChat, mode, agentDepth } = get() + const { workflowId, currentChat, mode, selectedModel } = get() if (!workflowId) return const abortController = new AbortController() set({ isSendingMessage: true, error: null, abortController }) @@ -1719,7 +1712,7 @@ export const useCopilotStore = create()( chatId: currentChat?.id, workflowId, mode: mode === 'ask' ? 'ask' : 'agent', - depth: agentDepth, + model: selectedModel, prefetch: get().agentPrefetch, createNewChat: !currentChat, stream: true, @@ -2192,7 +2185,7 @@ export const useCopilotStore = create()( updateDiffStore: async (_yamlContent: string) => {}, updateDiffStoreWithWorkflowState: async (_workflowState: any) => {}, - setAgentDepth: (depth) => set({ agentDepth: depth }), + setSelectedModel: (model) => set({ selectedModel: model }), setAgentPrefetch: (prefetch) => set({ agentPrefetch: prefetch }), })) ) diff --git a/apps/sim/stores/copilot/types.ts b/apps/sim/stores/copilot/types.ts index 4b0ce2eb03..7022fefad3 100644 --- a/apps/sim/stores/copilot/types.ts +++ b/apps/sim/stores/copilot/types.ts @@ -68,8 +68,17 @@ export type CopilotMode = 'ask' | 'agent' export interface CopilotState { mode: CopilotMode - agentDepth: 0 | 1 | 2 | 3 + selectedModel: + | 'gpt-5-fast' + | 'gpt-5' + | 'gpt-5-high' + | 'gpt-4o' + | 'gpt-4.1' + | 'o3' + | 'claude-4-sonnet' + | 'claude-4.1-opus' agentPrefetch: boolean + isCollapsed: boolean currentChat: CopilotChat | null chats: CopilotChat[] @@ -112,12 +121,11 @@ export interface CopilotState { currentUserMessageId?: string | null // Per-message metadata captured at send-time for reliable stats - messageMetaById?: Record } export interface CopilotActions { setMode: (mode: CopilotMode) => void - setAgentDepth: (depth: 0 | 1 | 2 | 3) => void + setSelectedModel: (model: CopilotStore['selectedModel']) => void setAgentPrefetch: (prefetch: boolean) => void setWorkflowId: (workflowId: string | null) => Promise diff --git a/apps/sim/stores/workflow-diff/store.ts b/apps/sim/stores/workflow-diff/store.ts index 6a9e83f535..736ca6b571 100644 --- a/apps/sim/stores/workflow-diff/store.ts +++ b/apps/sim/stores/workflow-diff/store.ts @@ -3,12 +3,12 @@ import { devtools } from 'zustand/middleware' import { getClientTool } from '@/lib/copilot/tools/client/manager' import { createLogger } from '@/lib/logs/console/logger' import { type DiffAnalysis, WorkflowDiffEngine } from '@/lib/workflows/diff' +import { validateWorkflowState } from '@/lib/workflows/validation' import { Serializer } from '@/serializer' import { useWorkflowRegistry } from '../workflows/registry/store' import { useSubBlockStore } from '../workflows/subblock/store' import { useWorkflowStore } from '../workflows/workflow/store' import type { WorkflowState } from '../workflows/workflow/types' -import { validateWorkflowState } from '@/lib/workflows/validation' const logger = createLogger('WorkflowDiffStore') @@ -296,22 +296,22 @@ export const useWorkflowDiffStore = create Date: Mon, 22 Sep 2025 19:47:59 -0700 Subject: [PATCH 29/43] Temp update prod dns --- apps/sim/lib/sim-agent/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/sim-agent/constants.ts b/apps/sim/lib/sim-agent/constants.ts index 594ed64f9d..3857f195d9 100644 --- a/apps/sim/lib/sim-agent/constants.ts +++ b/apps/sim/lib/sim-agent/constants.ts @@ -1,2 +1,2 @@ -export const SIM_AGENT_API_URL_DEFAULT = 'https://agent.sim.ai' +export const SIM_AGENT_API_URL_DEFAULT = 'https://d2vaeznw6mw0n7.cloudfront.net' export const SIM_AGENT_VERSION = '1.0.0' From b737362fda71402ea3ba6fb16240e5097dc4fa47 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 20:34:18 -0700 Subject: [PATCH 30/43] fix trigger check --- apps/sim/serializer/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 37907303e5..379fa01b4a 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -418,7 +418,7 @@ export class Serializer { params: Record ) { // Skip validation if the block is in trigger mode - if (block.triggerMode || blockConfig.category === 'triggers') { + if (block.triggerMode || blockConfig.category === 'triggers' || params.triggerMode === true) { logger.info('Skipping validation for block in trigger mode', { blockId: block.id, blockType: block.type, From 4f28f90f27728163db2b7faa094a34892962afdb Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 21:06:01 -0700 Subject: [PATCH 31/43] fix --- apps/sim/serializer/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 379fa01b4a..ba5a65e2a5 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -417,8 +417,12 @@ export class Serializer { blockConfig: any, params: Record ) { - // Skip validation if the block is in trigger mode - if (block.triggerMode || blockConfig.category === 'triggers' || params.triggerMode === true) { + // Skip validation if the block is used as a trigger + if ( + block.triggerMode === true || + blockConfig.category === 'triggers' || + params.triggerMode === true + ) { logger.info('Skipping validation for block in trigger mode', { blockId: block.id, blockType: block.type, From b190edf25d95080036a4162bbc43fe259b2545df Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 21:08:32 -0700 Subject: [PATCH 32/43] fix trigger mode check --- apps/sim/executor/index.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index aad90f6a9e..324a05efb9 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -641,12 +641,18 @@ export class Executor { (block) => block.metadata?.id === BlockType.STARTER ) + // Check for any type of trigger block (dedicated triggers or trigger-mode blocks) const hasTriggerBlocks = this.actualWorkflow.blocks.some((block) => { - return block.metadata?.category === 'triggers' || block.config?.params?.triggerMode === true + // Check if it's a dedicated trigger block (category: 'triggers') + if (block.metadata?.category === 'triggers') return true + // Check if it's a block with trigger mode enabled + if (block.config?.params?.triggerMode === true) return true + return false }) if (hasTriggerBlocks) { - // When triggers exist, we allow execution without a starter block + // When triggers exist (either dedicated or trigger-mode), we allow execution without a starter block + // The actual start block will be determined at runtime based on the execution context } else { // Legacy workflows: require a valid starter block and basic connection checks if (!starterBlock || !starterBlock.enabled) { @@ -783,12 +789,14 @@ export class Executor { throw new Error('Child workflow has multiple Input Trigger blocks. Keep only one.') } } else { - // Parent workflows can use any trigger block + // Parent workflows can use any trigger block (dedicated or trigger-mode) const triggerBlocks = this.actualWorkflow.blocks.filter( (block) => block.metadata?.id === 'input_trigger' || block.metadata?.id === 'api_trigger' || - block.metadata?.id === 'chat_trigger' + block.metadata?.id === 'chat_trigger' || + block.metadata?.category === 'triggers' || + block.config?.params?.triggerMode === true ) if (triggerBlocks.length > 0) { initBlock = triggerBlocks[0] From 7b870609dde1fedbe50c7e623c3cda177108b9e5 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 21:08:36 -0700 Subject: [PATCH 33/43] Fix yaml imports --- apps/sim/app/api/copilot/chat/route.ts | 1 + apps/sim/app/api/workflows/[id]/yaml/route.ts | 56 +++++++++++++++---- .../components/user-input/user-input.tsx | 4 +- .../workflow-block/workflow-block.tsx | 33 +++++++++-- apps/sim/lib/copilot/api.ts | 1 + apps/sim/lib/environment.ts | 6 +- apps/sim/stores/copilot/types.ts | 1 + 7 files changed, 81 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 229bdcc354..0bc4d312a6 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -42,6 +42,7 @@ const ChatMessageSchema = z.object({ .enum([ 'gpt-5-fast', 'gpt-5', + 'gpt-5-medium', 'gpt-5-high', 'gpt-4o', 'gpt-4.1', diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index 76bb8ea0e1..749ebb02c8 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -250,15 +250,16 @@ function normalizeBlockStructure(blocks: Record): Record): Record + inputs?: Record + triggerMode?: boolean data?: Record parentId?: string extent?: string @@ -542,6 +550,23 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ }) } + // Handle blocks that have inputs instead of subBlocks (from YAML/copilot format) + // This is especially important for trigger configuration + if (block.inputs) { + Object.entries(block.inputs).forEach(([inputKey, inputValue]) => { + const matchingSubBlock = blockConfig.subBlocks.find((sb) => sb.id === inputKey) + if (!subBlocks[inputKey]) { + subBlocks[inputKey] = { + id: inputKey, + type: matchingSubBlock?.type || (inputKey === 'triggerConfig' ? 'trigger-config' : 'short-input'), + value: inputValue, + } + } else if (inputValue !== undefined) { + subBlocks[inputKey].value = inputValue + } + }) + } + // Set up outputs from block configuration const outputs = resolveOutputType(blockConfig.outputs) @@ -564,10 +589,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ isWide: false, advancedMode: false, height: 0, + triggerMode: block.triggerMode || false, // Preserve triggerMode from imported block data: blockData, } - logger.debug(`[${requestId}] Processed regular block: ${block.id} -> ${newId}`) + logger.debug(`[${requestId}] Processed regular block: ${block.id} -> ${newId}`, { + blockType: block.type, + hasTriggerMode: block.triggerMode, + hasInputs: !!block.inputs, + inputKeys: block.inputs ? Object.keys(block.inputs) : [], + subBlockKeys: Object.keys(subBlocks), + }) } else { logger.warn(`[${requestId}] Unknown block type: ${block.type}`) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index 43e20b10e1..b5bded75e9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -1760,6 +1760,7 @@ const UserInput = forwardRef( const modelOptions = [ { value: 'gpt-5-fast', label: 'GPT-5 Fast' }, { value: 'gpt-5', label: 'GPT-5' }, + { value: 'gpt-5-medium', label: 'GPT-5 Medium' }, { value: 'gpt-5-high', label: 'GPT-5 High' }, { value: 'gpt-4o', label: 'GPT-4o' }, { value: 'gpt-4.1', label: 'GPT-4.1' }, @@ -1783,6 +1784,7 @@ const UserInput = forwardRef( return case 'gpt-5': return + case 'gpt-5-medium': case 'gpt-5-high': case 'gpt-4o': case 'claude-4-sonnet': @@ -3184,7 +3186,7 @@ const UserInput = forwardRef( if (['gpt-5-high', 'claude-4.1-opus'].includes(option.value)) { return } - if (['gpt-5', 'o3', 'claude-4-sonnet'].includes(option.value)) { + if (['gpt-5', 'gpt-5-medium', 'o3', 'claude-4-sonnet'].includes(option.value)) { return } if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(option.value)) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 3800801b69..927c7eb526 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -141,11 +141,28 @@ export function WorkflowBlock({ id, data }: NodeProps) { isShowingDiff, id, ]) + // Always call hooks to maintain consistent hook order + const storeHorizontalHandles = useWorkflowStore((state) => state.blocks[id]?.horizontalHandles ?? true) + const storeIsWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) + const storeBlockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) + const storeBlockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) + const storeBlockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false) + + // Get block properties from currentWorkflow when in diff mode, otherwise from workflow store const horizontalHandles = data.isPreview ? (data.blockState?.horizontalHandles ?? true) // In preview mode, use blockState and default to horizontal - : useWorkflowStore((state) => state.blocks[id]?.horizontalHandles ?? true) // Changed default to true for consistency - const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) - const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) + : currentWorkflow.isDiffMode + ? (currentWorkflow.blocks[id]?.horizontalHandles ?? true) + : storeHorizontalHandles + + const isWide = currentWorkflow.isDiffMode + ? (currentWorkflow.blocks[id]?.isWide ?? false) + : storeIsWide + + const blockHeight = currentWorkflow.isDiffMode + ? (currentWorkflow.blocks[id]?.height ?? 0) + : storeBlockHeight + // Get per-block webhook status by checking if webhook is configured const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) @@ -157,8 +174,14 @@ export function WorkflowBlock({ id, data }: NodeProps) { ) const blockWebhookStatus = !!(hasWebhookProvider && hasWebhookPath) - const blockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) - const blockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false) + const blockAdvancedMode = currentWorkflow.isDiffMode + ? (currentWorkflow.blocks[id]?.advancedMode ?? false) + : storeBlockAdvancedMode + + // Get triggerMode from currentWorkflow blocks when in diff mode, otherwise from workflow store + const blockTriggerMode = currentWorkflow.isDiffMode + ? currentWorkflow.blocks[id]?.triggerMode ?? false + : storeBlockTriggerMode // Local UI state for diff mode controls const [diffIsWide, setDiffIsWide] = useState(isWide) diff --git a/apps/sim/lib/copilot/api.ts b/apps/sim/lib/copilot/api.ts index 80f9e24ed4..072d32f9fe 100644 --- a/apps/sim/lib/copilot/api.ts +++ b/apps/sim/lib/copilot/api.ts @@ -60,6 +60,7 @@ export interface SendMessageRequest { model?: | 'gpt-5-fast' | 'gpt-5' + | 'gpt-5-medium' | 'gpt-5-high' | 'gpt-4o' | 'gpt-4.1' diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index a74ab24eb8..3825d841ef 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -21,9 +21,9 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = - env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || - env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' +export const isHosted = true + // env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || + // env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled diff --git a/apps/sim/stores/copilot/types.ts b/apps/sim/stores/copilot/types.ts index 7022fefad3..de0de48150 100644 --- a/apps/sim/stores/copilot/types.ts +++ b/apps/sim/stores/copilot/types.ts @@ -71,6 +71,7 @@ export interface CopilotState { selectedModel: | 'gpt-5-fast' | 'gpt-5' + | 'gpt-5-medium' | 'gpt-5-high' | 'gpt-4o' | 'gpt-4.1' From 6b2aa92782477789ac8ba58bcec526918aa33a10 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 21:12:56 -0700 Subject: [PATCH 34/43] Fix autolayout error --- .../w/[workflowId]/utils/auto-layout.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts index fa0c44c93a..427738aaff 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout.ts @@ -206,6 +206,18 @@ export async function applyAutoLayoutAndUpdateStore( loops: newWorkflowState.loops || {}, parallels: newWorkflowState.parallels || {}, deploymentStatuses: newWorkflowState.deploymentStatuses || {}, + // Sanitize edges: remove null/empty handle fields to satisfy schema (optional strings) + edges: (newWorkflowState.edges || []).map((edge: any) => { + const { sourceHandle, targetHandle, ...rest } = edge || {} + const sanitized: any = { ...rest } + if (typeof sourceHandle === 'string' && sourceHandle.length > 0) { + sanitized.sourceHandle = sourceHandle + } + if (typeof targetHandle === 'string' && targetHandle.length > 0) { + sanitized.targetHandle = targetHandle + } + return sanitized + }), } // Save the updated workflow state to the database From 2beec0494d571f6f1845cc55147c254224ee5465 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 21:43:59 -0700 Subject: [PATCH 35/43] fix deployed chat --- apps/sim/app/api/chat/utils.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index cb21950d8b..4c1be78c88 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -431,9 +431,10 @@ export async function executeWorkflowForChat( (acc, [id, block]) => { const blockConfig = getBlock(block.type) const isTriggerBlock = blockConfig?.category === 'triggers' + const isChatTrigger = block.type === 'chat_trigger' - // Skip trigger blocks during chat execution - if (!isTriggerBlock) { + // Keep all non-trigger blocks and also keep the chat_trigger block + if (!isTriggerBlock || isChatTrigger) { acc[id] = block } return acc @@ -488,8 +489,10 @@ export async function executeWorkflowForChat( // Filter edges to exclude connections to/from trigger blocks (same as manual execution) const triggerBlockIds = Object.keys(mergedStates).filter((id) => { - const blockConfig = getBlock(mergedStates[id].type) - return blockConfig?.category === 'triggers' + const type = mergedStates[id].type + const blockConfig = getBlock(type) + // Exclude chat_trigger from the list so its edges are preserved + return blockConfig?.category === 'triggers' && type !== 'chat_trigger' }) const filteredEdges = edges.filter( From 0b2910c7d123ab08205deda7d02bb4dac0e0c079 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 22:07:44 -0700 Subject: [PATCH 36/43] Fix copilot input text overflow --- .../components/user-input/user-input.tsx | 1740 +++++++++-------- 1 file changed, 900 insertions(+), 840 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index b5bded75e9..c487b5404c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -121,6 +121,7 @@ const UserInput = forwardRef( const [isDragging, setIsDragging] = useState(false) const [dragCounter, setDragCounter] = useState(0) const textareaRef = useRef(null) + const overlayRef = useRef(null) const fileInputRef = useRef(null) const [showMentionMenu, setShowMentionMenu] = useState(false) const mentionMenuRef = useRef(null) @@ -318,15 +319,37 @@ const UserInput = forwardRef( // Auto-resize textarea and toggle vertical scroll when exceeding max height useEffect(() => { const textarea = textareaRef.current + const overlay = overlayRef.current if (textarea) { const maxHeight = 120 textarea.style.height = 'auto' const nextHeight = Math.min(textarea.scrollHeight, maxHeight) textarea.style.height = `${nextHeight}px` textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden' + + // Also update overlay height to match + if (overlay) { + overlay.style.height = `${nextHeight}px` + overlay.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden' + } } }, [message]) + // Sync scroll position between textarea and overlay + useEffect(() => { + const textarea = textareaRef.current + const overlay = overlayRef.current + + if (!textarea || !overlay) return + + const handleScroll = () => { + overlay.scrollTop = textarea.scrollTop + } + + textarea.addEventListener('scroll', handleScroll) + return () => textarea.removeEventListener('scroll', handleScroll) + }, []) + // Close mention menu on outside click useEffect(() => { if (!showMentionMenu) return @@ -1234,10 +1257,10 @@ const UserInput = forwardRef( setSubmenuQueryStart(getCaretPos()) void ensureWorkflowBlocksLoaded() } else if (openSubmenuFor === 'Workflow Blocks') { - const q = getSubmenuQuery().toLowerCase() - const filtered = workflowBlocks.filter((b) => - (b.name || b.id).toLowerCase().includes(q) - ) + const q = getSubmenuQuery().toLowerCase() + const filtered = workflowBlocks.filter((b) => + (b.name || b.id).toLowerCase().includes(q) + ) if (filtered.length > 0) { const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] @@ -1758,15 +1781,15 @@ const UserInput = forwardRef( // Model configurations with their display names const modelOptions = [ - { value: 'gpt-5-fast', label: 'GPT-5 Fast' }, - { value: 'gpt-5', label: 'GPT-5' }, - { value: 'gpt-5-medium', label: 'GPT-5 Medium' }, - { value: 'gpt-5-high', label: 'GPT-5 High' }, - { value: 'gpt-4o', label: 'GPT-4o' }, - { value: 'gpt-4.1', label: 'GPT-4.1' }, + { value: 'gpt-5-fast', label: 'gpt-5-fast' }, + { value: 'gpt-5', label: 'gpt-5' }, + { value: 'gpt-5-medium', label: 'gpt-5-medium' }, + { value: 'gpt-5-high', label: 'gpt-5-high' }, + { value: 'gpt-4o', label: 'gpt-4o' }, + { value: 'gpt-4.1', label: 'gpt-4.1' }, { value: 'o3', label: 'o3' }, - { value: 'claude-4-sonnet', label: 'Claude 4 Sonnet' }, - { value: 'claude-4.1-opus', label: 'Claude 4.1 Opus' }, + { value: 'claude-4-sonnet', label: 'claude-4-sonnet' }, + { value: 'claude-4.1-opus', label: 'claude-4.1-opus' }, ] as const const getCollapsedModeLabel = () => { @@ -1779,23 +1802,17 @@ const UserInput = forwardRef( ? 'text-[var(--brand-primary-hover-hex)]' : 'text-muted-foreground' - switch (selectedModel) { - case 'gpt-5-fast': - return - case 'gpt-5': - return - case 'gpt-5-medium': - case 'gpt-5-high': - case 'gpt-4o': - case 'claude-4-sonnet': - return - case 'gpt-4.1': - case 'o3': - case 'claude-4.1-opus': - return - default: - return + // Match the dropdown icon logic exactly + if (['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(selectedModel)) { + return + } + if (['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes(selectedModel)) { + return } + if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)) { + return + } + return } const scrollActiveItemIntoView = (index: number) => { @@ -2110,8 +2127,11 @@ const UserInput = forwardRef( {/* Textarea Field with overlay */}
{/* Highlight overlay */} -
-
+            
+
                 {(() => {
                   const elements: React.ReactNode[] = []
                   const remaining = message
@@ -2157,466 +2177,757 @@ const UserInput = forwardRef(
               placeholder={isDragging ? 'Drop files here...' : effectivePlaceholder}
               disabled={disabled}
               rows={1}
-              className='relative z-[2] mb-2 min-h-[32px] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent px-[2px] py-1 font-sans text-sm text-transparent leading-[1.25rem] caret-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
-              style={{ height: 'auto' }}
+              className='relative z-[2] mb-2 min-h-[32px] w-full resize-none overflow-y-auto overflow-x-hidden break-words border-0 bg-transparent px-[2px] py-1 font-sans text-sm text-transparent leading-[1.25rem] caret-foreground focus-visible:ring-0 focus-visible:ring-offset-0'
+              style={{ height: 'auto', wordBreak: 'break-word' }}
             />
 
             {showMentionMenu && (
               <>
                 
- {openSubmenuFor ? ( - <> -
- {openSubmenuFor === 'Chats' - ? 'Chats' - : openSubmenuFor === 'Workflows' - ? 'All workflows' - : openSubmenuFor === 'Knowledge' - ? 'Knowledge Bases' - : openSubmenuFor === 'Blocks' - ? 'Blocks' - : openSubmenuFor === 'Workflow Blocks' - ? 'Workflow Blocks' - : openSubmenuFor === 'Templates' - ? 'Templates' - : 'Logs'} -
-
- {isSubmenu('Chats') && ( - <> - {isLoadingPastChats ? ( -
- Loading... -
- ) : pastChats.length === 0 ? ( -
- No past chats -
- ) : ( - pastChats - .filter((c) => - (c.title || 'Untitled Chat') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((chat, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertPastChatMention(chat) - setSubmenuQueryStart(null) - }} - > -
- -
- - {chat.title || 'Untitled Chat'} - -
- )) - )} - - )} - {isSubmenu('Workflows') && ( - <> - {isLoadingWorkflows ? ( -
- Loading... -
- ) : workflows.length === 0 ? ( -
- No workflows -
- ) : ( - workflows - .filter((w) => - (w.name || 'Untitled Workflow') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((wf, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertWorkflowMention(wf) - setSubmenuQueryStart(null) - }} - > -
+ {openSubmenuFor ? ( + <> +
+ {openSubmenuFor === 'Chats' + ? 'Chats' + : openSubmenuFor === 'Workflows' + ? 'All workflows' + : openSubmenuFor === 'Knowledge' + ? 'Knowledge Bases' + : openSubmenuFor === 'Blocks' + ? 'Blocks' + : openSubmenuFor === 'Workflow Blocks' + ? 'Workflow Blocks' + : openSubmenuFor === 'Templates' + ? 'Templates' + : 'Logs'} +
+
+ {isSubmenu('Chats') && ( + <> + {isLoadingPastChats ? ( +
+ Loading... +
+ ) : pastChats.length === 0 ? ( +
+ No past chats +
+ ) : ( + pastChats + .filter((c) => + (c.title || 'Untitled Chat') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((chat, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertPastChatMention(chat) + setSubmenuQueryStart(null) + }} + > +
+ - - {wf.name || 'Untitled Workflow'} -
- )) - )} - - )} - {isSubmenu('Knowledge') && ( - <> - {isLoadingKnowledge ? ( -
- Loading... -
- ) : knowledgeBases.length === 0 ? ( -
- No knowledge bases -
- ) : ( - knowledgeBases - .filter((k) => - (k.name || 'Untitled') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((kb, idx) => ( + + {chat.title || 'Untitled Chat'} + +
+ )) + )} + + )} + {isSubmenu('Workflows') && ( + <> + {isLoadingWorkflows ? ( +
+ Loading... +
+ ) : workflows.length === 0 ? ( +
+ No workflows +
+ ) : ( + workflows + .filter((w) => + (w.name || 'Untitled Workflow') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((wf, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertWorkflowMention(wf) + setSubmenuQueryStart(null) + }} + >
setSubmenuActiveIndex(idx)} - onClick={() => { - insertKnowledgeMention(kb) - setSubmenuQueryStart(null) - }} + className='h-3.5 w-3.5 flex-shrink-0 rounded' + style={{ backgroundColor: wf.color || '#3972F6' }} + /> + + {wf.name || 'Untitled Workflow'} + +
+ )) + )} + + )} + {isSubmenu('Knowledge') && ( + <> + {isLoadingKnowledge ? ( +
+ Loading... +
+ ) : knowledgeBases.length === 0 ? ( +
+ No knowledge bases +
+ ) : ( + knowledgeBases + .filter((k) => + (k.name || 'Untitled') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((kb, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertKnowledgeMention(kb) + setSubmenuQueryStart(null) + }} + > + + {kb.name || 'Untitled'} +
+ )) + )} + + )} + {isSubmenu('Blocks') && ( + <> + {isLoadingBlocks ? ( +
+ Loading... +
+ ) : blocksList.length === 0 ? ( +
+ No blocks found +
+ ) : ( + blocksList + .filter((b) => + (b.name || b.id) + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((blk, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertBlockMention(blk) + setSubmenuQueryStart(null) + }} + > +
- - {kb.name || 'Untitled'} + {blk.iconComponent && ( + + )}
- )) - )} - - )} - {isSubmenu('Blocks') && ( - <> - {isLoadingBlocks ? ( -
- Loading... -
- ) : blocksList.length === 0 ? ( -
- No blocks found -
- ) : ( - blocksList - .filter((b) => - (b.name || b.id) - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((blk, idx) => ( + {blk.name || blk.id} +
+ )) + )} + + )} + {isSubmenu('Workflow Blocks') && ( + <> + {isLoadingWorkflowBlocks ? ( +
+ Loading... +
+ ) : workflowBlocks.length === 0 ? ( +
+ No blocks in this workflow +
+ ) : ( + workflowBlocks + .filter((b) => + (b.name || b.id) + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((blk, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertWorkflowBlockMention(blk) + setSubmenuQueryStart(null) + }} + >
setSubmenuActiveIndex(idx)} - onClick={() => { - insertBlockMention(blk) - setSubmenuQueryStart(null) - }} + className='relative flex h-4 w-4 items-center justify-center rounded-[3px]' + style={{ backgroundColor: blk.bgColor || '#6B7280' }} > -
- {blk.iconComponent && ( - - )} -
- {blk.name || blk.id} + {blk.iconComponent && ( + + )}
- )) - )} - - )} - {isSubmenu('Workflow Blocks') && ( - <> - {isLoadingWorkflowBlocks ? ( -
- Loading... -
- ) : workflowBlocks.length === 0 ? ( + {blk.name || blk.id} +
+ )) + )} + + )} + {isSubmenu('Templates') && ( + <> + {isLoadingTemplates ? ( +
+ Loading... +
+ ) : templatesList.length === 0 ? ( +
+ No templates found +
+ ) : ( + templatesList + .filter((t) => + (t.name || 'Untitled Template') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((tpl, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertTemplateMention(tpl) + setSubmenuQueryStart(null) + }} + > +
+ ★ +
+ {tpl.name} + + {tpl.stars} + +
+ )) + )} + + )} + {isSubmenu('Logs') && ( + <> + {isLoadingLogs ? ( +
+ Loading... +
+ ) : logsList.length === 0 ? ( +
+ No executions found +
+ ) : ( + logsList + .filter((l) => + [l.workflowName, l.trigger || ''] + .join(' ') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((log, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertLogMention(log) + setSubmenuQueryStart(null) + }} + > + {log.level === 'error' ? ( + + ) : ( + + )} + {log.workflowName} + · + + {formatTimestamp(log.createdAt)} + + · + + {(log.trigger || 'manual').toLowerCase()} + +
+ )) + )} + + )} +
+ + ) : ( + <> + {(() => { + const q = ( + getActiveMentionQueryAtPosition(getCaretPos())?.query || '' + ).toLowerCase() + const filtered = mentionOptions.filter((label) => + label.toLowerCase().includes(q) + ) + if (q.length > 0 && filtered.length === 0) { + // Aggregated search view + const aggregated = [ + ...workflowBlocks + .filter((b) => (b.name || b.id).toLowerCase().includes(q)) + .map((b) => ({ + type: 'Workflow Blocks' as const, + id: b.id, + value: b, + onClick: () => insertWorkflowBlockMention(b), + })), + ...workflows + .filter((w) => + (w.name || 'Untitled Workflow').toLowerCase().includes(q) + ) + .map((w) => ({ + type: 'Workflows' as const, + id: w.id, + value: w, + onClick: () => insertWorkflowMention(w), + })), + ...blocksList + .filter((b) => (b.name || b.id).toLowerCase().includes(q)) + .map((b) => ({ + type: 'Blocks' as const, + id: b.id, + value: b, + onClick: () => insertBlockMention(b), + })), + ...knowledgeBases + .filter((k) => (k.name || 'Untitled').toLowerCase().includes(q)) + .map((k) => ({ + type: 'Knowledge' as const, + id: k.id, + value: k, + onClick: () => insertKnowledgeMention(k), + })), + ...templatesList + .filter((t) => + (t.name || 'Untitled Template').toLowerCase().includes(q) + ) + .map((t) => ({ + type: 'Templates' as const, + id: t.id, + value: t, + onClick: () => insertTemplateMention(t), + })), + ...pastChats + .filter((c) => (c.title || 'Untitled Chat').toLowerCase().includes(q)) + .map((c) => ({ + type: 'Chats' as const, + id: c.id, + value: c, + onClick: () => insertPastChatMention(c), + })), + ...logsList + .filter((l) => + (l.workflowName || 'Untitled Workflow').toLowerCase().includes(q) + ) + .map((l) => ({ + type: 'Logs' as const, + id: l.id, + value: l, + onClick: () => insertLogMention(l), + })), + ] + return ( +
+ {aggregated.length === 0 ? (
- No blocks in this workflow + No matches
) : ( - workflowBlocks - .filter((b) => - (b.name || b.id) - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((blk, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertWorkflowBlockMention(blk) - setSubmenuQueryStart(null) - }} - > -
- {blk.iconComponent && ( - + aggregated.map((item, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => item.onClick()} + > + {item.type === 'Chats' ? ( + <> +
+ +
+ + {(item.value as any).title || 'Untitled Chat'} + + + ) : item.type === 'Workflows' ? ( + <> +
+ + {(item.value as any).name || 'Untitled Workflow'} + + + ) : item.type === 'Knowledge' ? ( + <> + + + {(item.value as any).name || 'Untitled'} + + + ) : item.type === 'Blocks' ? ( + <> +
+ {(() => { + const Icon = (item.value as any).iconComponent + return Icon ? ( + + ) : null + })()} +
+ + {(item.value as any).name || (item.value as any).id} + + + ) : item.type === 'Workflow Blocks' ? ( + <> +
+ {(() => { + const Icon = (item.value as any).iconComponent + return Icon ? ( + + ) : null + })()} +
+ + {(item.value as any).name || (item.value as any).id} + + + ) : item.type === 'Logs' ? ( + <> + {(() => { + const v = item.value as any + return v.level === 'error' ? ( + + ) : ( + + ) + })()} + + {(item.value as any).workflowName} + + · + + {formatTimestamp((item.value as any).createdAt)} + + · + + {( + ((item.value as any).trigger as string) || 'manual' + ).toLowerCase()} + + + ) : ( + <> +
+ ★ +
+ + {(item.value as any).name || 'Untitled Template'} + + {typeof (item.value as any).stars === 'number' && ( + + {(item.value as any).stars} + )} -
- {blk.name || blk.id} -
- )) + + )} +
+ )) )} - - )} - {isSubmenu('Templates') && ( - <> - {isLoadingTemplates ? ( -
- Loading... -
- ) : templatesList.length === 0 ? ( -
- No templates found +
+ ) + } + // Filtered top-level options view + return ( +
+ {filtered.map((label, idx) => ( +
{ + setInAggregated(false) + setMentionActiveIndex(idx) + }} + onClick={() => { + if (label === 'Chats') { + resetActiveMentionQuery() + setOpenSubmenuFor('Chats') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensurePastChatsLoaded() + } else if (label === 'Workflows') { + resetActiveMentionQuery() + setOpenSubmenuFor('Workflows') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureWorkflowsLoaded() + } else if (label === 'Knowledge') { + resetActiveMentionQuery() + setOpenSubmenuFor('Knowledge') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureKnowledgeLoaded() + } else if (label === 'Blocks') { + resetActiveMentionQuery() + setOpenSubmenuFor('Blocks') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureBlocksLoaded() + } else if (label === 'Workflow Blocks') { + resetActiveMentionQuery() + setOpenSubmenuFor('Workflow Blocks') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureWorkflowBlocksLoaded() + } else if (label === 'Docs') { + // No submenu; insert immediately + insertDocsMention() + } else if (label === 'Templates') { + resetActiveMentionQuery() + setOpenSubmenuFor('Templates') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureTemplatesLoaded() + } else if (label === 'Logs') { + resetActiveMentionQuery() + setOpenSubmenuFor('Logs') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureLogsLoaded() + } + }} + > +
+ {label === 'Chats' ? ( + + ) : label === 'Workflows' ? ( + + ) : label === 'Blocks' ? ( + + ) : label === 'Workflow Blocks' ? ( + + ) : label === 'Knowledge' ? ( + + ) : label === 'Docs' ? ( + + ) : label === 'Templates' ? ( + + ) : label === 'Logs' ? ( + + ) : ( +
+ )} + {label === 'Workflows' ? 'All workflows' : label}
- ) : ( - templatesList + {label !== 'Docs' && ( + + )} +
+ ))} + + {(() => { + const aq = ( + getActiveMentionQueryAtPosition(getCaretPos())?.query || '' + ).toLowerCase() + const filteredLen = mentionOptions.filter((label) => + label.toLowerCase().includes(aq) + ).length + const aggregated = [ + ...workflowBlocks + .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) + .map((b) => ({ type: 'Workflow Blocks' as const, value: b })), + ...workflows + .filter((w) => + (w.name || 'Untitled Workflow').toLowerCase().includes(aq) + ) + .map((w) => ({ type: 'Workflows' as const, value: w })), + ...blocksList + .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) + .map((b) => ({ type: 'Blocks' as const, value: b })), + ...knowledgeBases + .filter((k) => (k.name || 'Untitled').toLowerCase().includes(aq)) + .map((k) => ({ type: 'Knowledge' as const, value: k })), + ...templatesList .filter((t) => - (t.name || 'Untitled Template') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) + (t.name || 'Untitled Template').toLowerCase().includes(aq) ) - .map((tpl, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertTemplateMention(tpl) - setSubmenuQueryStart(null) - }} - > -
- ★ -
- {tpl.name} - - {tpl.stars} - -
- )) - )} - - )} - {isSubmenu('Logs') && ( - <> - {isLoadingLogs ? ( -
- Loading... -
- ) : logsList.length === 0 ? ( -
- No executions found -
- ) : ( - logsList + .map((t) => ({ type: 'Templates' as const, value: t })), + ...pastChats + .filter((c) => + (c.title || 'Untitled Chat').toLowerCase().includes(aq) + ) + .map((c) => ({ type: 'Chats' as const, value: c })), + ...logsList .filter((l) => - [l.workflowName, l.trigger || ''] - .join(' ') + (l.workflowName || 'Untitled Workflow') .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) + .includes(aq) ) - .map((log, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertLogMention(log) - setSubmenuQueryStart(null) - }} - > - {log.level === 'error' ? ( - - ) : ( - - )} - {log.workflowName} - · - - {formatTimestamp(log.createdAt)} - - · - - {(log.trigger || 'manual').toLowerCase()} - -
- )) - )} - - )} -
- - ) : ( - <> - {(() => { - const q = ( - getActiveMentionQueryAtPosition(getCaretPos())?.query || '' - ).toLowerCase() - const filtered = mentionOptions.filter((label) => - label.toLowerCase().includes(q) - ) - if (q.length > 0 && filtered.length === 0) { - // Aggregated search view - const aggregated = [ - ...workflowBlocks - .filter((b) => (b.name || b.id).toLowerCase().includes(q)) - .map((b) => ({ - type: 'Workflow Blocks' as const, - id: b.id, - value: b, - onClick: () => insertWorkflowBlockMention(b), - })), - ...workflows - .filter((w) => - (w.name || 'Untitled Workflow').toLowerCase().includes(q) - ) - .map((w) => ({ - type: 'Workflows' as const, - id: w.id, - value: w, - onClick: () => insertWorkflowMention(w), - })), - ...blocksList - .filter((b) => (b.name || b.id).toLowerCase().includes(q)) - .map((b) => ({ - type: 'Blocks' as const, - id: b.id, - value: b, - onClick: () => insertBlockMention(b), - })), - ...knowledgeBases - .filter((k) => (k.name || 'Untitled').toLowerCase().includes(q)) - .map((k) => ({ - type: 'Knowledge' as const, - id: k.id, - value: k, - onClick: () => insertKnowledgeMention(k), - })), - ...templatesList - .filter((t) => - (t.name || 'Untitled Template').toLowerCase().includes(q) - ) - .map((t) => ({ - type: 'Templates' as const, - id: t.id, - value: t, - onClick: () => insertTemplateMention(t), - })), - ...pastChats - .filter((c) => (c.title || 'Untitled Chat').toLowerCase().includes(q)) - .map((c) => ({ - type: 'Chats' as const, - id: c.id, - value: c, - onClick: () => insertPastChatMention(c), - })), - ...logsList - .filter((l) => - (l.workflowName || 'Untitled Workflow').toLowerCase().includes(q) - ) - .map((l) => ({ - type: 'Logs' as const, - id: l.id, - value: l, - onClick: () => insertLogMention(l), - })), - ] - return ( -
- {aggregated.length === 0 ? ( -
- No matches + .map((l) => ({ type: 'Logs' as const, value: l })), + ] + if (!aq || aq.length === 0 || aggregated.length === 0) return null + return ( + <> +
+
+ Matches
- ) : ( - aggregated.map((item, idx) => ( + {aggregated.map((item, idx) => (
setSubmenuActiveIndex(idx)} - onClick={() => item.onClick()} + aria-selected={inAggregated && submenuActiveIndex === idx} + onMouseEnter={() => { + setInAggregated(true) + setSubmenuActiveIndex(idx) + }} + onClick={() => { + if (item.type === 'Chats') + insertPastChatMention(item.value as any) + else if (item.type === 'Workflows') + insertWorkflowMention(item.value as any) + else if (item.type === 'Knowledge') + insertKnowledgeMention(item.value as any) + else if (item.type === 'Blocks') + insertBlockMention(item.value as any) + else if ((item as any).type === 'Workflow Blocks') + insertWorkflowBlockMention(item.value as any) + else if (item.type === 'Templates') + insertTemplateMention(item.value as any) + else if (item.type === 'Logs') + insertLogMention(item.value as any) + }} > {item.type === 'Chats' ? ( <> @@ -2635,7 +2946,8 @@ const UserInput = forwardRef(
@@ -2729,310 +3041,18 @@ const UserInput = forwardRef( )}
- )) - )} -
- ) - } - // Filtered top-level options view - return ( -
- {filtered.map((label, idx) => ( -
{ - setInAggregated(false) - setMentionActiveIndex(idx) - }} - onClick={() => { - if (label === 'Chats') { - resetActiveMentionQuery() - setOpenSubmenuFor('Chats') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensurePastChatsLoaded() - } else if (label === 'Workflows') { - resetActiveMentionQuery() - setOpenSubmenuFor('Workflows') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowsLoaded() - } else if (label === 'Knowledge') { - resetActiveMentionQuery() - setOpenSubmenuFor('Knowledge') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureKnowledgeLoaded() - } else if (label === 'Blocks') { - resetActiveMentionQuery() - setOpenSubmenuFor('Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureBlocksLoaded() - } else if (label === 'Workflow Blocks') { - resetActiveMentionQuery() - setOpenSubmenuFor('Workflow Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowBlocksLoaded() - } else if (label === 'Docs') { - // No submenu; insert immediately - insertDocsMention() - } else if (label === 'Templates') { - resetActiveMentionQuery() - setOpenSubmenuFor('Templates') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureTemplatesLoaded() - } else if (label === 'Logs') { - resetActiveMentionQuery() - setOpenSubmenuFor('Logs') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureLogsLoaded() - } - }} - > -
- {label === 'Chats' ? ( - - ) : label === 'Workflows' ? ( - - ) : label === 'Blocks' ? ( - - ) : label === 'Workflow Blocks' ? ( - - ) : label === 'Knowledge' ? ( - - ) : label === 'Docs' ? ( - - ) : label === 'Templates' ? ( - - ) : label === 'Logs' ? ( - - ) : ( -
- )} - {label === 'Workflows' ? 'All workflows' : label} -
- {label !== 'Docs' && ( - - )} -
- ))} - - {(() => { - const aq = ( - getActiveMentionQueryAtPosition(getCaretPos())?.query || '' - ).toLowerCase() - const filteredLen = mentionOptions.filter((label) => - label.toLowerCase().includes(aq) - ).length - const aggregated = [ - ...workflowBlocks - .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) - .map((b) => ({ type: 'Workflow Blocks' as const, value: b })), - ...workflows - .filter((w) => - (w.name || 'Untitled Workflow').toLowerCase().includes(aq) - ) - .map((w) => ({ type: 'Workflows' as const, value: w })), - ...blocksList - .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) - .map((b) => ({ type: 'Blocks' as const, value: b })), - ...knowledgeBases - .filter((k) => (k.name || 'Untitled').toLowerCase().includes(aq)) - .map((k) => ({ type: 'Knowledge' as const, value: k })), - ...templatesList - .filter((t) => - (t.name || 'Untitled Template').toLowerCase().includes(aq) - ) - .map((t) => ({ type: 'Templates' as const, value: t })), - ...pastChats - .filter((c) => - (c.title || 'Untitled Chat').toLowerCase().includes(aq) - ) - .map((c) => ({ type: 'Chats' as const, value: c })), - ...logsList - .filter((l) => - (l.workflowName || 'Untitled Workflow') - .toLowerCase() - .includes(aq) - ) - .map((l) => ({ type: 'Logs' as const, value: l })), - ] - if (!aq || aq.length === 0 || aggregated.length === 0) return null - return ( - <> -
-
- Matches -
- {aggregated.map((item, idx) => ( -
{ - setInAggregated(true) - setSubmenuActiveIndex(idx) - }} - onClick={() => { - if (item.type === 'Chats') - insertPastChatMention(item.value as any) - else if (item.type === 'Workflows') - insertWorkflowMention(item.value as any) - else if (item.type === 'Knowledge') - insertKnowledgeMention(item.value as any) - else if (item.type === 'Blocks') - insertBlockMention(item.value as any) - else if ((item as any).type === 'Workflow Blocks') - insertWorkflowBlockMention(item.value as any) - else if (item.type === 'Templates') - insertTemplateMention(item.value as any) - else if (item.type === 'Logs') - insertLogMention(item.value as any) - }} - > - {item.type === 'Chats' ? ( - <> -
- -
- - {(item.value as any).title || 'Untitled Chat'} - - - ) : item.type === 'Workflows' ? ( - <> -
- - {(item.value as any).name || 'Untitled Workflow'} - - - ) : item.type === 'Knowledge' ? ( - <> - - - {(item.value as any).name || 'Untitled'} - - - ) : item.type === 'Blocks' ? ( - <> -
- {(() => { - const Icon = (item.value as any).iconComponent - return Icon ? ( - - ) : null - })()} -
- - {(item.value as any).name || (item.value as any).id} - - - ) : item.type === 'Workflow Blocks' ? ( - <> -
- {(() => { - const Icon = (item.value as any).iconComponent - return Icon ? ( - - ) : null - })()} -
- - {(item.value as any).name || (item.value as any).id} - - - ) : item.type === 'Logs' ? ( - <> - {(() => { - const v = item.value as any - return v.level === 'error' ? ( - - ) : ( - - ) - })()} - - {(item.value as any).workflowName} - - · - - {formatTimestamp((item.value as any).createdAt)} - - · - - {( - ((item.value as any).trigger as string) || 'manual' - ).toLowerCase()} - - - ) : ( - <> -
- ★ -
- - {(item.value as any).name || 'Untitled Template'} - - {typeof (item.value as any).stars === 'number' && ( - - {(item.value as any).stars} - - )} - - )} -
- ))} - - ) - })()} -
- ) - })()} - - )} -
- - )} + ))} + + ) + })()} +
+ ) + })()} + + )} +
+ + )}
{/* Bottom Row: Mode Selector + Attach Button + Send Button */} @@ -3126,89 +3146,129 @@ const UserInput = forwardRef( title='Choose mode' > {getModelIcon()} - {getCollapsedModeLabel()} + + {getCollapsedModeLabel()} + {!agentPrefetch && !['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel) && ( + MAX + )} + - + -
-
-
- MAX mode - - - + + - - - - - Significantly increases depth of reasoning -
- - Only available in Advanced and Behemoth modes - -
-
+ Significantly increases depth of reasoning +
+ + Only available for advanced models + + + +
+ { + if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)) return + setAgentPrefetch(!checked) + }} + />
- { - if (['gpt-5-fast', 'gpt-5'].includes(selectedModel)) return - setAgentPrefetch(!checked) - }} - /> -
-
-
-
-
-
- Model +
+
-
- {modelOptions.map((option) => { - const getIcon = () => { - if (['gpt-5-high', 'claude-4.1-opus'].includes(option.value)) { - return - } - if (['gpt-5', 'gpt-5-medium', 'o3', 'claude-4-sonnet'].includes(option.value)) { - return - } - if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(option.value)) { - return +
+
+
+
+ Model +
+
+ {/* Helper function to get icon for a model */} + {(() => { + const getModelIcon = (modelValue: string) => { + if (['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(modelValue)) { + return + } + if (['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes(modelValue)) { + return + } + if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(modelValue)) { + return + } + return
} - return
- } - return ( - setSelectedModel(option.value)} - className={cn( - 'flex h-7 items-center gap-1.5 px-2 py-1 text-left text-xs', - selectedModel === option.value ? 'bg-muted/50' : '' - )} - > - {getIcon()} - {option.label} - - ) - })} + const renderModelOption = (option: typeof modelOptions[number]) => ( + { + setSelectedModel(option.value) + // Automatically turn off max mode for fast models (Zap icon) + if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(option.value) && !agentPrefetch) { + setAgentPrefetch(true) + } + }} + className={cn( + 'flex h-7 items-center gap-1.5 px-2 py-1 text-left text-xs', + selectedModel === option.value ? 'bg-muted/50' : '' + )} + > + {getModelIcon(option.value)} + {option.label} + + ) + + return ( + <> + {/* OpenAI Models */} +
+
OpenAI
+
+ {modelOptions + .filter((option) => ['gpt-5-fast', 'gpt-5', 'gpt-5-medium', 'gpt-5-high', 'gpt-4o', 'gpt-4.1', 'o3'].includes(option.value)) + .map(renderModelOption)} +
+
+ + {/* Anthropic Models */} +
+
Anthropic
+
+ {modelOptions + .filter((option) => ['claude-4-sonnet', 'claude-4.1-opus'].includes(option.value)) + .map(renderModelOption)} +
+
+ + ) + })()} +
From daa1c8626e8285349bfffe5d8eb168973c08ee79 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 22:17:01 -0700 Subject: [PATCH 37/43] fix trigger mode persistence in addBlock with enableTriggerMode flag passed in --- apps/sim/socket-server/database/operations.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index 74dd3d87c6..9b37207251 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -270,6 +270,7 @@ async function handleBlockOperationTx( horizontalHandles: payload.horizontalHandles ?? true, isWide: payload.isWide ?? false, advancedMode: payload.advancedMode ?? false, + triggerMode: payload.triggerMode ?? false, height: payload.height || 0, } @@ -662,6 +663,7 @@ async function handleBlockOperationTx( horizontalHandles: payload.horizontalHandles ?? true, isWide: payload.isWide ?? false, advancedMode: payload.advancedMode ?? false, + triggerMode: payload.triggerMode ?? false, height: payload.height || 0, } From 20b297804fc8c46e3f26b176e0d75e04c5297582 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 22:31:28 -0700 Subject: [PATCH 38/43] Lint --- apps/sim/app/api/workflows/[id]/yaml/route.ts | 4 +- .../components/user-input/user-input.tsx | 1545 +++++++++-------- .../workflow-block/workflow-block.tsx | 22 +- apps/sim/lib/environment.ts | 4 +- 4 files changed, 808 insertions(+), 767 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index 749ebb02c8..4cceb9d834 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -558,7 +558,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ if (!subBlocks[inputKey]) { subBlocks[inputKey] = { id: inputKey, - type: matchingSubBlock?.type || (inputKey === 'triggerConfig' ? 'trigger-config' : 'short-input'), + type: + matchingSubBlock?.type || + (inputKey === 'triggerConfig' ? 'trigger-config' : 'short-input'), value: inputValue, } } else if (inputValue !== undefined) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx index c487b5404c..633cc0a35a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input.tsx @@ -326,7 +326,7 @@ const UserInput = forwardRef( const nextHeight = Math.min(textarea.scrollHeight, maxHeight) textarea.style.height = `${nextHeight}px` textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden' - + // Also update overlay height to match if (overlay) { overlay.style.height = `${nextHeight}px` @@ -339,13 +339,13 @@ const UserInput = forwardRef( useEffect(() => { const textarea = textareaRef.current const overlay = overlayRef.current - + if (!textarea || !overlay) return - + const handleScroll = () => { overlay.scrollTop = textarea.scrollTop } - + textarea.addEventListener('scroll', handleScroll) return () => textarea.removeEventListener('scroll', handleScroll) }, []) @@ -1257,10 +1257,10 @@ const UserInput = forwardRef( setSubmenuQueryStart(getCaretPos()) void ensureWorkflowBlocksLoaded() } else if (openSubmenuFor === 'Workflow Blocks') { - const q = getSubmenuQuery().toLowerCase() - const filtered = workflowBlocks.filter((b) => - (b.name || b.id).toLowerCase().includes(q) - ) + const q = getSubmenuQuery().toLowerCase() + const filtered = workflowBlocks.filter((b) => + (b.name || b.id).toLowerCase().includes(q) + ) if (filtered.length > 0) { const chosen = filtered[Math.max(0, Math.min(submenuActiveIndex, filtered.length - 1))] @@ -2127,7 +2127,7 @@ const UserInput = forwardRef( {/* Textarea Field with overlay */}
{/* Highlight overlay */} -
@@ -2184,750 +2184,459 @@ const UserInput = forwardRef( {showMentionMenu && ( <>
- {openSubmenuFor ? ( - <> -
- {openSubmenuFor === 'Chats' - ? 'Chats' - : openSubmenuFor === 'Workflows' - ? 'All workflows' - : openSubmenuFor === 'Knowledge' - ? 'Knowledge Bases' - : openSubmenuFor === 'Blocks' - ? 'Blocks' - : openSubmenuFor === 'Workflow Blocks' - ? 'Workflow Blocks' - : openSubmenuFor === 'Templates' - ? 'Templates' - : 'Logs'} -
-
- {isSubmenu('Chats') && ( - <> - {isLoadingPastChats ? ( -
- Loading... -
- ) : pastChats.length === 0 ? ( -
- No past chats -
- ) : ( - pastChats - .filter((c) => - (c.title || 'Untitled Chat') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((chat, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertPastChatMention(chat) - setSubmenuQueryStart(null) - }} - > -
- -
- - {chat.title || 'Untitled Chat'} - -
- )) - )} - - )} - {isSubmenu('Workflows') && ( - <> - {isLoadingWorkflows ? ( -
- Loading... -
- ) : workflows.length === 0 ? ( -
- No workflows -
- ) : ( - workflows - .filter((w) => - (w.name || 'Untitled Workflow') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((wf, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertWorkflowMention(wf) - setSubmenuQueryStart(null) - }} - > -
- - {wf.name || 'Untitled Workflow'} - -
- )) - )} - - )} - {isSubmenu('Knowledge') && ( - <> - {isLoadingKnowledge ? ( -
- Loading... -
- ) : knowledgeBases.length === 0 ? ( -
- No knowledge bases -
- ) : ( - knowledgeBases - .filter((k) => - (k.name || 'Untitled') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((kb, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertKnowledgeMention(kb) - setSubmenuQueryStart(null) - }} - > - - {kb.name || 'Untitled'} -
- )) - )} - - )} - {isSubmenu('Blocks') && ( - <> - {isLoadingBlocks ? ( -
- Loading... -
- ) : blocksList.length === 0 ? ( -
- No blocks found -
- ) : ( - blocksList - .filter((b) => - (b.name || b.id) - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((blk, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertBlockMention(blk) - setSubmenuQueryStart(null) - }} - > + ref={mentionMenuRef} + className={cn( + 'absolute bottom-full left-0 z-50 mb-1 flex max-h-64 flex-col overflow-hidden rounded-[8px] border bg-popover p-1 text-foreground shadow-md', + openSubmenuFor === 'Blocks' + ? 'w-80' + : openSubmenuFor === 'Templates' || + openSubmenuFor === 'Logs' || + aggregatedActive + ? 'w-96' + : 'w-56' + )} + > + {openSubmenuFor ? ( + <> +
+ {openSubmenuFor === 'Chats' + ? 'Chats' + : openSubmenuFor === 'Workflows' + ? 'All workflows' + : openSubmenuFor === 'Knowledge' + ? 'Knowledge Bases' + : openSubmenuFor === 'Blocks' + ? 'Blocks' + : openSubmenuFor === 'Workflow Blocks' + ? 'Workflow Blocks' + : openSubmenuFor === 'Templates' + ? 'Templates' + : 'Logs'} +
+
+ {isSubmenu('Chats') && ( + <> + {isLoadingPastChats ? ( +
+ Loading... +
+ ) : pastChats.length === 0 ? ( +
+ No past chats +
+ ) : ( + pastChats + .filter((c) => + (c.title || 'Untitled Chat') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((chat, idx) => (
- {blk.iconComponent && ( - + key={chat.id} + data-idx={idx} + className={cn( + 'flex items-center gap-2 rounded-[6px] px-2 py-1.5 text-sm hover:bg-muted/60', + submenuActiveIndex === idx && 'bg-muted' )} + role='menuitem' + aria-selected={submenuActiveIndex === idx} + onMouseEnter={() => setSubmenuActiveIndex(idx)} + onClick={() => { + insertPastChatMention(chat) + setSubmenuQueryStart(null) + }} + > +
+ +
+ + {chat.title || 'Untitled Chat'} +
- {blk.name || blk.id} -
- )) - )} - - )} - {isSubmenu('Workflow Blocks') && ( - <> - {isLoadingWorkflowBlocks ? ( -
- Loading... -
- ) : workflowBlocks.length === 0 ? ( -
- No blocks in this workflow -
- ) : ( - workflowBlocks - .filter((b) => - (b.name || b.id) - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((blk, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertWorkflowBlockMention(blk) - setSubmenuQueryStart(null) - }} - > + )) + )} + + )} + {isSubmenu('Workflows') && ( + <> + {isLoadingWorkflows ? ( +
+ Loading... +
+ ) : workflows.length === 0 ? ( +
+ No workflows +
+ ) : ( + workflows + .filter((w) => + (w.name || 'Untitled Workflow') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((wf, idx) => (
- {blk.iconComponent && ( - + key={wf.id} + data-idx={idx} + className={cn( + 'flex items-center gap-2 rounded-[6px] px-2 py-1.5 text-sm hover:bg-muted/60', + submenuActiveIndex === idx && 'bg-muted' )} + role='menuitem' + aria-selected={submenuActiveIndex === idx} + onMouseEnter={() => setSubmenuActiveIndex(idx)} + onClick={() => { + insertWorkflowMention(wf) + setSubmenuQueryStart(null) + }} + > +
+ + {wf.name || 'Untitled Workflow'} +
- {blk.name || blk.id} -
- )) - )} - - )} - {isSubmenu('Templates') && ( - <> - {isLoadingTemplates ? ( -
- Loading... -
- ) : templatesList.length === 0 ? ( -
- No templates found -
- ) : ( - templatesList - .filter((t) => - (t.name || 'Untitled Template') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((tpl, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertTemplateMention(tpl) - setSubmenuQueryStart(null) - }} - > -
- ★ + )) + )} + + )} + {isSubmenu('Knowledge') && ( + <> + {isLoadingKnowledge ? ( +
+ Loading... +
+ ) : knowledgeBases.length === 0 ? ( +
+ No knowledge bases +
+ ) : ( + knowledgeBases + .filter((k) => + (k.name || 'Untitled') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((kb, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertKnowledgeMention(kb) + setSubmenuQueryStart(null) + }} + > + + {kb.name || 'Untitled'}
- {tpl.name} - - {tpl.stars} - -
- )) - )} - - )} - {isSubmenu('Logs') && ( - <> - {isLoadingLogs ? ( -
- Loading... -
- ) : logsList.length === 0 ? ( -
- No executions found -
- ) : ( - logsList - .filter((l) => - [l.workflowName, l.trigger || ''] - .join(' ') - .toLowerCase() - .includes(getSubmenuQuery().toLowerCase()) - ) - .map((log, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => { - insertLogMention(log) - setSubmenuQueryStart(null) - }} - > - {log.level === 'error' ? ( - - ) : ( - - )} - {log.workflowName} - · - - {formatTimestamp(log.createdAt)} - - · - - {(log.trigger || 'manual').toLowerCase()} - -
- )) - )} - - )} -
- - ) : ( - <> - {(() => { - const q = ( - getActiveMentionQueryAtPosition(getCaretPos())?.query || '' - ).toLowerCase() - const filtered = mentionOptions.filter((label) => - label.toLowerCase().includes(q) - ) - if (q.length > 0 && filtered.length === 0) { - // Aggregated search view - const aggregated = [ - ...workflowBlocks - .filter((b) => (b.name || b.id).toLowerCase().includes(q)) - .map((b) => ({ - type: 'Workflow Blocks' as const, - id: b.id, - value: b, - onClick: () => insertWorkflowBlockMention(b), - })), - ...workflows - .filter((w) => - (w.name || 'Untitled Workflow').toLowerCase().includes(q) - ) - .map((w) => ({ - type: 'Workflows' as const, - id: w.id, - value: w, - onClick: () => insertWorkflowMention(w), - })), - ...blocksList - .filter((b) => (b.name || b.id).toLowerCase().includes(q)) - .map((b) => ({ - type: 'Blocks' as const, - id: b.id, - value: b, - onClick: () => insertBlockMention(b), - })), - ...knowledgeBases - .filter((k) => (k.name || 'Untitled').toLowerCase().includes(q)) - .map((k) => ({ - type: 'Knowledge' as const, - id: k.id, - value: k, - onClick: () => insertKnowledgeMention(k), - })), - ...templatesList - .filter((t) => - (t.name || 'Untitled Template').toLowerCase().includes(q) - ) - .map((t) => ({ - type: 'Templates' as const, - id: t.id, - value: t, - onClick: () => insertTemplateMention(t), - })), - ...pastChats - .filter((c) => (c.title || 'Untitled Chat').toLowerCase().includes(q)) - .map((c) => ({ - type: 'Chats' as const, - id: c.id, - value: c, - onClick: () => insertPastChatMention(c), - })), - ...logsList - .filter((l) => - (l.workflowName || 'Untitled Workflow').toLowerCase().includes(q) - ) - .map((l) => ({ - type: 'Logs' as const, - id: l.id, - value: l, - onClick: () => insertLogMention(l), - })), - ] - return ( -
- {aggregated.length === 0 ? ( + )) + )} + + )} + {isSubmenu('Blocks') && ( + <> + {isLoadingBlocks ? ( +
+ Loading... +
+ ) : blocksList.length === 0 ? (
- No matches + No blocks found
) : ( - aggregated.map((item, idx) => ( -
setSubmenuActiveIndex(idx)} - onClick={() => item.onClick()} - > - {item.type === 'Chats' ? ( - <> -
- -
- - {(item.value as any).title || 'Untitled Chat'} - - - ) : item.type === 'Workflows' ? ( - <> -
- - {(item.value as any).name || 'Untitled Workflow'} - - - ) : item.type === 'Knowledge' ? ( - <> - - - {(item.value as any).name || 'Untitled'} - - - ) : item.type === 'Blocks' ? ( - <> -
- {(() => { - const Icon = (item.value as any).iconComponent - return Icon ? ( - - ) : null - })()} -
- - {(item.value as any).name || (item.value as any).id} - - - ) : item.type === 'Workflow Blocks' ? ( - <> -
- {(() => { - const Icon = (item.value as any).iconComponent - return Icon ? ( - - ) : null - })()} -
- - {(item.value as any).name || (item.value as any).id} - - - ) : item.type === 'Logs' ? ( - <> - {(() => { - const v = item.value as any - return v.level === 'error' ? ( - - ) : ( - - ) - })()} - - {(item.value as any).workflowName} - - · - - {formatTimestamp((item.value as any).createdAt)} - - · - - {( - ((item.value as any).trigger as string) || 'manual' - ).toLowerCase()} - - - ) : ( - <> -
- ★ -
- - {(item.value as any).name || 'Untitled Template'} - - {typeof (item.value as any).stars === 'number' && ( - - {(item.value as any).stars} - + blocksList + .filter((b) => + (b.name || b.id) + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) + ) + .map((blk, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertBlockMention(blk) + setSubmenuQueryStart(null) + }} + > +
+ {blk.iconComponent && ( + )} - - )} -
- )) +
+ {blk.name || blk.id} +
+ )) )} -
- ) - } - // Filtered top-level options view - return ( -
- {filtered.map((label, idx) => ( -
{ - setInAggregated(false) - setMentionActiveIndex(idx) - }} - onClick={() => { - if (label === 'Chats') { - resetActiveMentionQuery() - setOpenSubmenuFor('Chats') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensurePastChatsLoaded() - } else if (label === 'Workflows') { - resetActiveMentionQuery() - setOpenSubmenuFor('Workflows') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowsLoaded() - } else if (label === 'Knowledge') { - resetActiveMentionQuery() - setOpenSubmenuFor('Knowledge') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureKnowledgeLoaded() - } else if (label === 'Blocks') { - resetActiveMentionQuery() - setOpenSubmenuFor('Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureBlocksLoaded() - } else if (label === 'Workflow Blocks') { - resetActiveMentionQuery() - setOpenSubmenuFor('Workflow Blocks') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureWorkflowBlocksLoaded() - } else if (label === 'Docs') { - // No submenu; insert immediately - insertDocsMention() - } else if (label === 'Templates') { - resetActiveMentionQuery() - setOpenSubmenuFor('Templates') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureTemplatesLoaded() - } else if (label === 'Logs') { - resetActiveMentionQuery() - setOpenSubmenuFor('Logs') - setSubmenuActiveIndex(0) - setSubmenuQueryStart(getCaretPos()) - void ensureLogsLoaded() - } - }} - > -
- {label === 'Chats' ? ( - - ) : label === 'Workflows' ? ( - - ) : label === 'Blocks' ? ( - - ) : label === 'Workflow Blocks' ? ( - - ) : label === 'Knowledge' ? ( - - ) : label === 'Docs' ? ( - - ) : label === 'Templates' ? ( - - ) : label === 'Logs' ? ( - - ) : ( -
- )} - {label === 'Workflows' ? 'All workflows' : label} + + )} + {isSubmenu('Workflow Blocks') && ( + <> + {isLoadingWorkflowBlocks ? ( +
+ Loading...
- {label !== 'Docs' && ( - - )} -
- ))} - - {(() => { - const aq = ( - getActiveMentionQueryAtPosition(getCaretPos())?.query || '' - ).toLowerCase() - const filteredLen = mentionOptions.filter((label) => - label.toLowerCase().includes(aq) - ).length - const aggregated = [ - ...workflowBlocks - .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) - .map((b) => ({ type: 'Workflow Blocks' as const, value: b })), - ...workflows - .filter((w) => - (w.name || 'Untitled Workflow').toLowerCase().includes(aq) + ) : workflowBlocks.length === 0 ? ( +
+ No blocks in this workflow +
+ ) : ( + workflowBlocks + .filter((b) => + (b.name || b.id) + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) ) - .map((w) => ({ type: 'Workflows' as const, value: w })), - ...blocksList - .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) - .map((b) => ({ type: 'Blocks' as const, value: b })), - ...knowledgeBases - .filter((k) => (k.name || 'Untitled').toLowerCase().includes(aq)) - .map((k) => ({ type: 'Knowledge' as const, value: k })), - ...templatesList + .map((blk, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertWorkflowBlockMention(blk) + setSubmenuQueryStart(null) + }} + > +
+ {blk.iconComponent && ( + + )} +
+ {blk.name || blk.id} +
+ )) + )} + + )} + {isSubmenu('Templates') && ( + <> + {isLoadingTemplates ? ( +
+ Loading... +
+ ) : templatesList.length === 0 ? ( +
+ No templates found +
+ ) : ( + templatesList .filter((t) => - (t.name || 'Untitled Template').toLowerCase().includes(aq) - ) - .map((t) => ({ type: 'Templates' as const, value: t })), - ...pastChats - .filter((c) => - (c.title || 'Untitled Chat').toLowerCase().includes(aq) + (t.name || 'Untitled Template') + .toLowerCase() + .includes(getSubmenuQuery().toLowerCase()) ) - .map((c) => ({ type: 'Chats' as const, value: c })), - ...logsList + .map((tpl, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => { + insertTemplateMention(tpl) + setSubmenuQueryStart(null) + }} + > +
+ ★ +
+ {tpl.name} + + {tpl.stars} + +
+ )) + )} + + )} + {isSubmenu('Logs') && ( + <> + {isLoadingLogs ? ( +
+ Loading... +
+ ) : logsList.length === 0 ? ( +
+ No executions found +
+ ) : ( + logsList .filter((l) => - (l.workflowName || 'Untitled Workflow') + [l.workflowName, l.trigger || ''] + .join(' ') .toLowerCase() - .includes(aq) + .includes(getSubmenuQuery().toLowerCase()) ) - .map((l) => ({ type: 'Logs' as const, value: l })), - ] - if (!aq || aq.length === 0 || aggregated.length === 0) return null - return ( - <> -
-
- Matches -
- {aggregated.map((item, idx) => ( + .map((log, idx) => (
{ - setInAggregated(true) - setSubmenuActiveIndex(idx) - }} + aria-selected={submenuActiveIndex === idx} + onMouseEnter={() => setSubmenuActiveIndex(idx)} onClick={() => { - if (item.type === 'Chats') - insertPastChatMention(item.value as any) - else if (item.type === 'Workflows') - insertWorkflowMention(item.value as any) - else if (item.type === 'Knowledge') - insertKnowledgeMention(item.value as any) - else if (item.type === 'Blocks') - insertBlockMention(item.value as any) - else if ((item as any).type === 'Workflow Blocks') - insertWorkflowBlockMention(item.value as any) - else if (item.type === 'Templates') - insertTemplateMention(item.value as any) - else if (item.type === 'Logs') - insertLogMention(item.value as any) + insertLogMention(log) + setSubmenuQueryStart(null) }} + > + {log.level === 'error' ? ( + + ) : ( + + )} + {log.workflowName} + · + + {formatTimestamp(log.createdAt)} + + · + + {(log.trigger || 'manual').toLowerCase()} + +
+ )) + )} + + )} +
+ + ) : ( + <> + {(() => { + const q = ( + getActiveMentionQueryAtPosition(getCaretPos())?.query || '' + ).toLowerCase() + const filtered = mentionOptions.filter((label) => + label.toLowerCase().includes(q) + ) + if (q.length > 0 && filtered.length === 0) { + // Aggregated search view + const aggregated = [ + ...workflowBlocks + .filter((b) => (b.name || b.id).toLowerCase().includes(q)) + .map((b) => ({ + type: 'Workflow Blocks' as const, + id: b.id, + value: b, + onClick: () => insertWorkflowBlockMention(b), + })), + ...workflows + .filter((w) => + (w.name || 'Untitled Workflow').toLowerCase().includes(q) + ) + .map((w) => ({ + type: 'Workflows' as const, + id: w.id, + value: w, + onClick: () => insertWorkflowMention(w), + })), + ...blocksList + .filter((b) => (b.name || b.id).toLowerCase().includes(q)) + .map((b) => ({ + type: 'Blocks' as const, + id: b.id, + value: b, + onClick: () => insertBlockMention(b), + })), + ...knowledgeBases + .filter((k) => (k.name || 'Untitled').toLowerCase().includes(q)) + .map((k) => ({ + type: 'Knowledge' as const, + id: k.id, + value: k, + onClick: () => insertKnowledgeMention(k), + })), + ...templatesList + .filter((t) => + (t.name || 'Untitled Template').toLowerCase().includes(q) + ) + .map((t) => ({ + type: 'Templates' as const, + id: t.id, + value: t, + onClick: () => insertTemplateMention(t), + })), + ...pastChats + .filter((c) => (c.title || 'Untitled Chat').toLowerCase().includes(q)) + .map((c) => ({ + type: 'Chats' as const, + id: c.id, + value: c, + onClick: () => insertPastChatMention(c), + })), + ...logsList + .filter((l) => + (l.workflowName || 'Untitled Workflow').toLowerCase().includes(q) + ) + .map((l) => ({ + type: 'Logs' as const, + id: l.id, + value: l, + onClick: () => insertLogMention(l), + })), + ] + return ( +
+ {aggregated.length === 0 ? ( +
+ No matches +
+ ) : ( + aggregated.map((item, idx) => ( +
setSubmenuActiveIndex(idx)} + onClick={() => item.onClick()} > {item.type === 'Chats' ? ( <> @@ -2946,8 +2655,7 @@ const UserInput = forwardRef(
@@ -3041,18 +2749,310 @@ const UserInput = forwardRef( )}
- ))} - - ) - })()} -
- ) - })()} - - )} -
- - )} + )) + )} +
+ ) + } + // Filtered top-level options view + return ( +
+ {filtered.map((label, idx) => ( +
{ + setInAggregated(false) + setMentionActiveIndex(idx) + }} + onClick={() => { + if (label === 'Chats') { + resetActiveMentionQuery() + setOpenSubmenuFor('Chats') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensurePastChatsLoaded() + } else if (label === 'Workflows') { + resetActiveMentionQuery() + setOpenSubmenuFor('Workflows') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureWorkflowsLoaded() + } else if (label === 'Knowledge') { + resetActiveMentionQuery() + setOpenSubmenuFor('Knowledge') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureKnowledgeLoaded() + } else if (label === 'Blocks') { + resetActiveMentionQuery() + setOpenSubmenuFor('Blocks') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureBlocksLoaded() + } else if (label === 'Workflow Blocks') { + resetActiveMentionQuery() + setOpenSubmenuFor('Workflow Blocks') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureWorkflowBlocksLoaded() + } else if (label === 'Docs') { + // No submenu; insert immediately + insertDocsMention() + } else if (label === 'Templates') { + resetActiveMentionQuery() + setOpenSubmenuFor('Templates') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureTemplatesLoaded() + } else if (label === 'Logs') { + resetActiveMentionQuery() + setOpenSubmenuFor('Logs') + setSubmenuActiveIndex(0) + setSubmenuQueryStart(getCaretPos()) + void ensureLogsLoaded() + } + }} + > +
+ {label === 'Chats' ? ( + + ) : label === 'Workflows' ? ( + + ) : label === 'Blocks' ? ( + + ) : label === 'Workflow Blocks' ? ( + + ) : label === 'Knowledge' ? ( + + ) : label === 'Docs' ? ( + + ) : label === 'Templates' ? ( + + ) : label === 'Logs' ? ( + + ) : ( +
+ )} + {label === 'Workflows' ? 'All workflows' : label} +
+ {label !== 'Docs' && ( + + )} +
+ ))} + + {(() => { + const aq = ( + getActiveMentionQueryAtPosition(getCaretPos())?.query || '' + ).toLowerCase() + const filteredLen = mentionOptions.filter((label) => + label.toLowerCase().includes(aq) + ).length + const aggregated = [ + ...workflowBlocks + .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) + .map((b) => ({ type: 'Workflow Blocks' as const, value: b })), + ...workflows + .filter((w) => + (w.name || 'Untitled Workflow').toLowerCase().includes(aq) + ) + .map((w) => ({ type: 'Workflows' as const, value: w })), + ...blocksList + .filter((b) => (b.name || b.id).toLowerCase().includes(aq)) + .map((b) => ({ type: 'Blocks' as const, value: b })), + ...knowledgeBases + .filter((k) => (k.name || 'Untitled').toLowerCase().includes(aq)) + .map((k) => ({ type: 'Knowledge' as const, value: k })), + ...templatesList + .filter((t) => + (t.name || 'Untitled Template').toLowerCase().includes(aq) + ) + .map((t) => ({ type: 'Templates' as const, value: t })), + ...pastChats + .filter((c) => + (c.title || 'Untitled Chat').toLowerCase().includes(aq) + ) + .map((c) => ({ type: 'Chats' as const, value: c })), + ...logsList + .filter((l) => + (l.workflowName || 'Untitled Workflow') + .toLowerCase() + .includes(aq) + ) + .map((l) => ({ type: 'Logs' as const, value: l })), + ] + if (!aq || aq.length === 0 || aggregated.length === 0) return null + return ( + <> +
+
+ Matches +
+ {aggregated.map((item, idx) => ( +
{ + setInAggregated(true) + setSubmenuActiveIndex(idx) + }} + onClick={() => { + if (item.type === 'Chats') + insertPastChatMention(item.value as any) + else if (item.type === 'Workflows') + insertWorkflowMention(item.value as any) + else if (item.type === 'Knowledge') + insertKnowledgeMention(item.value as any) + else if (item.type === 'Blocks') + insertBlockMention(item.value as any) + else if ((item as any).type === 'Workflow Blocks') + insertWorkflowBlockMention(item.value as any) + else if (item.type === 'Templates') + insertTemplateMention(item.value as any) + else if (item.type === 'Logs') + insertLogMention(item.value as any) + }} + > + {item.type === 'Chats' ? ( + <> +
+ +
+ + {(item.value as any).title || 'Untitled Chat'} + + + ) : item.type === 'Workflows' ? ( + <> +
+ + {(item.value as any).name || 'Untitled Workflow'} + + + ) : item.type === 'Knowledge' ? ( + <> + + + {(item.value as any).name || 'Untitled'} + + + ) : item.type === 'Blocks' ? ( + <> +
+ {(() => { + const Icon = (item.value as any).iconComponent + return Icon ? ( + + ) : null + })()} +
+ + {(item.value as any).name || (item.value as any).id} + + + ) : item.type === 'Workflow Blocks' ? ( + <> +
+ {(() => { + const Icon = (item.value as any).iconComponent + return Icon ? ( + + ) : null + })()} +
+ + {(item.value as any).name || (item.value as any).id} + + + ) : item.type === 'Logs' ? ( + <> + {(() => { + const v = item.value as any + return v.level === 'error' ? ( + + ) : ( + + ) + })()} + + {(item.value as any).workflowName} + + · + + {formatTimestamp((item.value as any).createdAt)} + + · + + {( + ((item.value as any).trigger as string) || 'manual' + ).toLowerCase()} + + + ) : ( + <> +
+ ★ +
+ + {(item.value as any).name || 'Untitled Template'} + + {typeof (item.value as any).stars === 'number' && ( + + {(item.value as any).stars} + + )} + + )} +
+ ))} + + ) + })()} +
+ ) + })()} + + )} +
+ + )}
{/* Bottom Row: Mode Selector + Attach Button + Send Button */} @@ -3148,9 +3148,10 @@ const UserInput = forwardRef( {getModelIcon()} {getCollapsedModeLabel()} - {!agentPrefetch && !['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel) && ( - MAX - )} + {!agentPrefetch && + !['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel) && ( + MAX + )} @@ -3194,7 +3195,8 @@ const UserInput = forwardRef( : undefined } onCheckedChange={(checked) => { - if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)) return + if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(selectedModel)) + return setAgentPrefetch(!checked) }} /> @@ -3212,10 +3214,18 @@ const UserInput = forwardRef( {/* Helper function to get icon for a model */} {(() => { const getModelIcon = (modelValue: string) => { - if (['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(modelValue)) { - return + if ( + ['gpt-5-high', 'o3', 'claude-4.1-opus'].includes(modelValue) + ) { + return ( + + ) } - if (['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes(modelValue)) { + if ( + ['gpt-5', 'gpt-5-medium', 'claude-4-sonnet'].includes( + modelValue + ) + ) { return } if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(modelValue)) { @@ -3224,13 +3234,20 @@ const UserInput = forwardRef( return
} - const renderModelOption = (option: typeof modelOptions[number]) => ( + const renderModelOption = ( + option: (typeof modelOptions)[number] + ) => ( { setSelectedModel(option.value) // Automatically turn off max mode for fast models (Zap icon) - if (['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes(option.value) && !agentPrefetch) { + if ( + ['gpt-4o', 'gpt-4.1', 'gpt-5-fast'].includes( + option.value + ) && + !agentPrefetch + ) { setAgentPrefetch(true) } }} @@ -3248,20 +3265,38 @@ const UserInput = forwardRef( <> {/* OpenAI Models */}
-
OpenAI
+
+ OpenAI +
{modelOptions - .filter((option) => ['gpt-5-fast', 'gpt-5', 'gpt-5-medium', 'gpt-5-high', 'gpt-4o', 'gpt-4.1', 'o3'].includes(option.value)) + .filter((option) => + [ + 'gpt-5-fast', + 'gpt-5', + 'gpt-5-medium', + 'gpt-5-high', + 'gpt-4o', + 'gpt-4.1', + 'o3', + ].includes(option.value) + ) .map(renderModelOption)}
{/* Anthropic Models */}
-
Anthropic
+
+ Anthropic +
{modelOptions - .filter((option) => ['claude-4-sonnet', 'claude-4.1-opus'].includes(option.value)) + .filter((option) => + ['claude-4-sonnet', 'claude-4.1-opus'].includes( + option.value + ) + ) .map(renderModelOption)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index 927c7eb526..c5cbd2508e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -142,27 +142,31 @@ export function WorkflowBlock({ id, data }: NodeProps) { id, ]) // Always call hooks to maintain consistent hook order - const storeHorizontalHandles = useWorkflowStore((state) => state.blocks[id]?.horizontalHandles ?? true) + const storeHorizontalHandles = useWorkflowStore( + (state) => state.blocks[id]?.horizontalHandles ?? true + ) const storeIsWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false) const storeBlockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0) - const storeBlockAdvancedMode = useWorkflowStore((state) => state.blocks[id]?.advancedMode ?? false) + const storeBlockAdvancedMode = useWorkflowStore( + (state) => state.blocks[id]?.advancedMode ?? false + ) const storeBlockTriggerMode = useWorkflowStore((state) => state.blocks[id]?.triggerMode ?? false) - + // Get block properties from currentWorkflow when in diff mode, otherwise from workflow store const horizontalHandles = data.isPreview ? (data.blockState?.horizontalHandles ?? true) // In preview mode, use blockState and default to horizontal : currentWorkflow.isDiffMode ? (currentWorkflow.blocks[id]?.horizontalHandles ?? true) : storeHorizontalHandles - + const isWide = currentWorkflow.isDiffMode ? (currentWorkflow.blocks[id]?.isWide ?? false) : storeIsWide - + const blockHeight = currentWorkflow.isDiffMode ? (currentWorkflow.blocks[id]?.height ?? 0) : storeBlockHeight - + // Get per-block webhook status by checking if webhook is configured const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) @@ -177,10 +181,10 @@ export function WorkflowBlock({ id, data }: NodeProps) { const blockAdvancedMode = currentWorkflow.isDiffMode ? (currentWorkflow.blocks[id]?.advancedMode ?? false) : storeBlockAdvancedMode - + // Get triggerMode from currentWorkflow blocks when in diff mode, otherwise from workflow store - const blockTriggerMode = currentWorkflow.isDiffMode - ? currentWorkflow.blocks[id]?.triggerMode ?? false + const blockTriggerMode = currentWorkflow.isDiffMode + ? (currentWorkflow.blocks[id]?.triggerMode ?? false) : storeBlockTriggerMode // Local UI state for diff mode controls diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index 3825d841ef..bbc0954d83 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -22,8 +22,8 @@ export const isTest = env.NODE_ENV === 'test' * Is this the hosted version of the application */ export const isHosted = true - // env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || - // env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' +// env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || +// env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled From ce4eaf32d7fe71fe1932c81a4f39c0197ca6d5c6 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 22:36:22 -0700 Subject: [PATCH 39/43] Fix failing tests --- apps/sim/app/api/copilot/chat/route.test.ts | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/apps/sim/app/api/copilot/chat/route.test.ts b/apps/sim/app/api/copilot/chat/route.test.ts index b8e63f4b9e..92a54281e9 100644 --- a/apps/sim/app/api/copilot/chat/route.test.ts +++ b/apps/sim/app/api/copilot/chat/route.test.ts @@ -220,12 +220,20 @@ describe('Copilot Chat API Route', () => { content: 'Hello', }, ], + chatMessages: [ + { + role: 'user', + content: 'Hello', + }, + ], workflowId: 'workflow-123', userId: 'user-123', stream: true, streamToolCalls: true, + model: 'gpt-5', mode: 'agent', messageId: 'mock-uuid-1234-5678', + version: '1.0.0', chatId: 'chat-123', }), }) @@ -283,12 +291,19 @@ describe('Copilot Chat API Route', () => { { role: 'assistant', content: 'Previous response' }, { role: 'user', content: 'New message' }, ], + chatMessages: [ + { role: 'user', content: 'Previous message' }, + { role: 'assistant', content: 'Previous response' }, + { role: 'user', content: 'New message' }, + ], workflowId: 'workflow-123', userId: 'user-123', stream: true, streamToolCalls: true, + model: 'gpt-5', mode: 'agent', messageId: 'mock-uuid-1234-5678', + version: '1.0.0', chatId: 'chat-123', }), }) @@ -335,12 +350,18 @@ describe('Copilot Chat API Route', () => { { role: 'system', content: 'User seems confused about the workflow' }, { role: 'user', content: 'Hello' }, ], + chatMessages: [ + { role: 'system', content: 'User seems confused about the workflow' }, + { role: 'user', content: 'Hello' }, + ], workflowId: 'workflow-123', userId: 'user-123', stream: true, streamToolCalls: true, + model: 'gpt-5', mode: 'agent', messageId: 'mock-uuid-1234-5678', + version: '1.0.0', chatId: 'chat-123', }), }) @@ -424,12 +445,15 @@ describe('Copilot Chat API Route', () => { expect.objectContaining({ body: JSON.stringify({ messages: [{ role: 'user', content: 'What is this workflow?' }], + chatMessages: [{ role: 'user', content: 'What is this workflow?' }], workflowId: 'workflow-123', userId: 'user-123', stream: true, streamToolCalls: true, + model: 'gpt-5', mode: 'ask', messageId: 'mock-uuid-1234-5678', + version: '1.0.0', chatId: 'chat-123', }), }) From f1370c5fed00dfeb009e570e4bb631fb41691e2e Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 22:36:58 -0700 Subject: [PATCH 40/43] Reset ishosted --- apps/sim/lib/environment.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index bbc0954d83..01ea3b8c13 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -21,9 +21,9 @@ export const isTest = env.NODE_ENV === 'test' /** * Is this the hosted version of the application */ -export const isHosted = true -// env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || -// env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' +export const isHosted = +env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || +env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled From 048c1b50ac9d77355550ec5880ea6632a6ea158d Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 22:37:06 -0700 Subject: [PATCH 41/43] Lint --- apps/sim/lib/environment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/environment.ts b/apps/sim/lib/environment.ts index 01ea3b8c13..a74ab24eb8 100644 --- a/apps/sim/lib/environment.ts +++ b/apps/sim/lib/environment.ts @@ -22,8 +22,8 @@ export const isTest = env.NODE_ENV === 'test' * Is this the hosted version of the application */ export const isHosted = -env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || -env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' + env.NEXT_PUBLIC_APP_URL === 'https://www.sim.ai' || + env.NEXT_PUBLIC_APP_URL === 'https://www.staging.sim.ai' /** * Is billing enforcement enabled From f5338348e585d51e928cc31e4bf14d4e3b5f3ab9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 22 Sep 2025 22:40:03 -0700 Subject: [PATCH 42/43] input format for legacy starter --- .../input-mapping/input-mapping.tsx | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx index a2d1f536df..86e362c05b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/input-mapping/input-mapping.tsx @@ -19,12 +19,28 @@ interface InputTriggerBlock { } } +interface StarterBlockLegacy { + type: 'starter' + subBlocks?: { + inputFormat?: { value?: InputFormatField[] } + } + config?: { + params?: { + inputFormat?: InputFormatField[] + } + } +} + function isInputTriggerBlock(value: unknown): value is InputTriggerBlock { return ( !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'input_trigger' ) } +function isStarterBlock(value: unknown): value is StarterBlockLegacy { + return !!value && typeof value === 'object' && (value as { type?: unknown }).type === 'starter' +} + function isInputFormatField(value: unknown): value is InputFormatField { if (typeof value !== 'object' || value === null) return false if (!('name' in value)) return false @@ -80,25 +96,36 @@ export function InputMapping({ } const { data } = await res.json() const blocks = (data?.state?.blocks as Record) || {} + // Prefer new input_trigger const triggerEntry = Object.entries(blocks).find(([, b]) => isInputTriggerBlock(b)) - if (!triggerEntry) { - if (isMounted) setChildInputFields([]) - return - } - const triggerBlock = triggerEntry[1] - if (!isInputTriggerBlock(triggerBlock)) { - if (isMounted) setChildInputFields([]) - return + if (triggerEntry && isInputTriggerBlock(triggerEntry[1])) { + const inputFormat = triggerEntry[1].subBlocks?.inputFormat?.value + if (Array.isArray(inputFormat)) { + const fields = (inputFormat as unknown[]) + .filter(isInputFormatField) + .map((f) => ({ name: f.name, type: f.type })) + if (isMounted) setChildInputFields(fields) + return + } } - const inputFormat = triggerBlock.subBlocks?.inputFormat?.value - if (Array.isArray(inputFormat)) { - const fields = (inputFormat as unknown[]) - .filter(isInputFormatField) - .map((f) => ({ name: f.name, type: f.type })) - if (isMounted) setChildInputFields(fields) - } else { - if (isMounted) setChildInputFields([]) + + // Fallback: legacy starter block inputFormat (subBlocks or config.params) + const starterEntry = Object.entries(blocks).find(([, b]) => isStarterBlock(b)) + if (starterEntry && isStarterBlock(starterEntry[1])) { + const starter = starterEntry[1] + const subBlockFormat = starter.subBlocks?.inputFormat?.value + const legacyParamsFormat = starter.config?.params?.inputFormat + const chosen = Array.isArray(subBlockFormat) ? subBlockFormat : legacyParamsFormat + if (Array.isArray(chosen)) { + const fields = (chosen as unknown[]) + .filter(isInputFormatField) + .map((f) => ({ name: f.name, type: f.type })) + if (isMounted) setChildInputFields(fields) + return + } } + + if (isMounted) setChildInputFields([]) } catch { if (isMounted) setChildInputFields([]) } @@ -291,7 +318,7 @@ function InputMappingField({ className='w-full whitespace-pre' style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }} > - {formatDisplayText(value, true)} + {formatDisplayText(value)}
From 01407835b1a6cf55986bff1d8370649943474d1a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 22 Sep 2025 23:20:16 -0700 Subject: [PATCH 43/43] Fix executor --- .../lib/workflow-execution-utils.ts | 36 ++++--------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/lib/workflow-execution-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/lib/workflow-execution-utils.ts index 26e0f5a886..71652bb2e5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/lib/workflow-execution-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/lib/workflow-execution-utils.ts @@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console/logger' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' -import { getBlock } from '@/blocks' import type { BlockOutput } from '@/blocks/types' import { Executor } from '@/executor' import type { ExecutionResult, StreamingExecution } from '@/executor/types' @@ -131,26 +130,9 @@ export async function executeWorkflowWithLogging( // Merge subblock states from the appropriate store const mergedStates = mergeSubblockState(validBlocks) - // Filter out trigger blocks for manual execution - const filteredStates = Object.entries(mergedStates).reduce( - (acc, [id, block]) => { - // Skip blocks with undefined type - if (!block || !block.type) { - logger.warn(`Skipping block with undefined type: ${id}`, block) - return acc - } - - const blockConfig = getBlock(block.type) - const isTriggerBlock = blockConfig?.category === 'triggers' - - // Skip trigger blocks during manual execution - if (!isTriggerBlock) { - acc[id] = block - } - return acc - }, - {} as typeof mergedStates - ) + // Don't filter out trigger blocks - let the executor handle them properly + // The standard executor has TriggerBlockHandler that knows how to handle triggers + const filteredStates = mergedStates const currentBlockStates = Object.entries(filteredStates).reduce( (acc, [id, block]) => { @@ -186,15 +168,9 @@ export async function executeWorkflowWithLogging( {} as Record ) - // Filter edges to exclude connections to/from trigger blocks - const triggerBlockIds = Object.keys(mergedStates).filter((id) => { - const blockConfig = getBlock(mergedStates[id].type) - return blockConfig?.category === 'triggers' - }) - - const filteredEdges = workflowEdges.filter( - (edge: any) => !triggerBlockIds.includes(edge.source) && !triggerBlockIds.includes(edge.target) - ) + // Don't filter edges - let all connections remain intact + // The executor's routing system will handle execution paths properly + const filteredEdges = workflowEdges // Create serialized workflow with filtered blocks and edges const workflow = new Serializer().serializeWorkflow(