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
2 changes: 1 addition & 1 deletion apps/sim/executor/utils/start-block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ describe('start-block utilities', () => {

expect(output.count).toBe(5)
expect(output.payload).toEqual({ event: 'push' })
expect(output.meta).toEqual({ source: 'api' })
expect(output.meta).toBe('{"source":"api"}')
expect(output.extra).toBe('untouched')
})
})
Expand Down
3 changes: 1 addition & 2 deletions apps/sim/executor/utils/start-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
isInternalFileUrl,
parseInternalFileUrl,
} from '@/lib/uploads/utils/file'
import { canonicalInputFormatFieldType } from '@/lib/workflows/input-format-schema'
import {
classifyStartBlockType,
resolveStartCandidates,
Expand Down Expand Up @@ -246,7 +245,7 @@ export function coerceValue(type: string | null | undefined, value: unknown): un
return value
}

switch (type ? (canonicalInputFormatFieldType(type) ?? type) : type) {
switch (type) {
case 'string':
return typeof value === 'string' ? value : String(value)
case 'number': {
Expand Down
47 changes: 20 additions & 27 deletions apps/sim/lib/workflows/editing/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,34 +345,27 @@ describe('validateInputsForBlock', () => {
).toBe(false)
})

describe('input-format', () => {
const config = { id: 'inputFormat', type: 'input-format' as const }
const validate = (value: unknown) =>
validateValueForSubBlockType(config, value, 'inputFormat', 'start_trigger', 'start-1')

it('stores type aliases as their canonical type and keeps untyped rows', () => {
expect(
validate([
{ name: 'payload', type: 'json' },
{ name: 'documents', type: 'files' },
{ name: 'message' },
])
).toEqual({
valid: true,
value: [
{ name: 'payload', type: 'object' },
{ name: 'documents', type: 'file[]' },
{ name: 'message' },
],
})
})

it('rejects a type no workflow input can declare, listing the allowed types', () => {
const result = validate([{ name: 'count', type: 'integer' }])
expect(result.valid).toBe(false)
expect(result.error?.error).toBe(
'Invalid type "integer" for inputFormat[0] - use one of: string, number, boolean, object, array, file[]'
it('stores input-format rows with legacy or aliased types unchanged', () => {
const rows = [
{ name: 'payload', type: 'json' },
{ name: 'title', type: 'short-input' },
{ name: 'message' },
]
expect(
validateValueForSubBlockType(
{ id: 'inputFormat', type: 'input-format' },
rows,
'inputFormat',
'start_trigger',
'start-1'
)
).toEqual({
valid: true,
value: [
{ name: 'payload', type: 'json' },
{ name: 'title', type: 'short-input' },
{ name: 'message' },
],
})
})

Expand Down
43 changes: 1 addition & 42 deletions apps/sim/lib/workflows/editing/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ import {
type SelectorReference,
validateSelectorIds,
} from '@/lib/workflows/editing/selector-validator'
import {
canonicalInputFormatFieldType,
INPUT_FORMAT_FIELD_TYPES,
} from '@/lib/workflows/input-format-schema'
import { containsReference } from '@/lib/workflows/sanitization/references'
import { getSkillById } from '@/lib/workflows/skills/operations'
import {
Expand Down Expand Up @@ -643,44 +639,7 @@ export function validateValueForSubBlockType(
return { valid: true, value }
}

case 'input-format': {
if (!Array.isArray(value)) {
return {
valid: false,
error: {
blockId,
blockType,
field: fieldName,
value,
error: `Invalid ${type} value for field "${fieldName}" - expected an array`,
},
}
}
const rows: unknown[] = []
for (const [index, row] of value.entries()) {
if (!isPlainRecord(row) || row.type === undefined) {
rows.push(row)
continue
}
const fieldType =
typeof row.type === 'string' ? canonicalInputFormatFieldType(row.type) : null
if (!fieldType) {
return {
valid: false,
error: {
blockId,
blockType,
field: fieldName,
value,
error: `Invalid type ${JSON.stringify(row.type)} for ${fieldName}[${index}] - use one of: ${INPUT_FORMAT_FIELD_TYPES.join(', ')}`,
},
}
}
rows.push(fieldType === row.type ? row : { ...row, type: fieldType })
}
return { valid: true, value: rows }
}

case 'input-format':
case 'table': {
// Should be an array
if (!Array.isArray(value)) {
Expand Down
11 changes: 3 additions & 8 deletions apps/sim/lib/workflows/input-format-schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod'

/** The input field types the editor offers and workflow runs accept. */
export const INPUT_FORMAT_FIELD_TYPES = [
/** The input field types the editor offers. */
const INPUT_FORMAT_FIELD_TYPES = [
'string',
'number',
'boolean',
Expand All @@ -12,14 +12,9 @@ export const INPUT_FORMAT_FIELD_TYPES = [

export type InputFormatFieldType = (typeof INPUT_FORMAT_FIELD_TYPES)[number]

/**
* Every spelling a stored field's `type` may use, mapped to its canonical type:
* `json` is the name blocks use for object params and outputs, `files` the
* legacy MCP alias.
*/
/** Every canonical type by name, plus `files`, the legacy MCP alias of `file[]`. */
const INPUT_FORMAT_FIELD_TYPES_BY_NAME = new Map<string, InputFormatFieldType>([
...INPUT_FORMAT_FIELD_TYPES.map((type) => [type, type] as const),
['json', 'object'],
['files', 'file[]'],
])

Expand Down
33 changes: 15 additions & 18 deletions apps/sim/lib/workflows/input-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,25 +108,22 @@ describe('workflow input schemas', () => {
expect(z.object(generateWorkflowInputShape(fields)).parse(input)).toEqual(input)
})

it('accepts the json and files aliases as their canonical types', () => {
const shape = z.object(
generateWorkflowInputShape([
{ name: 'payload', type: 'json' },
{ name: 'documents', type: 'files' },
])
)
expect(shape.safeParse({ payload: { a: 1 }, documents: [file] }).success).toBe(true)
expect(shape.safeParse({ payload: 'text', documents: [file] }).success).toBe(false)
it('accepts the files alias as file[]', () => {
const shape = z.object(generateWorkflowInputShape([{ name: 'documents', type: 'files' }]))
expect(shape.safeParse({ documents: [file] }).success).toBe(true)
expect(shape.safeParse({ documents: 'text' }).success).toBe(false)
})

it('rejects unsupported type names as a validation failure naming the field', () => {
expect(() => generateToolInputSchema([{ name: 'count', type: 'unsupported' }])).toThrow(
expect.objectContaining({
name: 'OrchestrationError',
code: 'validation',
message:
'Workflow input "count" has unsupported type "unsupported". Use one of: string, number, boolean, object, array, file[].',
it.each(['short-input', 'json'])(
'accepts any value for the legacy type %s and advertises no type for it',
(type) => {
const fields = [{ name: 'title', type }]
expect(generateToolInputSchema(fields).properties).toEqual({
title: { description: 'title' },
})
)
})
const shape = z.object(generateWorkflowInputShape(fields))
expect(shape.safeParse({ title: 'Quarterly report' }).success).toBe(true)
expect(shape.safeParse({ title: { text: 'Quarterly report' } }).success).toBe(true)
}
)
})
18 changes: 8 additions & 10 deletions apps/sim/lib/workflows/input-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
import { workflowFileInputSchema } from '@/lib/workflows/input-file-schema'
import {
canonicalInputFormatFieldType,
INPUT_FORMAT_FIELD_TYPES,
type InputFormatFieldType,
} from '@/lib/workflows/input-format-schema'
import type { InputFormatField } from '@/lib/workflows/types'
import { isSafeKey } from '@/tools/safe-assign'

function fieldTypeToSchema(type: InputFormatFieldType): z.ZodType {
/**
* A stored type outside the editor's list (legacy widget names such as `short-input`) accepts any
* value, as the executor passes it through unchanged.
*/
function fieldTypeToSchema(type: InputFormatFieldType | null): z.ZodType {
switch (type) {
case 'string':
return z.string()
Expand All @@ -23,15 +26,16 @@ function fieldTypeToSchema(type: InputFormatFieldType): z.ZodType {
return z.array(z.unknown())
case 'file[]':
return z.array(workflowFileInputSchema)
case null:
return z.unknown()
}
}

/**
* Shared by workflow run validation and MCP discovery so the advertised input
* types match execution. The executor supplies configured defaults at run time.
*
* @throws OrchestrationError `validation` when a field name is reserved or its
* type is not one a workflow input can declare.
* @throws OrchestrationError `validation` when a field name is reserved.
*/
export function generateWorkflowInputShape(inputFormat: InputFormatField[]): z.ZodRawShape {
const shape: Record<string, z.ZodType> = {}
Expand All @@ -46,12 +50,6 @@ export function generateWorkflowInputShape(inputFormat: InputFormatField[]): z.Z
)
}
const type = canonicalInputFormatFieldType(field.type)
if (!type) {
throw new OrchestrationError(
'validation',
`Workflow input "${name}" has unsupported type "${field.type}". Use one of: ${INPUT_FORMAT_FIELD_TYPES.join(', ')}.`
)
}

const schema = fieldTypeToSchema(type).describe(
field.description?.trim() ||
Expand Down
5 changes: 2 additions & 3 deletions apps/sim/lib/workflows/triggers/run-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,15 @@ describe('file inputs through workflow run options', () => {
})
})

describe('mock payloads for aliased field types', () => {
it('samples a json field as an object the run validation accepts', () => {
describe('mock payloads for legacy field types', () => {
it('samples a json field with a value the run validation accepts', () => {
const [option] = resolveTriggerRunOptions({
start: {
type: 'start_trigger',
name: 'Start',
subBlocks: { inputFormat: { value: [{ name: 'payload', type: 'json' }] } },
},
})
expect(option.mockPayload).toEqual({ payload: {} })
expect(validateTriggerInput(option, option.mockPayload)).toEqual({ ok: true })
})
})
Expand Down
Loading