diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx index 16b8e60d6c..8decb13cf7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select.tsx @@ -4,11 +4,8 @@ import type React from 'react' import { useMemo } from 'react' import { RepeatIcon, SplitIcon } from 'lucide-react' import { Combobox, type ComboboxOptionGroup } from '@/components/emcn' -import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { getToolOutputs } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { getBlock } from '@/blocks' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -124,42 +121,27 @@ export function OutputSelect({ : `block-${block.id}` const blockConfig = getBlock(block.type) - const responseFormatValue = - shouldUseBaseline && baselineWorkflow - ? baselineWorkflow.blocks?.[block.id]?.subBlocks?.responseFormat?.value - : subBlockValues?.[block.id]?.responseFormat - const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id) + const isTriggerCapable = blockConfig ? hasTriggerCapability(blockConfig) : false + const effectiveTriggerMode = Boolean(block.triggerMode && isTriggerCapable) let outputsToProcess: Record = {} - - if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - schemaFields.forEach((field) => { - outputsToProcess[field.name] = { type: field.type } - }) - } else { - outputsToProcess = blockConfig?.outputs || {} - } - } else { - // Build subBlocks object for tool selector - const rawSubBlockValues = - shouldUseBaseline && baselineWorkflow - ? baselineWorkflow.blocks?.[block.id]?.subBlocks - : subBlockValues?.[block.id] - const subBlocks: Record = {} - if (rawSubBlockValues && typeof rawSubBlockValues === 'object') { - for (const [key, val] of Object.entries(rawSubBlockValues)) { - // Handle both { value: ... } and raw value formats - subBlocks[key] = val && typeof val === 'object' && 'value' in val ? val : { value: val } - } + const rawSubBlockValues = + shouldUseBaseline && baselineWorkflow + ? baselineWorkflow.blocks?.[block.id]?.subBlocks + : subBlockValues?.[block.id] + const subBlocks: Record = {} + if (rawSubBlockValues && typeof rawSubBlockValues === 'object') { + for (const [key, val] of Object.entries(rawSubBlockValues)) { + // Handle both { value: ... } and raw value formats + subBlocks[key] = val && typeof val === 'object' && 'value' in val ? val : { value: val } } - - const toolOutputs = blockConfig ? getToolOutputs(blockConfig, subBlocks) : {} - outputsToProcess = - Object.keys(toolOutputs).length > 0 ? toolOutputs : blockConfig?.outputs || {} } + outputsToProcess = getEffectiveBlockOutputs(block.type, subBlocks, { + triggerMode: effectiveTriggerMode, + preferToolOutputs: !effectiveTriggerMode, + }) as Record + if (Object.keys(outputsToProcess).length === 0) return const addOutput = (path: string, outputObj: unknown, prefix = '') => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx index cf6baf554f..b2a32bb3a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/connection-blocks.tsx @@ -61,8 +61,6 @@ function ConnectionItem({ blockId: connection.id, blockType: connection.type, mergedSubBlocks, - responseFormat: connection.responseFormat, - operation: connection.operation, triggerMode: sourceBlock?.triggerMode, }) const hasFields = fields.length > 0 diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index fbec4fe0ad..0c1dbc951f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -14,16 +14,11 @@ import { } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { - getBlockOutputPaths, - getBlockOutputType, + getEffectiveBlockOutputPaths, + getEffectiveBlockOutputType, getOutputPathsFromSchema, - getToolOutputPaths, - getToolOutputType, } from '@/lib/workflows/blocks/block-outputs' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers' import { KeyboardNavigationHandler } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler' import type { @@ -214,43 +209,19 @@ const getOutputTypeForPath = ( outputPath: string, mergedSubBlocksOverride?: Record ): string => { - if (block?.triggerMode && blockConfig?.triggers?.enabled) { - return getBlockOutputType(block.type, outputPath, mergedSubBlocksOverride, true) - } - if (block?.type === 'starter') { - const startWorkflowValue = - mergedSubBlocksOverride?.startWorkflow?.value ?? getSubBlockValue(blockId, 'startWorkflow') - - if (startWorkflowValue === 'chat') { - const chatModeTypes: Record = { - input: 'string', - conversationId: 'string', - files: 'file[]', - } - return chatModeTypes[outputPath] || 'any' - } - 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 - ) - if (field?.type) return field.type - } - } else if (blockConfig?.category === 'triggers') { - const blockState = useWorkflowStore.getState().blocks[blockId] - const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {}) - return getBlockOutputType(block.type, outputPath, subBlocks) - } else if (blockConfig?.tools?.config?.tool) { - const blockState = useWorkflowStore.getState().blocks[blockId] - const subBlocks = mergedSubBlocksOverride ?? (blockState?.subBlocks || {}) - return getToolOutputType(blockConfig, subBlocks, outputPath) + if (block?.type === 'variables') { + return 'any' } const subBlocks = mergedSubBlocksOverride ?? useWorkflowStore.getState().blocks[blockId]?.subBlocks - const triggerMode = block?.triggerMode && blockConfig?.triggers?.enabled - return getBlockOutputType(block?.type ?? '', outputPath, subBlocks, triggerMode) + const isTriggerCapable = blockConfig ? hasTriggerCapability(blockConfig) : false + const triggerMode = Boolean(block?.triggerMode && isTriggerCapable) + + return getEffectiveBlockOutputType(block?.type ?? '', outputPath, subBlocks, { + triggerMode, + preferToolOutputs: !triggerMode, + }) } /** @@ -1088,24 +1059,9 @@ export const TagDropdown: React.FC = ({ const normalizedBlockName = normalizeName(blockName) const mergedSubBlocks = getMergedSubBlocks(activeSourceBlockId) - const responseFormatValue = mergedSubBlocks?.responseFormat?.value - const responseFormat = parseResponseFormatSafely(responseFormatValue, activeSourceBlockId) - let blockTags: string[] - if (sourceBlock.type === 'evaluator') { - const metricsValue = getSubBlockValue(activeSourceBlockId, 'metrics') - - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - blockTags = validMetrics.map( - (metric: { name: string }) => `${normalizedBlockName}.${metric.name.toLowerCase()}` - ) - } else { - const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (sourceBlock.type === 'variables') { + if (sourceBlock.type === 'variables') { const variablesValue = getSubBlockValue(activeSourceBlockId, 'variables') if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) { @@ -1119,106 +1075,24 @@ export const TagDropdown: React.FC = ({ } else { blockTags = [normalizedBlockName] } - } else if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - blockTags = schemaFields.map((field) => `${normalizedBlockName}.${field.name}`) - } else { - const outputPaths = getBlockOutputPaths( - sourceBlock.type, - mergedSubBlocks, - sourceBlock.triggerMode - ) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { - if (sourceBlock.type === 'starter') { - const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value - - if (startWorkflowValue === 'chat') { - blockTags = [ - `${normalizedBlockName}.input`, - `${normalizedBlockName}.conversationId`, - `${normalizedBlockName}.files`, - ] - } else { - const inputFormatValue = mergedSubBlocks?.inputFormat?.value - - 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 if (sourceBlock.type === 'api_trigger' || sourceBlock.type === 'input_trigger') { - const inputFormatValue = mergedSubBlocks?.inputFormat?.value - - 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 = [] - } - } else { - blockTags = [normalizedBlockName] - } } else { - 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 if (sourceBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK) { - blockTags = [normalizedBlockName] - } else { - blockTags = [] - } - } else if (sourceBlock?.triggerMode && blockConfig.triggers?.enabled) { - const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true) - if (dynamicOutputs.length > 0) { - blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (sourceBlock.type === 'human_in_the_loop') { - const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - - const isSelfReference = activeSourceBlockId === blockId + const sourceBlockConfig = getBlock(sourceBlock.type) + const isTriggerCapable = sourceBlockConfig ? hasTriggerCapability(sourceBlockConfig) : false + const effectiveTriggerMode = Boolean(sourceBlock.triggerMode && isTriggerCapable) + const outputPaths = getEffectiveBlockOutputPaths(sourceBlock.type, mergedSubBlocks, { + triggerMode: effectiveTriggerMode, + preferToolOutputs: !effectiveTriggerMode, + }) + const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - if (dynamicOutputs.length > 0) { - const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference - ? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')) - : allTags - } else { - const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks) - const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference - ? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')) - : allTags - } + if (sourceBlock.type === 'human_in_the_loop' && activeSourceBlockId === blockId) { + blockTags = allTags.filter( + (tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint') + ) + } else if (allTags.length === 0) { + blockTags = [normalizedBlockName] } else { - const toolOutputPaths = getToolOutputPaths(blockConfig, mergedSubBlocks) - - if (toolOutputPaths.length > 0) { - blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths( - sourceBlock.type, - mergedSubBlocks, - sourceBlock.triggerMode - ) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } + blockTags = allTags } } @@ -1432,45 +1306,10 @@ export const TagDropdown: React.FC = ({ const normalizedBlockName = normalizeName(blockName) const mergedSubBlocks = getMergedSubBlocks(accessibleBlockId) - const responseFormatValue = mergedSubBlocks?.responseFormat?.value - const responseFormat = parseResponseFormatSafely(responseFormatValue, accessibleBlockId) let blockTags: string[] - if (blockConfig.category === 'triggers' || accessibleBlock.type === 'starter') { - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) - - if (dynamicOutputs.length > 0) { - blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - } else if (accessibleBlock.type === 'starter') { - const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value - if (startWorkflowValue === 'chat') { - blockTags = [ - `${normalizedBlockName}.input`, - `${normalizedBlockName}.conversationId`, - `${normalizedBlockName}.files`, - ] - } else { - blockTags = [normalizedBlockName] - } - } else if (accessibleBlock.type === TRIGGER_TYPES.GENERIC_WEBHOOK) { - blockTags = [normalizedBlockName] - } else { - blockTags = [] - } - } else if (accessibleBlock.type === 'evaluator') { - const metricsValue = getSubBlockValue(accessibleBlockId, 'metrics') - - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - blockTags = validMetrics.map( - (metric: { name: string }) => `${normalizedBlockName}.${metric.name.toLowerCase()}` - ) - } else { - const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (accessibleBlock.type === 'variables') { + if (accessibleBlock.type === 'variables') { const variablesValue = getSubBlockValue(accessibleBlockId, 'variables') if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) { @@ -1484,57 +1323,26 @@ export const TagDropdown: React.FC = ({ } else { blockTags = [normalizedBlockName] } - } else if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - blockTags = schemaFields.map((field) => `${normalizedBlockName}.${field.name}`) - } else { - const outputPaths = getBlockOutputPaths( - accessibleBlock.type, - mergedSubBlocks, - accessibleBlock.triggerMode - ) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (!blockConfig.outputs || Object.keys(blockConfig.outputs).length === 0) { - blockTags = [normalizedBlockName] } else { - const blockState = blocks[accessibleBlockId] - if (blockState?.triggerMode && blockConfig.triggers?.enabled) { - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true) - if (dynamicOutputs.length > 0) { - blockTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true) - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } - } else if (accessibleBlock.type === 'human_in_the_loop') { - const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks) - - const isSelfReference = accessibleBlockId === blockId + const accessibleBlockConfig = getBlock(accessibleBlock.type) + const isTriggerCapable = accessibleBlockConfig + ? hasTriggerCapability(accessibleBlockConfig) + : false + const effectiveTriggerMode = Boolean(accessibleBlock.triggerMode && isTriggerCapable) + const outputPaths = getEffectiveBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, { + triggerMode: effectiveTriggerMode, + preferToolOutputs: !effectiveTriggerMode, + }) + const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - if (dynamicOutputs.length > 0) { - const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - blockTags = isSelfReference - ? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')) - : allTags - } else { - blockTags = [`${normalizedBlockName}.url`, `${normalizedBlockName}.resumeEndpoint`] - } + if (accessibleBlock.type === 'human_in_the_loop' && accessibleBlockId === blockId) { + blockTags = allTags.filter( + (tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint') + ) + } else if (allTags.length === 0) { + blockTags = [normalizedBlockName] } else { - const toolOutputPaths = getToolOutputPaths(blockConfig, mergedSubBlocks) - - if (toolOutputPaths.length > 0) { - blockTags = toolOutputPaths.map((path) => `${normalizedBlockName}.${path}`) - } else { - const outputPaths = getBlockOutputPaths( - accessibleBlock.type, - mergedSubBlocks, - accessibleBlock.triggerMode - ) - - blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) - } + blockTags = allTags } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts index a61ee158ac..a509707db7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections.ts @@ -1,10 +1,8 @@ import { useShallow } from 'zustand/react/shallow' -import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' +import { getBlock } from '@/blocks' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -20,18 +18,7 @@ export interface ConnectedBlock { type: string outputType: string | string[] name: string - responseFormat?: { - // Support both formats - fields?: Field[] - name?: string - schema?: { - type: string - properties: Record - required?: string[] - } - } outputs?: Record - operation?: string } export function useBlockConnections(blockId: string) { @@ -102,47 +89,32 @@ export function useBlockConnections(blockId: string) { // Get merged subblocks for this source block const mergedSubBlocks = getMergedSubBlocks(sourceId) + const blockConfig = getBlock(sourceBlock.type) + const isTriggerCapable = blockConfig ? hasTriggerCapability(blockConfig) : false + const effectiveTriggerMode = Boolean(sourceBlock.triggerMode && isTriggerCapable) - // Get the response format from the subblock store - const responseFormatValue = useSubBlockStore.getState().getValue(sourceId, 'responseFormat') - - // Safely parse response format with proper error handling - const responseFormat = parseResponseFormatSafely(responseFormatValue, sourceId) - - // Get operation value for tool-based blocks - const operationValue = useSubBlockStore.getState().getValue(sourceId, 'operation') + const blockOutputs = getEffectiveBlockOutputs(sourceBlock.type, mergedSubBlocks, { + triggerMode: effectiveTriggerMode, + preferToolOutputs: !effectiveTriggerMode, + }) - // Use getBlockOutputs to properly handle dynamic outputs from inputFormat - const blockOutputs = getBlockOutputs( - sourceBlock.type, - mergedSubBlocks, - sourceBlock.triggerMode - ) - - // Extract fields from the response format if available, otherwise use block outputs - let outputFields: Field[] - if (responseFormat) { - outputFields = extractFieldsFromSchema(responseFormat) - } else { - // Convert block outputs to field format - outputFields = Object.entries(blockOutputs).map(([key, value]: [string, any]) => ({ + const outputFields: Field[] = Object.entries(blockOutputs).map( + ([key, value]: [string, any]) => ({ name: key, type: value && typeof value === 'object' && 'type' in value ? value.type : 'string', description: value && typeof value === 'object' && 'description' in value ? value.description : undefined, - })) - } + }) + ) return { id: sourceBlock.id, type: sourceBlock.type, outputType: outputFields.map((field: Field) => field.name), name: sourceBlock.name, - responseFormat, outputs: blockOutputs, - operation: operationValue, distance: nodeDistances.get(sourceId) || Number.POSITIVE_INFINITY, } }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts index 233f06e58d..b8c344783a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields.ts @@ -1,13 +1,8 @@ 'use client' import { useMemo } from 'react' -import { extractFieldsFromSchema } from '@/lib/core/utils/response-format' -import { - getBlockOutputPaths, - getBlockOutputs, - getToolOutputs, -} from '@/lib/workflows/blocks/block-outputs' -import { TRIGGER_TYPES } from '@/lib/workflows/triggers/triggers' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import type { SchemaField } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item' import { getBlock } from '@/blocks' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -76,11 +71,7 @@ const extractNestedFields = (properties: Record): SchemaField[] => /** * Creates a schema field from an output definition */ -const createFieldFromOutput = ( - name: string, - output: any, - responseFormatFields?: SchemaField[] -): SchemaField => { +const createFieldFromOutput = (name: string, output: any): SchemaField => { const hasExplicitType = isObject(output) && typeof output.type === 'string' const type = hasExplicitType ? output.type : isObject(output) ? 'object' : 'string' @@ -90,11 +81,7 @@ const createFieldFromOutput = ( description: isObject(output) && 'description' in output ? output.description : undefined, } - if (name === 'data' && responseFormatFields && responseFormatFields.length > 0) { - field.children = responseFormatFields - } else { - field.children = extractChildFields(output) - } + field.children = extractChildFields(output) return field } @@ -103,8 +90,6 @@ interface UseBlockOutputFieldsParams { blockId: string blockType: string mergedSubBlocks?: Record - responseFormat?: any - operation?: string triggerMode?: boolean } @@ -116,8 +101,6 @@ export function useBlockOutputFields({ blockId, blockType, mergedSubBlocks, - responseFormat, - operation, triggerMode, }: UseBlockOutputFieldsParams): SchemaField[] { return useMemo(() => { @@ -138,21 +121,6 @@ export function useBlockOutputFields({ return [] } - // Handle evaluator blocks - use metrics if available - if (blockType === 'evaluator') { - const metricsValue = mergedSubBlocks?.metrics?.value ?? getSubBlockValue(blockId, 'metrics') - - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - return validMetrics.map((metric: { name: string }) => ({ - name: metric.name.toLowerCase(), - type: 'number', - description: `Metric: ${metric.name}`, - })) - } - // Fall through to use blockConfig.outputs - } - // Handle variables blocks - use variable assignments if available if (blockType === 'variables') { const variablesValue = @@ -172,123 +140,16 @@ export function useBlockOutputFields({ return [] } - // Get base outputs using getBlockOutputs (handles triggers, starter, approval, etc.) - let baseOutputs: Record = {} - - if (blockConfig.category === 'triggers' || blockType === 'starter') { - // Use getBlockOutputPaths to get dynamic outputs, then reconstruct the structure - const outputPaths = getBlockOutputPaths(blockType, mergedSubBlocks, triggerMode) - if (outputPaths.length > 0) { - // Reconstruct outputs structure from paths - // This is a simplified approach - we'll use the paths to build the structure - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks, triggerMode) - } else if (blockType === 'starter') { - const startWorkflowValue = mergedSubBlocks?.startWorkflow?.value - if (startWorkflowValue === 'chat') { - baseOutputs = { - input: { type: 'string', description: 'User message' }, - conversationId: { type: 'string', description: 'Conversation ID' }, - files: { type: 'file[]', description: 'Uploaded files' }, - } - } else { - const inputFormatValue = mergedSubBlocks?.inputFormat?.value - if (inputFormatValue && Array.isArray(inputFormatValue) && inputFormatValue.length > 0) { - baseOutputs = {} - inputFormatValue.forEach((field: { name?: string; type?: string }) => { - if (field.name && field.name.trim() !== '') { - baseOutputs[field.name] = { - type: field.type || 'string', - description: `Field from input format`, - } - } - }) - } - } - } else if (blockType === TRIGGER_TYPES.GENERIC_WEBHOOK) { - // Generic webhook returns the whole payload - baseOutputs = {} - } else { - baseOutputs = {} - } - } else if (triggerMode && blockConfig.triggers?.enabled) { - // Trigger mode enabled - const dynamicOutputs = getBlockOutputPaths(blockType, mergedSubBlocks, true) - if (dynamicOutputs.length > 0) { - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks, true) - } else { - baseOutputs = blockConfig.outputs || {} - } - } else if (blockType === 'approval') { - // Approval block uses dynamic outputs from inputFormat - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks) - } else { - // For tool-based blocks, try to get tool outputs first - const toolOutputs = blockConfig ? getToolOutputs(blockConfig, mergedSubBlocks) : {} - - if (Object.keys(toolOutputs).length > 0) { - baseOutputs = toolOutputs - } else { - baseOutputs = getBlockOutputs(blockType, mergedSubBlocks, triggerMode) - } - } - - // Handle responseFormat - const responseFormatFields = responseFormat ? extractFieldsFromSchema(responseFormat) : [] - - // If responseFormat exists and has fields, merge with base outputs - if (responseFormatFields.length > 0) { - // If base outputs is empty, use responseFormat fields directly - if (Object.keys(baseOutputs).length === 0) { - return responseFormatFields.map((field) => ({ - name: field.name, - type: field.type, - description: field.description, - children: undefined, // ResponseFormat fields are flat - })) - } - - // Otherwise, merge: responseFormat takes precedence for 'data' field - const fields: SchemaField[] = [] - const responseFormatFieldNames = new Set(responseFormatFields.map((f) => f.name)) - - // Add base outputs, replacing 'data' with responseFormat fields if present - for (const [name, output] of Object.entries(baseOutputs)) { - if (name === 'data' && responseFormatFields.length > 0) { - fields.push( - createFieldFromOutput( - name, - output, - responseFormatFields.map((f) => ({ - name: f.name, - type: f.type, - description: f.description, - })) - ) - ) - } else if (!responseFormatFieldNames.has(name)) { - fields.push(createFieldFromOutput(name, output)) - } - } - - // Add responseFormat fields that aren't in base outputs - for (const field of responseFormatFields) { - if (!baseOutputs[field.name]) { - fields.push({ - name: field.name, - type: field.type, - description: field.description, - }) - } - } - - return fields - } - - // No responseFormat, just use base outputs + const isTriggerCapable = hasTriggerCapability(blockConfig) + const effectiveTriggerMode = Boolean(triggerMode && isTriggerCapable) + const baseOutputs = getEffectiveBlockOutputs(blockType, mergedSubBlocks, { + triggerMode: effectiveTriggerMode, + preferToolOutputs: !effectiveTriggerMode, + }) as Record if (Object.keys(baseOutputs).length === 0) { return [] } return Object.entries(baseOutputs).map(([name, output]) => createFieldFromOutput(name, output)) - }, [blockId, blockType, mergedSubBlocks, responseFormat, operation, triggerMode]) + }, [blockId, blockType, mergedSubBlocks, triggerMode]) } diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index a5ef28d99e..3fe339b45a 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -1,8 +1,6 @@ -import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { normalizeInputFormatValue } from '@/lib/workflows/input-format' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' +import { getBlock } from '@/blocks/registry' import { isTriggerBehavior, normalizeName } from '@/executor/constants' import type { ExecutionContext } from '@/executor/types' import type { OutputSchema } from '@/executor/utils/block-reference' @@ -12,8 +10,6 @@ import { isBranchNodeId, } from '@/executor/utils/subflow-utils' import type { SerializedBlock } from '@/serializer/types' -import type { ToolConfig } from '@/tools/types' -import { getTool } from '@/tools/utils' export interface BlockDataCollection { blockData: Record @@ -21,118 +17,44 @@ export interface BlockDataCollection { blockOutputSchemas: Record } -/** - * Block types where inputFormat fields should be merged into outputs schema. - * These are blocks where users define custom fields via inputFormat that become - * valid output paths (e.g., , , ). - * - * Note: This includes non-trigger blocks like 'starter' and 'human_in_the_loop' which - * have category 'blocks' but still need their inputFormat exposed as outputs. - */ -const BLOCKS_WITH_INPUT_FORMAT_OUTPUTS = [ - 'start_trigger', - 'starter', - 'api_trigger', - 'input_trigger', - 'generic_webhook', - 'human_in_the_loop', -] as const - -function getInputFormatFields(block: SerializedBlock): OutputSchema { - const inputFormat = normalizeInputFormatValue(block.config?.params?.inputFormat) - if (inputFormat.length === 0) { - return {} - } - - const schema: OutputSchema = {} - for (const field of inputFormat) { - if (!field.name) continue - schema[field.name] = { type: field.type || 'any' } - } - - return schema -} - -function getEvaluatorMetricsSchema(block: SerializedBlock): OutputSchema | undefined { - if (block.metadata?.id !== 'evaluator') return undefined - - const metrics = block.config?.params?.metrics - if (!Array.isArray(metrics) || metrics.length === 0) return undefined - - const validMetrics = metrics.filter( - (m: { name?: string }) => m?.name && typeof m.name === 'string' - ) - if (validMetrics.length === 0) return undefined - - const schema: OutputSchema = { ...(block.outputs as OutputSchema) } - for (const metric of validMetrics) { - schema[metric.name.toLowerCase()] = { type: 'number' } - } - return schema +interface SubBlockWithValue { + value?: unknown } -function getResponseFormatSchema(block: SerializedBlock): OutputSchema | undefined { - const responseFormatValue = block.config?.params?.responseFormat - if (!responseFormatValue) return undefined +function paramsToSubBlocks( + params: Record | undefined +): Record { + if (!params) return {} - const parsed = parseResponseFormatSafely(responseFormatValue, block.id) - if (!parsed) return undefined - - const fields = extractFieldsFromSchema(parsed) - if (fields.length === 0) return undefined - - const schema: OutputSchema = {} - for (const field of fields) { - schema[field.name] = { type: field.type || 'any' } + const subBlocks: Record = {} + for (const [key, value] of Object.entries(params)) { + subBlocks[key] = { value } } - return schema + return subBlocks } -export function getBlockSchema( - block: SerializedBlock, - toolConfig?: ToolConfig -): OutputSchema | undefined { +function getRegistrySchema(block: SerializedBlock): OutputSchema | undefined { const blockType = block.metadata?.id - - if ( - blockType && - BLOCKS_WITH_INPUT_FORMAT_OUTPUTS.includes( - blockType as (typeof BLOCKS_WITH_INPUT_FORMAT_OUTPUTS)[number] - ) - ) { - const baseOutputs = (block.outputs as OutputSchema) || {} - const inputFormatFields = getInputFormatFields(block) - const merged = { ...baseOutputs, ...inputFormatFields } - if (Object.keys(merged).length > 0) { - return merged - } - } - - const evaluatorSchema = getEvaluatorMetricsSchema(block) - if (evaluatorSchema) { - return evaluatorSchema - } - - const responseFormatSchema = getResponseFormatSchema(block) - if (responseFormatSchema) { - return responseFormatSchema - } - - const isTrigger = isTriggerBehavior(block) - - if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) { - return block.outputs as OutputSchema - } - - if (toolConfig?.outputs && Object.keys(toolConfig.outputs).length > 0) { - return toolConfig.outputs as OutputSchema - } - - if (block.outputs && Object.keys(block.outputs).length > 0) { - return block.outputs as OutputSchema + if (!blockType) return undefined + + const subBlocks = paramsToSubBlocks(block.config?.params) + const blockConfig = getBlock(blockType) + const isTriggerCapable = blockConfig ? hasTriggerCapability(blockConfig) : false + const triggerMode = Boolean(isTriggerBehavior(block) && isTriggerCapable) + const outputs = getEffectiveBlockOutputs(blockType, subBlocks, { + triggerMode, + preferToolOutputs: !triggerMode, + includeHidden: true, + }) as OutputSchema + + if (!outputs || Object.keys(outputs).length === 0) { + return undefined } + return outputs +} - return undefined +export function getBlockSchema(block: SerializedBlock): OutputSchema | undefined { + return getRegistrySchema(block) } export function collectBlockData( @@ -170,9 +92,7 @@ export function collectBlockData( blockNameMapping[normalizeName(block.metadata.name)] = id } - const toolId = block.config?.tool - const toolConfig = toolId ? getTool(toolId) : undefined - const schema = getBlockSchema(block, toolConfig) + const schema = getBlockSchema(block) if (schema && Object.keys(schema).length > 0) { blockOutputSchemas[id] = schema } diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts index 01a804900f..f3befe4a86 100644 --- a/apps/sim/executor/variables/resolvers/block.test.ts +++ b/apps/sim/executor/variables/resolvers/block.test.ts @@ -5,10 +5,10 @@ import { BlockResolver } from './block' import type { ResolutionContext } from './reference' vi.mock('@sim/logger', () => loggerMock) - -vi.mock('@/lib/workflows/blocks/block-outputs', () => ({ - getBlockOutputs: vi.fn(() => ({})), -})) +vi.mock('@/blocks/registry', async () => { + const actual = await vi.importActual('@/blocks/registry') + return actual +}) function createTestWorkflow( blocks: Array<{ @@ -135,7 +135,7 @@ describe('BlockResolver', () => { }) it.concurrent('should return undefined for non-existent path when no schema defined', () => { - const workflow = createTestWorkflow([{ id: 'source' }]) + const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }]) const resolver = new BlockResolver(workflow) const ctx = createTestContext('current', { source: { existing: 'value' }, @@ -144,55 +144,93 @@ describe('BlockResolver', () => { expect(resolver.resolve('', ctx)).toBeUndefined() }) - it.concurrent('should throw error for path not in output schema', async () => { - const { getBlockOutputs } = await import('@/lib/workflows/blocks/block-outputs') - const mockGetBlockOutputs = vi.mocked(getBlockOutputs) - const customOutputs = { - validField: { type: 'string', description: 'A valid field' }, - nested: { - child: { type: 'number', description: 'Nested child' }, - }, - } - mockGetBlockOutputs.mockReturnValue(customOutputs as any) - + it.concurrent('should throw error for path not in output schema', () => { const workflow = createTestWorkflow([ { id: 'source', - outputs: customOutputs, + type: 'start_trigger', }, ]) const resolver = new BlockResolver(workflow) const ctx = createTestContext('current', { - source: { validField: 'value', nested: { child: 42 } }, + source: { input: 'value' }, }) expect(() => resolver.resolve('', ctx)).toThrow( /"invalidField" doesn't exist on block "source"/ ) expect(() => resolver.resolve('', ctx)).toThrow(/Available fields:/) - - mockGetBlockOutputs.mockReturnValue({}) }) it.concurrent('should return undefined for path in schema but missing in data', () => { const workflow = createTestWorkflow([ { id: 'source', - outputs: { - requiredField: { type: 'string', description: 'Always present' }, - optionalField: { type: 'string', description: 'Sometimes missing' }, - }, + type: 'function', }, ]) const resolver = new BlockResolver(workflow) const ctx = createTestContext('current', { - source: { requiredField: 'value' }, + source: { stdout: 'log output' }, }) - expect(resolver.resolve('', ctx)).toBe('value') - expect(resolver.resolve('', ctx)).toBeUndefined() + expect(resolver.resolve('', ctx)).toBe('log output') + expect(resolver.resolve('', ctx)).toBeUndefined() }) + it.concurrent( + 'should allow hiddenFromDisplay fields for pre-execution schema validation', + () => { + const workflow = createTestWorkflow([ + { + id: 'workflow-block', + name: 'Workflow', + type: 'workflow', + }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', {}) + + expect(resolver.resolve('', ctx)).toBeUndefined() + } + ) + + it.concurrent( + 'should allow hiddenFromDisplay fields for workflow_input pre-execution schema validation', + () => { + const workflow = createTestWorkflow([ + { + id: 'workflow-input-block', + name: 'Workflow Input', + type: 'workflow_input', + }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', {}) + + expect(resolver.resolve('', ctx)).toBeUndefined() + } + ) + + it.concurrent( + 'should allow hiddenFromDisplay fields for HITL pre-execution schema validation', + () => { + const workflow = createTestWorkflow([ + { + id: 'hitl-block', + name: 'HITL', + type: 'human_in_the_loop', + }, + ]) + const resolver = new BlockResolver(workflow) + const ctx = createTestContext('current', {}) + + expect(resolver.resolve('', ctx)).toBeUndefined() + expect(resolver.resolve('', ctx)).toBeUndefined() + expect(resolver.resolve('', ctx)).toBeUndefined() + } + ) + it.concurrent('should return undefined for non-existent block', () => { const workflow = createTestWorkflow([{ id: 'existing' }]) const resolver = new BlockResolver(workflow) @@ -975,7 +1013,7 @@ describe('BlockResolver', () => { }) it.concurrent('should handle output with undefined values', () => { - const workflow = createTestWorkflow([{ id: 'source' }]) + const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }]) const resolver = new BlockResolver(workflow) const ctx = createTestContext('current', { source: { value: undefined, other: 'exists' }, @@ -985,7 +1023,7 @@ describe('BlockResolver', () => { }) it.concurrent('should return undefined for deeply nested non-existent path', () => { - const workflow = createTestWorkflow([{ id: 'source' }]) + const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }]) const resolver = new BlockResolver(workflow) const ctx = createTestContext('current', { source: { level1: { level2: {} } }, diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 63ab361381..1b4335e594 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -17,7 +17,6 @@ import { type Resolver, } from '@/executor/variables/resolvers/reference' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' -import { getTool } from '@/tools/utils' export class BlockResolver implements Resolver { private nameToBlockId: Map @@ -68,9 +67,7 @@ export class BlockResolver implements Resolver { blockData[blockId] = output } - const toolId = block.config?.tool - const toolConfig = toolId ? getTool(toolId) : undefined - const outputSchema = getBlockSchema(block, toolConfig) + const outputSchema = getBlockSchema(block) if (outputSchema && Object.keys(outputSchema).length > 0) { blockOutputSchemas[blockId] = outputSchema diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts index ea8ce31873..efd9d4c367 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/queries.ts @@ -8,13 +8,15 @@ import { } from '@/lib/copilot/tools/shared/workflow-utils' import { mcpService } from '@/lib/mcp/service' import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace' -import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs' import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator' import { loadDeployedWorkflowState, loadWorkflowFromNormalizedTables, } from '@/lib/workflows/persistence/utils' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' +import { getBlock } from '@/blocks/registry' import { normalizeName } from '@/executor/constants' import type { Loop, Parallel } from '@/stores/workflows/workflow/types' import { @@ -343,7 +345,13 @@ export async function executeGetBlockOutputs( continue } - const outputs = getBlockOutputPaths(block.type, block.subBlocks, block.triggerMode) + const blockConfig = getBlock(block.type) + const isTriggerCapable = blockConfig ? hasTriggerCapability(blockConfig) : false + const triggerMode = Boolean(block.triggerMode && isTriggerCapable) + const outputs = getEffectiveBlockOutputPaths(block.type, block.subBlocks, { + triggerMode, + preferToolOutputs: !triggerMode, + }) results.push({ blockId, blockName, @@ -485,7 +493,13 @@ export async function executeGetBlockUpstreamReferences( ? getSubflowInsidePaths(block.type, accessibleBlockId, loops, parallels) : ['results'] } else { - outputPaths = getBlockOutputPaths(block.type, block.subBlocks, block.triggerMode) + const blockConfig = getBlock(block.type) + const isTriggerCapable = blockConfig ? hasTriggerCapability(blockConfig) : false + const triggerMode = Boolean(block.triggerMode && isTriggerCapable) + outputPaths = getEffectiveBlockOutputPaths(block.type, block.subBlocks, { + triggerMode, + preferToolOutputs: !triggerMode, + }) } const formattedOutputs = formatOutputsWithPrefix(outputPaths, blockName) 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 7bcff1fee3..ed6b60acbc 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 @@ -232,7 +232,11 @@ export const getBlocksMetadataServerTool: BaseServerTool< const resolvedToolId = resolveToolIdForOperation(blockConfig, opId) const toolCfg = resolvedToolId ? toolsRegistry[resolvedToolId] : undefined const toolParams: Record = toolCfg?.params || {} - const toolOutputs: Record = toolCfg?.outputs || {} + const toolOutputs: Record = toolCfg?.outputs + ? Object.fromEntries( + Object.entries(toolCfg.outputs).filter(([_, def]) => !isHiddenFromDisplay(def)) + ) + : {} const filteredToolParams: Record = {} for (const [k, v] of Object.entries(toolParams)) { if (!(k in blockInputs)) filteredToolParams[k] = v diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/builders.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/builders.test.ts new file mode 100644 index 0000000000..a83a7efd47 --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/builders.test.ts @@ -0,0 +1,44 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { createBlockFromParams } from './builders' + +const agentBlockConfig = { + type: 'agent', + name: 'Agent', + outputs: { + content: { type: 'string', description: 'Default content output' }, + }, + subBlocks: [{ id: 'responseFormat', type: 'response-format' }], +} + +vi.mock('@/blocks/registry', () => ({ + getAllBlocks: () => [agentBlockConfig], + getBlock: (type: string) => (type === 'agent' ? agentBlockConfig : undefined), +})) + +describe('createBlockFromParams', () => { + it('derives agent outputs from responseFormat when outputs are not provided', () => { + const block = createBlockFromParams('b-agent', { + type: 'agent', + name: 'Agent', + inputs: { + responseFormat: { + type: 'object', + properties: { + answer: { + type: 'string', + description: 'Structured answer text', + }, + }, + required: ['answer'], + }, + }, + triggerMode: false, + }) + + expect(block.outputs.answer).toBeDefined() + expect(block.outputs.answer.type).toBe('string') + }) +}) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/builders.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/builders.ts index 935e7bceee..529a0bc780 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/builders.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/builders.ts @@ -1,8 +1,9 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' -import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { buildCanonicalIndex, isCanonicalPair } from '@/lib/workflows/subblocks/visibility' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { getAllBlocks } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' @@ -39,6 +40,8 @@ export function createBlockFromParams( // Determine outputs based on trigger mode const triggerMode = params.triggerMode || false + const isTriggerCapable = blockConfig ? hasTriggerCapability(blockConfig) : false + const effectiveTriggerMode = Boolean(triggerMode && isTriggerCapable) let outputs: Record if (params.outputs) { @@ -54,7 +57,10 @@ export function createBlockFromParams( subBlocks[key] = { id: key, type: 'short-input', value: value } }) } - outputs = getBlockOutputs(params.type, subBlocks, triggerMode) + outputs = getEffectiveBlockOutputs(params.type, subBlocks, { + triggerMode: effectiveTriggerMode, + preferToolOutputs: !effectiveTriggerMode, + }) } else { outputs = {} } diff --git a/apps/sim/lib/workflows/blocks/block-outputs.test.ts b/apps/sim/lib/workflows/blocks/block-outputs.test.ts new file mode 100644 index 0000000000..ccd2b22e72 --- /dev/null +++ b/apps/sim/lib/workflows/blocks/block-outputs.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest' +import { + getEffectiveBlockOutputPaths, + getEffectiveBlockOutputs, + getEffectiveBlockOutputType, +} from '@/lib/workflows/blocks/block-outputs' + +type SubBlocks = Record + +function rootPaths(paths: string[]): string[] { + return [...new Set(paths.map((path) => path.split('.')[0]).filter(Boolean))].sort() +} + +describe('block outputs parity', () => { + it.concurrent('keeps evaluator tag paths and types aligned', () => { + const subBlocks: SubBlocks = { + metrics: { + value: [ + { + name: 'Accuracy', + description: 'How accurate the answer is', + range: { min: 0, max: 1 }, + }, + { + name: 'Relevance', + description: 'How relevant the answer is', + range: { min: 0, max: 1 }, + }, + ], + }, + } + + const options = { triggerMode: false, preferToolOutputs: true } + const outputs = getEffectiveBlockOutputs('evaluator', subBlocks, options) + const paths = getEffectiveBlockOutputPaths('evaluator', subBlocks, options) + + expect(rootPaths(paths)).toEqual(Object.keys(outputs).sort()) + expect(paths).toContain('accuracy') + expect(paths).toContain('relevance') + expect(getEffectiveBlockOutputType('evaluator', 'accuracy', subBlocks, options)).toBe('number') + expect(getEffectiveBlockOutputType('evaluator', 'relevance', subBlocks, options)).toBe('number') + }) + + it.concurrent('keeps agent responseFormat tag paths and types aligned', () => { + const subBlocks: SubBlocks = { + responseFormat: { + value: { + name: 'calculator_output', + schema: { + type: 'object', + properties: { + min: { type: 'number' }, + max: { type: 'number' }, + }, + required: ['min', 'max'], + additionalProperties: false, + }, + strict: true, + }, + }, + } + + const options = { triggerMode: false, preferToolOutputs: true } + const outputs = getEffectiveBlockOutputs('agent', subBlocks, options) + const paths = getEffectiveBlockOutputPaths('agent', subBlocks, options) + + expect(rootPaths(paths)).toEqual(Object.keys(outputs).sort()) + expect(paths).toContain('min') + expect(paths).toContain('max') + expect(getEffectiveBlockOutputType('agent', 'min', subBlocks, options)).toBe('number') + expect(getEffectiveBlockOutputType('agent', 'max', subBlocks, options)).toBe('number') + }) +}) diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index edb95fdf0f..15fb4161b7 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -33,6 +33,12 @@ interface SubBlockWithValue { value?: unknown } +interface EffectiveOutputOptions { + triggerMode?: boolean + preferToolOutputs?: boolean + includeHidden?: boolean +} + type ConditionValue = string | number | boolean /** @@ -96,12 +102,13 @@ function evaluateOutputCondition( */ function filterOutputsByCondition( outputs: OutputDefinition, - subBlocks: Record | undefined + subBlocks: Record | undefined, + includeHidden = false ): OutputDefinition { const filtered: OutputDefinition = {} for (const [key, value] of Object.entries(outputs)) { - if (isHiddenFromDisplay(value)) continue + if (!includeHidden && isHiddenFromDisplay(value)) continue if (!value || typeof value !== 'object' || !('condition' in value)) { filtered[key] = value @@ -112,8 +119,13 @@ function filterOutputsByCondition( const passes = !condition || evaluateOutputCondition(condition, subBlocks) if (passes) { - const { condition: _, hiddenFromDisplay: __, ...rest } = value - filtered[key] = rest + if (includeHidden) { + const { condition: _, ...rest } = value + filtered[key] = rest + } else { + const { condition: _, hiddenFromDisplay: __, ...rest } = value + filtered[key] = rest + } } } @@ -243,8 +255,10 @@ function applyInputFormatToOutputs( export function getBlockOutputs( blockType: string, subBlocks?: Record, - triggerMode?: boolean + triggerMode?: boolean, + options?: { includeHidden?: boolean } ): OutputDefinition { + const includeHidden = options?.includeHidden ?? false const blockConfig = getBlock(blockType) if (!blockConfig) return {} @@ -269,7 +283,8 @@ export function getBlockOutputs( // Start with block config outputs (respects hiddenFromDisplay via filterOutputsByCondition) const baseOutputs = filterOutputsByCondition( { ...(blockConfig.outputs || {}) } as OutputDefinition, - subBlocks + subBlocks, + includeHidden ) // Add inputFormat fields (resume form fields) @@ -292,29 +307,111 @@ export function getBlockOutputs( return getLegacyStarterOutputs(subBlocks) } + const baseOutputs = { ...(blockConfig.outputs || {}) } + const filteredOutputs = filterOutputsByCondition(baseOutputs, subBlocks, includeHidden) + return applyInputFormatToOutputs(blockType, blockConfig, subBlocks, filteredOutputs) +} + +export function getResponseFormatOutputs( + subBlocks?: Record, + blockId = 'block' +): OutputDefinition | undefined { + const responseFormatValue = subBlocks?.responseFormat?.value + if (!responseFormatValue) return undefined + + const parsed = parseResponseFormatSafely(responseFormatValue, blockId) + if (!parsed) return undefined + + const fields = extractFieldsFromSchema(parsed) + if (fields.length === 0) return undefined + + const outputs: OutputDefinition = {} + for (const field of fields) { + outputs[field.name] = { + type: (field.type || 'any') as any, + description: field.description || `Field from Agent: ${field.name}`, + } + } + + return outputs +} + +export function getEvaluatorMetricOutputs( + subBlocks?: Record +): OutputDefinition | undefined { + const metricsValue = subBlocks?.metrics?.value + if (!metricsValue || !Array.isArray(metricsValue) || metricsValue.length === 0) return undefined + + const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) + if (validMetrics.length === 0) return undefined + + const outputs: OutputDefinition = {} + for (const metric of validMetrics as Array<{ name: string }>) { + outputs[metric.name.toLowerCase()] = { + type: 'number', + description: `Metric score: ${metric.name}`, + } + } + + return outputs +} + +export function getEffectiveBlockOutputs( + blockType: string, + subBlocks?: Record, + options?: EffectiveOutputOptions +): OutputDefinition { + const triggerMode = options?.triggerMode ?? false + const preferToolOutputs = options?.preferToolOutputs ?? !triggerMode + const includeHidden = options?.includeHidden ?? false + if (blockType === 'agent') { - const responseFormatValue = subBlocks?.responseFormat?.value - if (responseFormatValue) { - const parsed = parseResponseFormatSafely(responseFormatValue, 'agent') - if (parsed) { - const fields = extractFieldsFromSchema(parsed) - if (fields.length > 0) { - const outputs: OutputDefinition = {} - for (const field of fields) { - outputs[field.name] = { - type: (field.type || 'any') as any, - description: field.description || `Field from Agent: ${field.name}`, - } - } - return outputs - } - } + const responseFormatOutputs = getResponseFormatOutputs(subBlocks, 'agent') + if (responseFormatOutputs) return responseFormatOutputs + } + + let baseOutputs: OutputDefinition + if (triggerMode) { + baseOutputs = getBlockOutputs(blockType, subBlocks, true, { includeHidden }) + } else if (preferToolOutputs) { + const blockConfig = getBlock(blockType) + const toolOutputs = blockConfig + ? (getToolOutputs(blockConfig, subBlocks, { includeHidden }) as OutputDefinition) + : {} + baseOutputs = + toolOutputs && Object.keys(toolOutputs).length > 0 + ? toolOutputs + : getBlockOutputs(blockType, subBlocks, false, { includeHidden }) + } else { + baseOutputs = getBlockOutputs(blockType, subBlocks, false, { includeHidden }) + } + + if (blockType === 'evaluator') { + const metricOutputs = getEvaluatorMetricOutputs(subBlocks) + if (metricOutputs) { + return { ...baseOutputs, ...metricOutputs } } } - const baseOutputs = { ...(blockConfig.outputs || {}) } - const filteredOutputs = filterOutputsByCondition(baseOutputs, subBlocks) - return applyInputFormatToOutputs(blockType, blockConfig, subBlocks, filteredOutputs) + return baseOutputs +} + +export function getEffectiveBlockOutputPaths( + blockType: string, + subBlocks?: Record, + options?: EffectiveOutputOptions +): string[] { + const outputs = getEffectiveBlockOutputs(blockType, subBlocks, options) + const paths = generateOutputPaths(outputs) + + if (blockType === TRIGGER_TYPES.START) { + return paths.filter((path) => { + const key = path.split('.')[0] + return !shouldFilterReservedField(blockType, key, '', subBlocks) + }) + } + + return paths } function shouldFilterReservedField( @@ -352,24 +449,6 @@ function isFileOutputDefinition(value: unknown): value is { type: FileOutputType return type === 'file' || type === 'file[]' } -export function getBlockOutputPaths( - blockType: string, - subBlocks?: Record, - triggerMode?: boolean -): string[] { - const outputs = getBlockOutputs(blockType, subBlocks, triggerMode) - const paths = generateOutputPaths(outputs) - - if (blockType === TRIGGER_TYPES.START) { - return paths.filter((path) => { - const key = path.split('.')[0] - return !shouldFilterReservedField(blockType, key, '', subBlocks) - }) - } - - return paths -} - function getFilePropertyType(outputs: OutputDefinition, pathParts: string[]): string | null { const lastPart = pathParts[pathParts.length - 1] if (!lastPart || !USER_FILE_PROPERTY_TYPES[lastPart as keyof typeof USER_FILE_PROPERTY_TYPES]) { @@ -453,13 +532,13 @@ function extractType(value: unknown): string { return typeof value === 'string' ? value : 'any' } -export function getBlockOutputType( +export function getEffectiveBlockOutputType( blockType: string, outputPath: string, subBlocks?: Record, - triggerMode?: boolean + options?: EffectiveOutputOptions ): string { - const outputs = getBlockOutputs(blockType, subBlocks, triggerMode) + const outputs = getEffectiveBlockOutputs(blockType, subBlocks, options) const cleanPath = outputPath.replace(/\[(\d+)\]/g, '') const pathParts = cleanPath.split('.').filter(Boolean) @@ -531,60 +610,6 @@ function generateOutputPaths(outputs: Record, prefix = ''): string[ return paths } -/** - * Recursively generates all output paths with their types from an outputs schema. - * - * @param outputs - The outputs schema object - * @param prefix - Current path prefix for recursion - * @returns Array of objects containing path and type for each output field - */ -function generateOutputPathsWithTypes( - outputs: Record, - prefix = '' -): Array<{ path: string; type: string }> { - const paths: Array<{ path: string; type: string }> = [] - - for (const [key, value] of Object.entries(outputs)) { - const currentPath = prefix ? `${prefix}.${key}` : key - - if (typeof value === 'string') { - paths.push({ path: currentPath, type: value }) - } else if (typeof value === 'object' && value !== null) { - if ('type' in value && typeof value.type === 'string') { - if (isFileOutputDefinition(value)) { - paths.push({ path: currentPath, type: value.type }) - for (const prop of USER_FILE_ACCESSIBLE_PROPERTIES) { - paths.push({ - path: `${currentPath}.${prop}`, - type: USER_FILE_PROPERTY_TYPES[prop as keyof typeof USER_FILE_PROPERTY_TYPES], - }) - } - continue - } - - if (value.type === 'array' && value.items?.properties) { - paths.push({ path: currentPath, type: 'array' }) - const subPaths = generateOutputPathsWithTypes(value.items.properties, currentPath) - paths.push(...subPaths) - } else if ((value.type === 'object' || value.type === 'json') && value.properties) { - paths.push({ path: currentPath, type: value.type }) - const subPaths = generateOutputPathsWithTypes(value.properties, currentPath) - paths.push(...subPaths) - } else { - paths.push({ path: currentPath, type: value.type }) - } - } else { - const subPaths = generateOutputPathsWithTypes(value, currentPath) - paths.push(...subPaths) - } - } else { - paths.push({ path: currentPath, type: 'any' }) - } - } - - return paths -} - /** * Gets the tool outputs for a block operation. * @@ -594,8 +619,10 @@ function generateOutputPathsWithTypes( */ export function getToolOutputs( blockConfig: BlockConfig, - subBlocks?: Record + subBlocks?: Record, + options?: { includeHidden?: boolean } ): Record { + const includeHidden = options?.includeHidden ?? false if (!blockConfig?.tools?.config?.tool) return {} try { @@ -613,49 +640,18 @@ export function getToolOutputs( const toolConfig = getTool(toolId) if (!toolConfig?.outputs) return {} - - return toolConfig.outputs + if (includeHidden) { + return toolConfig.outputs + } + return Object.fromEntries( + Object.entries(toolConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def)) + ) } catch (error) { logger.warn('Failed to get tool outputs', { error }) return {} } } -export function getToolOutputPaths( - blockConfig: BlockConfig, - subBlocks?: Record -): string[] { - const outputs = getToolOutputs(blockConfig, subBlocks) - - if (!outputs || Object.keys(outputs).length === 0) return [] - - if (subBlocks && blockConfig.outputs) { - const filteredOutputs: Record = {} - - for (const [key, value] of Object.entries(outputs)) { - const blockOutput = blockConfig.outputs[key] - - if (!blockOutput || typeof blockOutput !== 'object') { - filteredOutputs[key] = value - continue - } - - const condition = 'condition' in blockOutput ? blockOutput.condition : undefined - if (condition) { - if (evaluateOutputCondition(condition, subBlocks)) { - filteredOutputs[key] = value - } - } else { - filteredOutputs[key] = value - } - } - - return generateOutputPaths(filteredOutputs) - } - - return generateOutputPaths(outputs) -} - /** * Generates output paths from a schema definition. * @@ -665,24 +661,3 @@ export function getToolOutputPaths( export function getOutputPathsFromSchema(outputs: Record): string[] { return generateOutputPaths(outputs) } - -/** - * Gets the output type for a specific path in a tool's outputs. - * - * @param blockConfig - The block configuration containing tools config - * @param subBlocks - SubBlock values for tool selection - * @param path - The dot-separated path to the output field - * @returns The type of the output field, or 'any' if not found - */ -export function getToolOutputType( - blockConfig: BlockConfig, - subBlocks: Record | undefined, - path: string -): string { - const outputs = getToolOutputs(blockConfig, subBlocks) - if (!outputs || Object.keys(outputs).length === 0) return 'any' - - const pathsWithTypes = generateOutputPathsWithTypes(outputs) - const matchingPath = pathsWithTypes.find((p) => p.path === path) - return matchingPath?.type || 'any' -} diff --git a/apps/sim/lib/workflows/defaults.ts b/apps/sim/lib/workflows/defaults.ts index d93dd0b135..7de0c058a7 100644 --- a/apps/sim/lib/workflows/defaults.ts +++ b/apps/sim/lib/workflows/defaults.ts @@ -1,4 +1,4 @@ -import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { getBlock } from '@/blocks' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import type { BlockState, SubBlockState, WorkflowState } from '@/stores/workflows/workflow/types' @@ -85,7 +85,10 @@ function buildStartBlockState( subBlockValues[config.id] = initialValue ?? null }) - const outputs = getBlockOutputs(blockConfig.type, subBlocks) + const outputs = getEffectiveBlockOutputs(blockConfig.type, subBlocks, { + triggerMode: false, + preferToolOutputs: true, + }) const blockState: BlockState = { id: blockId, diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index 03039b7f81..aae0f248cd 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -1,8 +1,9 @@ import type { Edge } from 'reactflow' import { v4 as uuidv4 } from 'uuid' import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants' -import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks' +import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' import { getBlock } from '@/blocks' import { isAnnotationOnlyBlock, normalizeName } from '@/executor/constants' @@ -188,7 +189,12 @@ export function prepareBlockState(options: PrepareBlockStateOptions): BlockState }) } - const outputs = getBlockOutputs(type, subBlocks, triggerMode) + const isTriggerCapable = hasTriggerCapability(blockConfig) + const effectiveTriggerMode = Boolean(triggerMode && isTriggerCapable) + const outputs = getEffectiveBlockOutputs(type, subBlocks, { + triggerMode: effectiveTriggerMode, + preferToolOutputs: !effectiveTriggerMode, + }) return { id,