From 9192769e552c842460727c8de3171e2258df6cf1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 14:23:50 -0800 Subject: [PATCH 1/3] fix input format --- apps/sim/executor/utils/block-data.ts | 52 +++++++++++++++++-- .../sim/lib/workflows/comparison/normalize.ts | 6 +-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index 0c7ec4bd1f..a6e3089706 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -1,3 +1,4 @@ +import { normalizeInputFormatValue } from '@/lib/workflows/input-format' import { normalizeName } from '@/executor/constants' import type { ExecutionContext } from '@/executor/types' import type { OutputSchema } from '@/executor/utils/block-reference' @@ -11,6 +12,37 @@ export interface BlockDataCollection { blockOutputSchemas: Record } +/** + * Triggers 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., , ). + */ +const TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS = [ + 'start_trigger', + 'starter', + 'api_trigger', + 'input_trigger', + 'generic_webhook', + 'human_in_the_loop', + 'approval', +] 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) { + schema[field.name!] = { + type: (field.type || 'any') as 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any', + } + } + + return schema +} + export function getBlockSchema( block: SerializedBlock, toolConfig?: ToolConfig @@ -19,17 +51,31 @@ export function getBlockSchema( block.metadata?.category === 'triggers' || (block.config?.params as Record | undefined)?.triggerMode === true - // Triggers use saved outputs (defines the trigger payload schema) + const blockType = block.metadata?.id + + if ( + isTrigger && + blockType && + TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS.includes( + blockType as (typeof TRIGGERS_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 + } + } + if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) { return block.outputs as OutputSchema } - // When a tool is selected, tool outputs are the source of truth if (toolConfig?.outputs && Object.keys(toolConfig.outputs).length > 0) { return toolConfig.outputs as OutputSchema } - // Fallback to saved outputs for blocks without tools if (block.outputs && Object.keys(block.outputs).length > 0) { return block.outputs as OutputSchema } diff --git a/apps/sim/lib/workflows/comparison/normalize.ts b/apps/sim/lib/workflows/comparison/normalize.ts index 571f201138..c467f73e03 100644 --- a/apps/sim/lib/workflows/comparison/normalize.ts +++ b/apps/sim/lib/workflows/comparison/normalize.ts @@ -156,10 +156,10 @@ export function normalizeVariables(variables: unknown): Record } /** Input format item with optional UI-only fields */ -type InputFormatItem = Record & { value?: unknown; collapsed?: boolean } +type InputFormatItem = Record & { collapsed?: boolean } /** - * Sanitizes inputFormat array by removing UI-only fields like value and collapsed + * Sanitizes inputFormat array by removing UI-only fields like collapsed * @param inputFormat - Array of input format configurations * @returns Sanitized input format array */ @@ -167,7 +167,7 @@ export function sanitizeInputFormat(inputFormat: unknown[] | undefined): Record< if (!Array.isArray(inputFormat)) return [] return inputFormat.map((item) => { if (item && typeof item === 'object' && !Array.isArray(item)) { - const { value, collapsed, ...rest } = item as InputFormatItem + const { collapsed, ...rest } = item as InputFormatItem return rest } return item as Record From 05451f2384f6da0da0791e0009cb76cbf0783e86 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 14:41:18 -0800 Subject: [PATCH 2/3] fix tests --- apps/sim/executor/utils/block-data.ts | 3 +- .../lib/workflows/comparison/compare.test.ts | 73 +++++++++++++++++-- .../workflows/comparison/normalize.test.ts | 9 ++- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index a6e3089706..07864fca4b 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -35,7 +35,8 @@ function getInputFormatFields(block: SerializedBlock): OutputSchema { const schema: OutputSchema = {} for (const field of inputFormat) { - schema[field.name!] = { + if (!field.name) continue + schema[field.name] = { type: (field.type || 'any') as 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any', } } diff --git a/apps/sim/lib/workflows/comparison/compare.test.ts b/apps/sim/lib/workflows/comparison/compare.test.ts index 31af020e30..db4fb6fd08 100644 --- a/apps/sim/lib/workflows/comparison/compare.test.ts +++ b/apps/sim/lib/workflows/comparison/compare.test.ts @@ -557,7 +557,8 @@ describe('hasWorkflowChanged', () => { }) describe('InputFormat SubBlock Special Handling', () => { - it.concurrent('should ignore value and collapsed fields in inputFormat', () => { + it.concurrent('should ignore collapsed field but detect value changes in inputFormat', () => { + // Only collapsed changes - should NOT detect as change const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { @@ -578,8 +579,8 @@ describe('hasWorkflowChanged', () => { subBlocks: { inputFormat: { value: [ - { id: 'input1', name: 'Name', value: 'Jane', collapsed: false }, - { id: 'input2', name: 'Age', value: 30, collapsed: true }, + { id: 'input1', name: 'Name', value: 'John', collapsed: false }, + { id: 'input2', name: 'Age', value: 25, collapsed: true }, ], }, }, @@ -589,6 +590,32 @@ describe('hasWorkflowChanged', () => { expect(hasWorkflowChanged(state1, state2)).toBe(false) }) + it.concurrent('should detect value changes in inputFormat', () => { + const state1 = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + subBlocks: { + inputFormat: { + value: [{ id: 'input1', name: 'Name', value: 'John' }], + }, + }, + }), + }, + }) + const state2 = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + subBlocks: { + inputFormat: { + value: [{ id: 'input1', name: 'Name', value: 'Jane' }], + }, + }, + }), + }, + }) + expect(hasWorkflowChanged(state1, state2)).toBe(true) + }) + it.concurrent('should detect actual inputFormat changes', () => { const state1 = createWorkflowState({ blocks: { @@ -1712,15 +1739,15 @@ describe('hasWorkflowChanged', () => { }) describe('Input Format Field Scenarios', () => { - it.concurrent('should not detect change when inputFormat value is typed and cleared', () => { - // The "value" field in inputFormat is UI-only and should be ignored + it.concurrent('should not detect change when only inputFormat collapsed changes', () => { + // The "collapsed" field in inputFormat is UI-only and should be ignored const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [ - { id: 'field1', name: 'Name', type: 'string', value: '', collapsed: false }, + { id: 'field1', name: 'Name', type: 'string', value: 'test', collapsed: false }, ], }, }, @@ -1738,7 +1765,7 @@ describe('hasWorkflowChanged', () => { id: 'field1', name: 'Name', type: 'string', - value: 'typed then cleared', + value: 'test', collapsed: true, }, ], @@ -1748,10 +1775,40 @@ describe('hasWorkflowChanged', () => { }, }) - // value and collapsed are UI-only fields - should NOT detect as change + // collapsed is UI-only field - should NOT detect as change expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) + it.concurrent('should detect change when inputFormat value changes', () => { + // The "value" field in inputFormat is meaningful and should trigger change detection + const deployedState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + subBlocks: { + inputFormat: { + value: [{ id: 'field1', name: 'Name', type: 'string', value: '' }], + }, + }, + }), + }, + }) + + const currentState = createWorkflowState({ + blocks: { + block1: createBlock('block1', { + subBlocks: { + inputFormat: { + value: [{ id: 'field1', name: 'Name', type: 'string', value: 'new value' }], + }, + }, + }), + }, + }) + + // value changes should be detected + expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) + }) + it.concurrent('should detect change when inputFormat field name changes', () => { const deployedState = createWorkflowState({ blocks: { diff --git a/apps/sim/lib/workflows/comparison/normalize.test.ts b/apps/sim/lib/workflows/comparison/normalize.test.ts index ca22205876..66bf52f342 100644 --- a/apps/sim/lib/workflows/comparison/normalize.test.ts +++ b/apps/sim/lib/workflows/comparison/normalize.test.ts @@ -370,7 +370,7 @@ describe('Workflow Normalization Utilities', () => { expect(sanitizeInputFormat({} as any)).toEqual([]) }) - it.concurrent('should remove value and collapsed fields', () => { + it.concurrent('should remove collapsed field but keep value', () => { const inputFormat = [ { id: 'input1', name: 'Name', value: 'John', collapsed: true }, { id: 'input2', name: 'Age', value: 25, collapsed: false }, @@ -379,13 +379,13 @@ describe('Workflow Normalization Utilities', () => { const result = sanitizeInputFormat(inputFormat) expect(result).toEqual([ - { id: 'input1', name: 'Name' }, - { id: 'input2', name: 'Age' }, + { id: 'input1', name: 'Name', value: 'John' }, + { id: 'input2', name: 'Age', value: 25 }, { id: 'input3', name: 'Email' }, ]) }) - it.concurrent('should preserve all other fields', () => { + it.concurrent('should preserve all other fields including value', () => { const inputFormat = [ { id: 'input1', @@ -402,6 +402,7 @@ describe('Workflow Normalization Utilities', () => { expect(result[0]).toEqual({ id: 'input1', name: 'Complex Input', + value: 'test-value', type: 'string', required: true, validation: { min: 0, max: 100 }, From a6561f32b9c75f4ff955a4d3d6e311c90ff1ad4a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 15:34:22 -0800 Subject: [PATCH 3/3] address bugbot comment --- apps/sim/executor/constants.ts | 20 ++++++++++++++ apps/sim/executor/execution/block-executor.ts | 8 ++---- .../handlers/trigger/trigger-handler.ts | 12 ++------- apps/sim/executor/utils/block-data.ts | 26 ++++++++++--------- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/apps/sim/executor/constants.ts b/apps/sim/executor/constants.ts index 387f560c40..ba2c2fc23b 100644 --- a/apps/sim/executor/constants.ts +++ b/apps/sim/executor/constants.ts @@ -275,6 +275,26 @@ export function isTriggerBlockType(blockType: string | undefined): boolean { return blockType !== undefined && (TRIGGER_BLOCK_TYPES as readonly string[]).includes(blockType) } +/** + * Determines if a block behaves as a trigger based on its metadata and config. + * This is used for execution flow decisions where trigger-like behavior matters. + * + * A block is considered trigger-like if: + * - Its category is 'triggers' + * - It has triggerMode enabled + * - It's a starter block (legacy entry point) + */ +export function isTriggerBehavior(block: { + metadata?: { category?: string; id?: string } + config?: { params?: { triggerMode?: boolean } } +}): boolean { + return ( + block.metadata?.category === 'triggers' || + block.config?.params?.triggerMode === true || + block.metadata?.id === BlockType.STARTER + ) +} + export function isMetadataOnlyBlockType(blockType: string | undefined): boolean { return ( blockType !== undefined && (METADATA_ONLY_BLOCK_TYPES as readonly string[]).includes(blockType) diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 5e2ec09cc5..b26f2175bf 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -11,6 +11,7 @@ import { DEFAULTS, EDGE, isSentinelBlockType, + isTriggerBehavior, } from '@/executor/constants' import type { DAGNode } from '@/executor/dag/builder' import { ChildWorkflowError } from '@/executor/errors/child-workflow-error' @@ -346,12 +347,7 @@ export class BlockExecutor { return filtered } - const isTrigger = - block.metadata?.category === 'triggers' || - block.config?.params?.triggerMode === true || - block.metadata?.id === BlockType.STARTER - - if (isTrigger) { + if (isTriggerBehavior(block)) { const filtered: NormalizedBlockOutput = {} const internalKeys = ['webhook', 'workflowId'] for (const [key, value] of Object.entries(output)) { diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.ts b/apps/sim/executor/handlers/trigger/trigger-handler.ts index fd0508f7c7..d9be91d233 100644 --- a/apps/sim/executor/handlers/trigger/trigger-handler.ts +++ b/apps/sim/executor/handlers/trigger/trigger-handler.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { BlockType } from '@/executor/constants' +import { BlockType, isTriggerBehavior } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' @@ -7,15 +7,7 @@ const logger = createLogger('TriggerBlockHandler') export class TriggerBlockHandler implements BlockHandler { canHandle(block: SerializedBlock): boolean { - if (block.metadata?.id === BlockType.STARTER) { - return true - } - - const isTriggerCategory = block.metadata?.category === 'triggers' - - const hasTriggerMode = block.config?.params?.triggerMode === true - - return isTriggerCategory || hasTriggerMode + return isTriggerBehavior(block) } async execute( diff --git a/apps/sim/executor/utils/block-data.ts b/apps/sim/executor/utils/block-data.ts index 07864fca4b..c6fc1c1850 100644 --- a/apps/sim/executor/utils/block-data.ts +++ b/apps/sim/executor/utils/block-data.ts @@ -1,5 +1,5 @@ import { normalizeInputFormatValue } from '@/lib/workflows/input-format' -import { normalizeName } from '@/executor/constants' +import { isTriggerBehavior, normalizeName } from '@/executor/constants' import type { ExecutionContext } from '@/executor/types' import type { OutputSchema } from '@/executor/utils/block-reference' import type { SerializedBlock } from '@/serializer/types' @@ -13,18 +13,20 @@ export interface BlockDataCollection { } /** - * Triggers where inputFormat fields should be merged into outputs schema. + * 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., , ). + * 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 TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS = [ +const BLOCKS_WITH_INPUT_FORMAT_OUTPUTS = [ 'start_trigger', 'starter', 'api_trigger', 'input_trigger', 'generic_webhook', 'human_in_the_loop', - 'approval', ] as const function getInputFormatFields(block: SerializedBlock): OutputSchema { @@ -48,17 +50,15 @@ export function getBlockSchema( block: SerializedBlock, toolConfig?: ToolConfig ): OutputSchema | undefined { - const isTrigger = - block.metadata?.category === 'triggers' || - (block.config?.params as Record | undefined)?.triggerMode === true - const blockType = block.metadata?.id + // For blocks that expose inputFormat as outputs, always merge them + // This includes both triggers (start_trigger, generic_webhook) and + // non-triggers (starter, human_in_the_loop) that have inputFormat if ( - isTrigger && blockType && - TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS.includes( - blockType as (typeof TRIGGERS_WITH_INPUT_FORMAT_OUTPUTS)[number] + BLOCKS_WITH_INPUT_FORMAT_OUTPUTS.includes( + blockType as (typeof BLOCKS_WITH_INPUT_FORMAT_OUTPUTS)[number] ) ) { const baseOutputs = (block.outputs as OutputSchema) || {} @@ -69,6 +69,8 @@ export function getBlockSchema( } } + const isTrigger = isTriggerBehavior(block) + if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) { return block.outputs as OutputSchema }