Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/sim/executor/execution/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
108 changes: 102 additions & 6 deletions apps/sim/executor/utils/start-block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>).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'),
Expand All @@ -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',
Expand All @@ -67,15 +67,14 @@ describe('start-block utilities', () => {
const output = buildStartBlockOutput({
resolution,
workflowInput: { payload: 'value' },
isDeployedExecution: true,
})

expect(output.payload).toBe('value')
expect(output.input).toBeUndefined()
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: {
Expand Down Expand Up @@ -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')
})
})
})
35 changes: 26 additions & 9 deletions apps/sim/executor/utils/start-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ interface DerivedInputResult {

function deriveInputFromFormat(
inputFormat: InputFormatField[],
workflowInput: unknown,
isDeployedExecution: boolean
workflowInput: unknown
): DerivedInputResult {
const structuredInput: Record<string, unknown> = {}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<string, unknown>,
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
}
}
}

Expand Down Expand Up @@ -401,17 +416,19 @@ function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | 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:
Expand Down