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
20 changes: 9 additions & 11 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import {
createEnvVarPattern,
createWorkflowVariablePattern,
Expand Down Expand Up @@ -387,7 +388,12 @@ function resolveWorkflowVariables(
if (type === 'number') {
variableValue = Number(variableValue)
} else if (type === 'boolean') {
variableValue = variableValue === 'true' || variableValue === true
if (typeof variableValue === 'boolean') {
// Already a boolean, keep as-is
} else {
const normalized = String(variableValue).toLowerCase().trim()
variableValue = normalized === 'true'
}
} else if (type === 'json' && typeof variableValue === 'string') {
try {
variableValue = JSON.parse(variableValue)
Expand Down Expand Up @@ -687,11 +693,7 @@ export async function POST(req: NextRequest) {
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) {
prologue += `const ${k} = undefined;\n`
} else {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
}
prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
prologueLineCount++
}

Expand Down Expand Up @@ -762,11 +764,7 @@ export async function POST(req: NextRequest) {
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
if (v === undefined) {
prologue += `${k} = None\n`
} else {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
}
prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n`
prologueLineCount++
}
const wrapped = [
Expand Down
48 changes: 48 additions & 0 deletions apps/sim/executor/utils/code-formatting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Formats a JavaScript/TypeScript value as a code literal for the target language.
* Handles special cases like null, undefined, booleans, and Python-specific number representations.
*
* @param value - The value to format
* @param language - Target language ('javascript' or 'python')
* @returns A string literal representation valid in the target language
*
* @example
* formatLiteralForCode(null, 'python') // => 'None'
* formatLiteralForCode(true, 'python') // => 'True'
* formatLiteralForCode(NaN, 'python') // => "float('nan')"
* formatLiteralForCode("hello", 'javascript') // => '"hello"'
* formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')"
*/
export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string {
const isPython = language === 'python'

if (value === undefined) {
return isPython ? 'None' : 'undefined'
}
if (value === null) {
return isPython ? 'None' : 'null'
}
if (typeof value === 'boolean') {
return isPython ? (value ? 'True' : 'False') : String(value)
}
if (typeof value === 'number') {
if (Number.isNaN(value)) {
return isPython ? "float('nan')" : 'NaN'
}
if (value === Number.POSITIVE_INFINITY) {
return isPython ? "float('inf')" : 'Infinity'
}
if (value === Number.NEGATIVE_INFINITY) {
return isPython ? "float('-inf')" : '-Infinity'
}
return String(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
// Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python
if (isPython) {
return `json.loads(${JSON.stringify(JSON.stringify(value))})`
}
return JSON.stringify(value)
}
18 changes: 9 additions & 9 deletions apps/sim/executor/variables/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,14 @@ export class VariableResolver {

let replacementError: Error | null = null

// Use generic utility for smart variable reference replacement
const blockType = block?.metadata?.id
const language =
blockType === BlockType.FUNCTION
? ((block?.config?.params as Record<string, unknown> | undefined)?.language as
| string
| undefined)
: undefined

let result = replaceValidReferences(template, (match) => {
if (replacementError) return match

Expand All @@ -167,14 +174,7 @@ export class VariableResolver {
return match
}

const blockType = block?.metadata?.id
const isInTemplateLiteral =
blockType === BlockType.FUNCTION &&
template.includes('${') &&
template.includes('}') &&
template.includes('`')

return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral)
return this.blockResolver.formatValueForBlock(resolved, blockType, language)
} catch (error) {
replacementError = error instanceof Error ? error : new Error(String(error))
return match
Expand Down
10 changes: 2 additions & 8 deletions apps/sim/executor/variables/resolvers/block.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,15 +257,9 @@ describe('BlockResolver', () => {
expect(result).toBe('"hello"')
})

it.concurrent('should format string for function block in template literal', () => {
it.concurrent('should format object for function block', () => {
const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock('hello', 'function', true)
expect(result).toBe('hello')
})

it.concurrent('should format object for function block in template literal', () => {
const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock({ a: 1 }, 'function', true)
const result = resolver.formatValueForBlock({ a: 1 }, 'function')
expect(result).toBe('{"a":1}')
})

Expand Down
35 changes: 5 additions & 30 deletions apps/sim/executor/variables/resolvers/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type OutputSchema,
resolveBlockReference,
} from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import {
navigatePath,
type ResolutionContext,
Expand Down Expand Up @@ -159,17 +160,13 @@ export class BlockResolver implements Resolver {
return this.nameToBlockId.get(normalizeName(name))
}

public formatValueForBlock(
value: any,
blockType: string | undefined,
isInTemplateLiteral = false
): string {
public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
if (blockType === 'condition') {
return this.stringifyForCondition(value)
}

if (blockType === 'function') {
return this.formatValueForCodeContext(value, isInTemplateLiteral)
return this.formatValueForCodeContext(value, language)
}

if (blockType === 'response') {
Expand Down Expand Up @@ -210,29 +207,7 @@ export class BlockResolver implements Resolver {
return String(value)
}

private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string {
if (isInTemplateLiteral) {
if (typeof value === 'string') {
return value
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
return String(value)
}

if (typeof value === 'string') {
return JSON.stringify(value)
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
if (value === undefined) {
return 'undefined'
}
if (value === null) {
return 'null'
}
return String(value)
private formatValueForCodeContext(value: any, language?: string): string {
return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
}
}
10 changes: 8 additions & 2 deletions apps/sim/executor/variables/resolvers/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export function navigatePath(obj: any, path: string[]): any {
const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
if (arrayMatch) {
const [, prop, bracketsPart] = arrayMatch
current = current[prop]
current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[prop]
: undefined
if (current === undefined || current === null) {
return undefined
}
Expand All @@ -49,7 +52,10 @@ export function navigatePath(obj: any, path: string[]): any {
const index = Number.parseInt(part, 10)
current = Array.isArray(current) ? current[index] : undefined
} else {
current = current[part]
current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[part]
: undefined
}
}
return current
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/execution/isolated-vm-worker.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ async function executeCode(request) {
for (const [key, value] of Object.entries(contextVariables)) {
if (value === undefined) {
await jail.set(key, undefined)
} else if (value === null) {
await jail.set(key, null)
} else {
await jail.set(key, new ivm.ExternalCopy(value).copyInto())
}
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/workflows/variables/variable-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('VariableManager', () => {
it.concurrent('should handle boolean type variables', () => {
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
Expand Down Expand Up @@ -128,7 +128,7 @@ describe('VariableManager', () => {
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true)
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
})

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/workflows/variables/variable-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class VariableManager {
// Special case for 'anything else' in the test
if (unquoted === 'anything else') return true
const normalized = String(unquoted).toLowerCase().trim()
return normalized === 'true' || normalized === '1'
return normalized === 'true'
}

case 'object':
Expand Down