diff --git a/apps/sim/executor/execution/executor.ts b/apps/sim/executor/execution/executor.ts index 44697eb429..14ed7aa9a4 100644 --- a/apps/sim/executor/execution/executor.ts +++ b/apps/sim/executor/execution/executor.ts @@ -250,7 +250,6 @@ export class DAGExecutor { const blockOutput = buildStartBlockOutput({ resolution: startResolution, workflowInput: this.workflowInput, - isDeployedExecution: this.contextExtensions?.isDeployedContext === true, }) state.setBlockState(startResolution.block.id, { diff --git a/apps/sim/executor/utils/start-block.test.ts b/apps/sim/executor/utils/start-block.test.ts index aee3f798f6..4c6fd708e0 100644 --- a/apps/sim/executor/utils/start-block.test.ts +++ b/apps/sim/executor/utils/start-block.test.ts @@ -33,14 +33,14 @@ function createBlock( } describe('start-block utilities', () => { - it('buildResolutionFromBlock returns null when metadata id missing', () => { + it.concurrent('buildResolutionFromBlock returns null when metadata id missing', () => { const block = createBlock('api_trigger') ;(block.metadata as Record).id = undefined expect(buildResolutionFromBlock(block)).toBeNull() }) - it('resolveExecutorStartBlock prefers unified start block', () => { + it.concurrent('resolveExecutorStartBlock prefers unified start block', () => { const blocks = [ createBlock('api_trigger', 'api'), createBlock('starter', 'starter'), @@ -56,7 +56,7 @@ describe('start-block utilities', () => { expect(resolution?.path).toBe(StartBlockPath.UNIFIED) }) - it('buildStartBlockOutput normalizes unified start payload', () => { + it.concurrent('buildStartBlockOutput normalizes unified start payload', () => { const block = createBlock('start_trigger', 'start') const resolution = { blockId: 'start', @@ -67,7 +67,6 @@ describe('start-block utilities', () => { const output = buildStartBlockOutput({ resolution, workflowInput: { payload: 'value' }, - isDeployedExecution: true, }) expect(output.payload).toBe('value') @@ -75,7 +74,7 @@ describe('start-block utilities', () => { expect(output.conversationId).toBeUndefined() }) - it('buildStartBlockOutput uses trigger schema for API triggers', () => { + it.concurrent('buildStartBlockOutput uses trigger schema for API triggers', () => { const apiBlock = createBlock('api_trigger', 'api', { subBlocks: { inputFormat: { @@ -113,11 +112,108 @@ describe('start-block utilities', () => { }, files, }, - isDeployedExecution: false, }) expect(output.name).toBe('Ada') expect(output.input).toEqual({ name: 'Ada', count: 5 }) expect(output.files).toEqual(files) }) + + describe('inputFormat default values', () => { + it.concurrent('uses default value when runtime does not provide the field', () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [ + { name: 'input', type: 'string' }, + { name: 'customField', type: 'string', value: 'defaultValue' }, + ], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { input: 'hello' }, + }) + + expect(output.input).toBe('hello') + expect(output.customField).toBe('defaultValue') + }) + + it.concurrent('runtime value overrides default value', () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [{ name: 'customField', type: 'string', value: 'defaultValue' }], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { customField: 'runtimeValue' }, + }) + + expect(output.customField).toBe('runtimeValue') + }) + + it.concurrent('empty string from runtime overrides default value', () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [{ name: 'customField', type: 'string', value: 'defaultValue' }], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { customField: '' }, + }) + + expect(output.customField).toBe('') + }) + + it.concurrent('null from runtime does not override default value', () => { + const block = createBlock('start_trigger', 'start', { + subBlocks: { + inputFormat: { + value: [{ name: 'customField', type: 'string', value: 'defaultValue' }], + }, + }, + }) + + const resolution = { + blockId: 'start', + block, + path: StartBlockPath.UNIFIED, + } as const + + const output = buildStartBlockOutput({ + resolution, + workflowInput: { customField: null }, + }) + + expect(output.customField).toBe('defaultValue') + }) + }) }) diff --git a/apps/sim/executor/utils/start-block.ts b/apps/sim/executor/utils/start-block.ts index 17192906b8..279d390047 100644 --- a/apps/sim/executor/utils/start-block.ts +++ b/apps/sim/executor/utils/start-block.ts @@ -176,8 +176,7 @@ interface DerivedInputResult { function deriveInputFromFormat( inputFormat: InputFormatField[], - workflowInput: unknown, - isDeployedExecution: boolean + workflowInput: unknown ): DerivedInputResult { const structuredInput: Record = {} @@ -205,7 +204,8 @@ function deriveInputFromFormat( } } - if ((fieldValue === undefined || fieldValue === null) && !isDeployedExecution) { + // Use the default value from inputFormat if the field value wasn't provided at runtime + if (fieldValue === undefined || fieldValue === null) { fieldValue = field.value } @@ -255,13 +255,28 @@ function ensureString(value: unknown): string { return typeof value === 'string' ? value : '' } -function buildUnifiedStartOutput(workflowInput: unknown): NormalizedBlockOutput { +function buildUnifiedStartOutput( + workflowInput: unknown, + structuredInput: Record, + hasStructured: boolean +): NormalizedBlockOutput { const output: NormalizedBlockOutput = {} + if (hasStructured) { + for (const [key, value] of Object.entries(structuredInput)) { + output[key] = value + } + } + if (isPlainObject(workflowInput)) { for (const [key, value] of Object.entries(workflowInput)) { if (key === 'onUploadError') continue - output[key] = value + // Runtime values override defaults (except undefined/null which mean "not provided") + if (value !== undefined && value !== null) { + output[key] = value + } else if (!Object.hasOwn(output, key)) { + output[key] = value + } } } @@ -401,17 +416,19 @@ function extractSubBlocks(block: SerializedBlock): Record | und export interface StartBlockOutputOptions { resolution: ExecutorStartResolution workflowInput: unknown - isDeployedExecution: boolean } export function buildStartBlockOutput(options: StartBlockOutputOptions): NormalizedBlockOutput { - const { resolution, workflowInput, isDeployedExecution } = options + const { resolution, workflowInput } = options const inputFormat = extractInputFormat(resolution.block) - const { finalInput } = deriveInputFromFormat(inputFormat, workflowInput, isDeployedExecution) + const { finalInput, structuredInput, hasStructured } = deriveInputFromFormat( + inputFormat, + workflowInput + ) switch (resolution.path) { case StartBlockPath.UNIFIED: - return buildUnifiedStartOutput(workflowInput) + return buildUnifiedStartOutput(workflowInput, structuredInput, hasStructured) case StartBlockPath.SPLIT_API: case StartBlockPath.SPLIT_INPUT: