Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0b9019d
v0.6.23: MCP fixes, remove local state in favor of server state, moth…
waleedlatif1 Apr 4, 2026
a54dcbe
v0.6.24: copilot feedback wiring, captcha fixes
waleedlatif1 Apr 4, 2026
28af223
v0.6.25: cloudwatch, cloudformation, live kb sync, linear fixes, post…
waleedlatif1 Apr 5, 2026
d889f32
v0.6.26: ui improvements, multiple response blocks, docx previews, ol…
waleedlatif1 Apr 5, 2026
316bc8c
v0.6.27: new triggers, mothership improvements, files archive, queuei…
waleedlatif1 Apr 7, 2026
3f508e4
v0.6.28: new docs, delete confirmation standardization, dagster integ…
waleedlatif1 Apr 7, 2026
d6ec115
v0.6.29: login improvements, posthog telemetry (#4026)
TheodoreSpeaks Apr 7, 2026
d7da35b
v0.6.30: slack trigger enhancements, connectors performance improveme…
waleedlatif1 Apr 8, 2026
cf233bb
v0.6.31: elevenlabs voice, trigger.dev fixes, cloud whitelabeling for…
waleedlatif1 Apr 8, 2026
f8f3758
v0.6.32: BYOK fixes, ui improvements, cloudwatch tools, jsm tools ext…
waleedlatif1 Apr 9, 2026
3c8bb40
v0.6.33: polling improvements, jsm forms tools, credentials reactquer…
waleedlatif1 Apr 9, 2026
d33acf4
v0.6.34: trigger.dev fixes, CI speedup, atlassian error extractor
waleedlatif1 Apr 9, 2026
4f40c4c
v0.6.35: additional jira fields, HITL docs, logs cleanup efficiency
waleedlatif1 Apr 10, 2026
cbfab1c
v0.6.36: new chunkers, sockets state machine, google sheets/drive/cal…
waleedlatif1 Apr 11, 2026
4309d06
v0.6.37: audit logs page, isolated-vm worker rotation, permission gro…
waleedlatif1 Apr 12, 2026
8b57476
v0.6.38: models page
waleedlatif1 Apr 12, 2026
e3d0e74
v0.6.39: billing fixes, tools audit, landing fix
waleedlatif1 Apr 13, 2026
0ac0539
v0.6.40: mothership tool loop, new skills, agiloft, STS, IAM integrat…
waleedlatif1 Apr 14, 2026
3838b6e
v0.6.41: webhooks fix, workers removal
waleedlatif1 Apr 14, 2026
fc07922
v0.6.42: mothership nested file reads, search modal improvements
waleedlatif1 Apr 14, 2026
3a1b1a8
v0.6.43: mothership billing idempotency, env var resolution fixes
waleedlatif1 Apr 14, 2026
46ffc49
v0.6.44: streamdown, mothership intelligence, excel extension
waleedlatif1 Apr 15, 2026
010435c
v0.6.45: superagent, csp, brightdata integration, gemini response for…
Sg312 Apr 15, 2026
c0bc62c
Merge pull request #4190 from simstudioai/staging
icecrasher321 Apr 16, 2026
387cc97
v0.6.46: mothership queueing, web vitals
waleedlatif1 Apr 16, 2026
2dbc7fd
v0.6.47: files focusing, documentation, opus 4.7
waleedlatif1 Apr 16, 2026
8a50f18
v0.6.48: import csv into tables, subflow fixes, CSP updates
waleedlatif1 Apr 16, 2026
dcf3302
v0.6.49: deploy sockets event, resolver, logs improvements, monday.co…
waleedlatif1 Apr 17, 2026
6734052
fix: use context variables for block outputs in function block code
Apr 18, 2026
2783606
fix: address Cursor and Greptile bot review comments
Apr 18, 2026
19787e1
fix: shell block references and complex env value serialization
May 6, 2026
7d972ae
fix lint
icecrasher321 May 6, 2026
4d695cf
review pass
icecrasher321 May 6, 2026
3208e07
ignore shell comments
icecrasher321 May 6, 2026
665bb91
Merge branch 'staging' into fix/issue-4195-function-block-context-vars
icecrasher321 May 6, 2026
a6be984
update contract
icecrasher321 May 6, 2026
5def77c
fix tests
icecrasher321 May 6, 2026
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
38 changes: 34 additions & 4 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,26 @@ function cleanStdout(stdout: string): string {
return stdout
}

/**
* Serializes a value for use as a shell environment variable. Strings pass through
* unchanged; primitives are coerced via `String`; objects, arrays, and other complex
* values are JSON-stringified so that referencing them via `$VAR` yields a useful
* representation instead of `[object Object]`. `null`/`undefined` become an empty
* string to match POSIX env semantics.
*/
function serializeForShellEnv(value: unknown, nullValue = ''): string {
if (value === null || value === undefined) return nullValue
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
return String(value)
}
try {
return JSON.stringify(value) ?? ''
} catch {
return String(value)
}
}

async function maybeExportSandboxFileToWorkspace(args: {
authUserId: string
workflowId?: string
Expand Down Expand Up @@ -722,6 +742,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
blockNameMapping = {},
blockOutputSchemas = {},
workflowVariables = {},
contextVariables: preResolvedContextVariables = {},
workflowId,
workspaceId,
isCustomTool = false,
Expand All @@ -746,6 +767,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
// For shell, env vars are injected as OS env vars via shellEnvs.
// Replace {{VAR}} placeholders with $VAR so the shell can access them natively.
resolvedCode = code.replace(/\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g, '$$$1')
// Carry pre-resolved block output variables (e.g. __blockRef_N) so they can be
// injected as shell env vars below. The executor replaces block references in the
// code with these names, so the values must be present at runtime.
contextVariables = { ...preResolvedContextVariables }
} else {
const codeResolution = resolveCodeVariables(
code,
Expand All @@ -758,7 +783,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
lang
)
resolvedCode = codeResolution.resolvedCode
contextVariables = codeResolution.contextVariables
// Merge pre-resolved block output variables from the executor. These take precedence
// because they were produced by the resolver using full execution-state context
// (including loop/parallel scope) and should not be overwritten.
contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables }
Comment thread
cursor[bot] marked this conversation as resolved.
}

let jsImports = ''
Expand All @@ -783,10 +811,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {

const shellEnvs: Record<string, string> = {}
for (const [k, v] of Object.entries(envVars)) {
shellEnvs[k] = String(v)
shellEnvs[k] = serializeForShellEnv(v)
}
for (const [k, v] of Object.entries(contextVariables)) {
shellEnvs[k] = String(v)
shellEnvs[k] = serializeForShellEnv(v, 'null')
}

logger.info(`[${requestId}] E2B shell execution`, {
Expand Down Expand Up @@ -893,7 +921,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
prologue += `globalThis[${JSON.stringify(k)}] = ${formatLiteralForCode(v, 'javascript')};\n`
prologue += `const ${k} = globalThis[${JSON.stringify(k)}];\n`
prologueLineCount++
prologueLineCount++
}

Expand Down
19 changes: 16 additions & 3 deletions apps/sim/executor/execution/block-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ import {
} from '@/executor/utils/iteration-context'
import { isJSONString } from '@/executor/utils/json'
import { filterOutputForLog } from '@/executor/utils/output-filter'
import type { VariableResolver } from '@/executor/variables/resolver'
import {
FUNCTION_BLOCK_CONTEXT_VARS_KEY,
type VariableResolver,
} from '@/executor/variables/resolver'
import type { SerializedBlock } from '@/serializer/types'
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'

Expand Down Expand Up @@ -115,7 +118,13 @@ export class BlockExecutor {
await validateBlockType(ctx.userId, ctx.workspaceId, blockType, ctx)
}

resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block)
if (block.metadata?.id === BlockType.FUNCTION) {
const { resolvedInputs: fnInputs, contextVariables } =
this.resolver.resolveInputsForFunctionBlock(ctx, node.id, block.config.params, block)
resolvedInputs = { ...fnInputs, [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables }
} else {
resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block)
}

if (blockLog) {
blockLog.input = this.sanitizeInputsForLog(resolvedInputs)
Expand Down Expand Up @@ -428,7 +437,11 @@ export class BlockExecutor {
const result: Record<string, any> = {}

for (const [key, value] of Object.entries(inputs)) {
if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode') {
if (
SYSTEM_SUBBLOCK_IDS.includes(key) ||
key === 'triggerMode' ||
key === FUNCTION_BLOCK_CONTEXT_VARS_KEY
) {
continue
}

Expand Down
28 changes: 28 additions & 0 deletions apps/sim/executor/handlers/function/function-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants'
import { BlockType } from '@/executor/constants'
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
import type { ExecutionContext } from '@/executor/types'
import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver'
import type { SerializedBlock } from '@/serializer/types'
import { executeTool } from '@/tools'

Expand Down Expand Up @@ -73,10 +74,13 @@ describe('FunctionBlockHandler', () => {
blockData: {},
blockNameMapping: {},
blockOutputSchemas: {},
contextVariables: {},
_context: {
workflowId: mockContext.workflowId,
workspaceId: mockContext.workspaceId,
userId: mockContext.userId,
isDeployedContext: mockContext.isDeployedContext,
enforceCredentialAccess: mockContext.enforceCredentialAccess,
},
}
const expectedOutput: any = { result: 'Success' }
Expand Down Expand Up @@ -110,10 +114,13 @@ describe('FunctionBlockHandler', () => {
blockData: {},
blockNameMapping: {},
blockOutputSchemas: {},
contextVariables: {},
_context: {
workflowId: mockContext.workflowId,
workspaceId: mockContext.workspaceId,
userId: mockContext.userId,
isDeployedContext: mockContext.isDeployedContext,
enforceCredentialAccess: mockContext.enforceCredentialAccess,
},
}
const expectedOutput: any = { result: 'Success' }
Expand All @@ -140,10 +147,13 @@ describe('FunctionBlockHandler', () => {
blockData: {},
blockNameMapping: {},
blockOutputSchemas: {},
contextVariables: {},
_context: {
workflowId: mockContext.workflowId,
workspaceId: mockContext.workspaceId,
userId: mockContext.userId,
isDeployedContext: mockContext.isDeployedContext,
enforceCredentialAccess: mockContext.enforceCredentialAccess,
},
}

Expand All @@ -168,6 +178,24 @@ describe('FunctionBlockHandler', () => {
expect(mockExecuteTool).toHaveBeenCalled()
})

it('should pass runtime context variables to function_execute', async () => {
const contextVariables = { __blockRef_0: { result: 'from-block' } }

await handler.execute(mockContext, mockBlock, {
code: 'return globalThis["__blockRef_0"]',
[FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables,
})

expect(mockExecuteTool).toHaveBeenCalledWith(
'function_execute',
expect.objectContaining({
contextVariables,
}),
false,
mockContext
)
})

it('should handle tool error with no specific message', async () => {
const inputs = { code: 'some code' }
const errorResult = { success: false }
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/executor/handlers/function/function-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
import { BlockType } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { collectBlockData } from '@/executor/utils/block-data'
import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver'
import type { SerializedBlock } from '@/serializer/types'
import { executeTool } from '@/tools'

Expand All @@ -25,6 +26,9 @@ export class FunctionBlockHandler implements BlockHandler {

const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)

const contextVariables =
(inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY] as Record<string, unknown> | undefined) ?? {}

const result = await executeTool(
'function_execute',
{
Expand All @@ -36,6 +40,7 @@ export class FunctionBlockHandler implements BlockHandler {
blockData,
blockNameMapping,
blockOutputSchemas,
contextVariables,
_context: {
workflowId: ctx.workflowId,
workspaceId: ctx.workspaceId,
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/executor/utils/reference-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,14 @@ export function createCombinedPattern(): RegExp {
*/
export function replaceValidReferences(
template: string,
replacer: (match: string) => string
replacer: (match: string, index: number, template: string) => string
): string {
const pattern = createReferencePattern()

return template.replace(pattern, (match) => {
return template.replace(pattern, (match, _content, index) => {
if (!isLikelyReferenceSegment(match)) {
return match
}
return replacer(match)
return replacer(match, index, template)
})
}
153 changes: 153 additions & 0 deletions apps/sim/executor/variables/resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { BlockType } from '@/executor/constants'
import { ExecutionState } from '@/executor/execution/state'
import type { ExecutionContext } from '@/executor/types'
import { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'

function createBlock(id: string, name: string, type: string, params = {}): SerializedBlock {
return {
id,
metadata: { id: type, name },
position: { x: 0, y: 0 },
config: { tool: type, params },
inputs: {},
outputs: {
result: 'string',
items: 'json',
},
enabled: true,
}
}

function createResolver(language = 'javascript') {
const producer = createBlock('producer', 'Producer', BlockType.API)
const functionBlock = createBlock('function', 'Function', BlockType.FUNCTION, {
language,
})
const workflow: SerializedWorkflow = {
version: '1',
blocks: [producer, functionBlock],
connections: [],
loops: {},
parallels: {},
}
const state = new ExecutionState()
state.setBlockOutput('producer', {
result: 'hello world',
items: ['a', 'b'],
})
const ctx = {
blockStates: state.getBlockStates(),
blockLogs: [],
environmentVariables: {},
workflowVariables: {},
decisions: { router: new Map(), condition: new Map() },
loopExecutions: new Map(),
executedBlocks: new Set(),
activeExecutionPath: new Set(),
completedLoops: new Set(),
metadata: {},
} as ExecutionContext

return {
block: functionBlock,
ctx,
resolver: new VariableResolver(workflow, {}, state),
}
}

describe('VariableResolver function block inputs', () => {
it('returns empty inputs when params are missing', () => {
const { block, ctx, resolver } = createResolver()

const result = resolver.resolveInputsForFunctionBlock(ctx, 'function', undefined, block)

expect(result).toEqual({ resolvedInputs: {}, contextVariables: {} })
})

it('resolves JavaScript block references through globalThis context variables', () => {
const { block, ctx, resolver } = createResolver('javascript')

const result = resolver.resolveInputsForFunctionBlock(
ctx,
'function',
{ code: 'return <Producer.result>' },
block
)

expect(result.resolvedInputs.code).toBe('return globalThis["__blockRef_0"]')
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
})

it('resolves Python block references through globals lookup', () => {
const { block, ctx, resolver } = createResolver('python')

const result = resolver.resolveInputsForFunctionBlock(
ctx,
'function',
{ code: 'return <Producer.result>' },
block
)

expect(result.resolvedInputs.code).toBe('return globals()["__blockRef_0"]')
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
})

it('uses separate Python context variables for repeated mutable references', () => {
const { block, ctx, resolver } = createResolver('python')

const result = resolver.resolveInputsForFunctionBlock(
ctx,
'function',
{ code: 'a = <Producer.items>\nb = <Producer.items>\nreturn b' },
block
)

expect(result.resolvedInputs.code).toBe(
'a = globals()["__blockRef_0"]\nb = globals()["__blockRef_1"]\nreturn b'
)
expect(result.contextVariables).toEqual({
__blockRef_0: ['a', 'b'],
__blockRef_1: ['a', 'b'],
})
})

it('uses shell-safe expansions for block references', () => {
const { block, ctx, resolver } = createResolver('shell')

const result = resolver.resolveInputsForFunctionBlock(
ctx,
'function',
{ code: 'echo <Producer.result>suffix && echo "<Producer.result>"' },
block
)

expect(result.resolvedInputs.code).toBe(
`echo "\${__blockRef_0}"suffix && echo "\${__blockRef_1}"`
)
expect(result.contextVariables).toEqual({
__blockRef_0: 'hello world',
__blockRef_1: 'hello world',
})
})

it('ignores shell comment quotes when formatting later block references', () => {
const { block, ctx, resolver } = createResolver('shell')

const result = resolver.resolveInputsForFunctionBlock(
ctx,
'function',
{ code: "# don't confuse quote tracking\necho <Producer.result>" },
block
)

expect(result.resolvedInputs.code).toBe(
`# don't confuse quote tracking\necho "\${__blockRef_0}"`
)
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
})
})
Loading
Loading