Skip to content

Commit 7b6aa72

Browse files
octo-patchwaleedlatif1TheodoreSpeaksSg312icecrasher321
authored
improvement(resolver): use context variables for block outputs in function block code (#4223)
* v0.6.29: login improvements, posthog telemetry (#4026) * feat(posthog): Add tracking on mothership abort (#4023) Co-authored-by: Theodore Li <theo@sim.ai> * fix(login): fix captcha headers for manual login (#4025) * fix(signup): fix turnstile key loading * fix(login): fix captcha header passing * Catch user already exists, remove login form captcha * fix: use context variables for block outputs in function block code When a function block references another block's output via <BlockA.result>, the executor previously embedded the full value as a JavaScript literal directly in the code string. For large outputs (>50 KB), this caused the code string to exceed the terminal console display limit, making inputs appear truncated or replaced with { __simTruncated: true } in the UI. Instead, block output references in function block code are now stored as named global variables (__blockRef_N) in the isolated VM context. The code string only contains the compact variable name, keeping it small regardless of the referenced value size. Loop/parallel/env/workflow references are still inlined as literals since the API route has no way to resolve them independently. The _runtimeContextVars key is filtered from sanitizeInputsForLog so it does not appear in execution logs or SSE events. Pre-resolved context variables are merged with any variables produced by the API route resolveCodeVariables, with executor values taking precedence. Fixes #4195 * fix: address Cursor and Greptile bot review comments - Pass preResolvedContextVariables through to shellEnvs for Shell language (Cursor: shell loses pre-resolved block refs, executes against undefined vars) - Remove duplicate CodeExecutionOutput interface declaration (Cursor + Greptile: dead duplicate declaration in tools/function/types.ts) - Deduplicate identical block references in resolveCodeWithContextVars so the same <BlockA.result> reused multiple times shares one __blockRef_N slot (Greptile P2: avoid duplicating large payloads across the wire) * fix: shell block references and complex env value serialization Two follow-ups to the function-block context-variable refactor: - resolveCodeWithContextVars now emits `$__blockRef_N` for shell function blocks so the script dereferences the env var injected by the executor. Other languages still receive the bare identifier. - The function-execute route now JSON-stringifies non-primitive values when building shell env vars, replacing the previous `String(v)` call that produced `[object Object]` for objects/arrays. Co-Authored-By: Octopus <liyuan851277048@icloud.com> * fix lint * review pass * ignore shell comments * update contract * fix tests --------- Co-authored-by: Waleed <walif6@gmail.com> Co-authored-by: Theodore Li <theodoreqili@gmail.com> Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com> Co-authored-by: octo-patch <octo-patch@github.com> Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
1 parent ae87481 commit 7b6aa72

12 files changed

Lines changed: 494 additions & 11 deletions

File tree

apps/sim/app/api/function/execute/route.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,26 @@ function cleanStdout(stdout: string): string {
587587
return stdout
588588
}
589589

590+
/**
591+
* Serializes a value for use as a shell environment variable. Strings pass through
592+
* unchanged; primitives are coerced via `String`; objects, arrays, and other complex
593+
* values are JSON-stringified so that referencing them via `$VAR` yields a useful
594+
* representation instead of `[object Object]`. `null`/`undefined` become an empty
595+
* string to match POSIX env semantics.
596+
*/
597+
function serializeForShellEnv(value: unknown, nullValue = ''): string {
598+
if (value === null || value === undefined) return nullValue
599+
if (typeof value === 'string') return value
600+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
601+
return String(value)
602+
}
603+
try {
604+
return JSON.stringify(value) ?? ''
605+
} catch {
606+
return String(value)
607+
}
608+
}
609+
590610
async function maybeExportSandboxFileToWorkspace(args: {
591611
authUserId: string
592612
workflowId?: string
@@ -722,6 +742,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
722742
blockNameMapping = {},
723743
blockOutputSchemas = {},
724744
workflowVariables = {},
745+
contextVariables: preResolvedContextVariables = {},
725746
workflowId,
726747
workspaceId,
727748
isCustomTool = false,
@@ -746,6 +767,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
746767
// For shell, env vars are injected as OS env vars via shellEnvs.
747768
// Replace {{VAR}} placeholders with $VAR so the shell can access them natively.
748769
resolvedCode = code.replace(/\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g, '$$$1')
770+
// Carry pre-resolved block output variables (e.g. __blockRef_N) so they can be
771+
// injected as shell env vars below. The executor replaces block references in the
772+
// code with these names, so the values must be present at runtime.
773+
contextVariables = { ...preResolvedContextVariables }
749774
} else {
750775
const codeResolution = resolveCodeVariables(
751776
code,
@@ -758,7 +783,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
758783
lang
759784
)
760785
resolvedCode = codeResolution.resolvedCode
761-
contextVariables = codeResolution.contextVariables
786+
// Merge pre-resolved block output variables from the executor. These take precedence
787+
// because they were produced by the resolver using full execution-state context
788+
// (including loop/parallel scope) and should not be overwritten.
789+
contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables }
762790
}
763791

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

784812
const shellEnvs: Record<string, string> = {}
785813
for (const [k, v] of Object.entries(envVars)) {
786-
shellEnvs[k] = String(v)
814+
shellEnvs[k] = serializeForShellEnv(v)
787815
}
788816
for (const [k, v] of Object.entries(contextVariables)) {
789-
shellEnvs[k] = String(v)
817+
shellEnvs[k] = serializeForShellEnv(v, 'null')
790818
}
791819

792820
logger.info(`[${requestId}] E2B shell execution`, {
@@ -893,7 +921,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
893921
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
894922
prologueLineCount++
895923
for (const [k, v] of Object.entries(contextVariables)) {
896-
prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
924+
prologue += `globalThis[${JSON.stringify(k)}] = ${formatLiteralForCode(v, 'javascript')};\n`
925+
prologue += `const ${k} = globalThis[${JSON.stringify(k)}];\n`
926+
prologueLineCount++
897927
prologueLineCount++
898928
}
899929

apps/sim/executor/execution/block-executor.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ import {
4444
} from '@/executor/utils/iteration-context'
4545
import { isJSONString } from '@/executor/utils/json'
4646
import { filterOutputForLog } from '@/executor/utils/output-filter'
47-
import type { VariableResolver } from '@/executor/variables/resolver'
47+
import {
48+
FUNCTION_BLOCK_CONTEXT_VARS_KEY,
49+
type VariableResolver,
50+
} from '@/executor/variables/resolver'
4851
import type { SerializedBlock } from '@/serializer/types'
4952
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants'
5053

@@ -115,7 +118,13 @@ export class BlockExecutor {
115118
await validateBlockType(ctx.userId, ctx.workspaceId, blockType, ctx)
116119
}
117120

118-
resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block)
121+
if (block.metadata?.id === BlockType.FUNCTION) {
122+
const { resolvedInputs: fnInputs, contextVariables } =
123+
this.resolver.resolveInputsForFunctionBlock(ctx, node.id, block.config.params, block)
124+
resolvedInputs = { ...fnInputs, [FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables }
125+
} else {
126+
resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block)
127+
}
119128

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

430439
for (const [key, value] of Object.entries(inputs)) {
431-
if (SYSTEM_SUBBLOCK_IDS.includes(key) || key === 'triggerMode') {
440+
if (
441+
SYSTEM_SUBBLOCK_IDS.includes(key) ||
442+
key === 'triggerMode' ||
443+
key === FUNCTION_BLOCK_CONTEXT_VARS_KEY
444+
) {
432445
continue
433446
}
434447

apps/sim/executor/handlers/function/function-handler.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/execution/constants'
33
import { BlockType } from '@/executor/constants'
44
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
55
import type { ExecutionContext } from '@/executor/types'
6+
import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver'
67
import type { SerializedBlock } from '@/serializer/types'
78
import { executeTool } from '@/tools'
89

@@ -73,10 +74,13 @@ describe('FunctionBlockHandler', () => {
7374
blockData: {},
7475
blockNameMapping: {},
7576
blockOutputSchemas: {},
77+
contextVariables: {},
7678
_context: {
7779
workflowId: mockContext.workflowId,
7880
workspaceId: mockContext.workspaceId,
81+
userId: mockContext.userId,
7982
isDeployedContext: mockContext.isDeployedContext,
83+
enforceCredentialAccess: mockContext.enforceCredentialAccess,
8084
},
8185
}
8286
const expectedOutput: any = { result: 'Success' }
@@ -110,10 +114,13 @@ describe('FunctionBlockHandler', () => {
110114
blockData: {},
111115
blockNameMapping: {},
112116
blockOutputSchemas: {},
117+
contextVariables: {},
113118
_context: {
114119
workflowId: mockContext.workflowId,
115120
workspaceId: mockContext.workspaceId,
121+
userId: mockContext.userId,
116122
isDeployedContext: mockContext.isDeployedContext,
123+
enforceCredentialAccess: mockContext.enforceCredentialAccess,
117124
},
118125
}
119126
const expectedOutput: any = { result: 'Success' }
@@ -140,10 +147,13 @@ describe('FunctionBlockHandler', () => {
140147
blockData: {},
141148
blockNameMapping: {},
142149
blockOutputSchemas: {},
150+
contextVariables: {},
143151
_context: {
144152
workflowId: mockContext.workflowId,
145153
workspaceId: mockContext.workspaceId,
154+
userId: mockContext.userId,
146155
isDeployedContext: mockContext.isDeployedContext,
156+
enforceCredentialAccess: mockContext.enforceCredentialAccess,
147157
},
148158
}
149159

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

181+
it('should pass runtime context variables to function_execute', async () => {
182+
const contextVariables = { __blockRef_0: { result: 'from-block' } }
183+
184+
await handler.execute(mockContext, mockBlock, {
185+
code: 'return globalThis["__blockRef_0"]',
186+
[FUNCTION_BLOCK_CONTEXT_VARS_KEY]: contextVariables,
187+
})
188+
189+
expect(mockExecuteTool).toHaveBeenCalledWith(
190+
'function_execute',
191+
expect.objectContaining({
192+
contextVariables,
193+
}),
194+
false,
195+
mockContext
196+
)
197+
})
198+
171199
it('should handle tool error with no specific message', async () => {
172200
const inputs = { code: 'some code' }
173201
const errorResult = { success: false }

apps/sim/executor/handlers/function/function-handler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DEFAULT_CODE_LANGUAGE } from '@/lib/execution/languages'
33
import { BlockType } from '@/executor/constants'
44
import type { BlockHandler, ExecutionContext } from '@/executor/types'
55
import { collectBlockData } from '@/executor/utils/block-data'
6+
import { FUNCTION_BLOCK_CONTEXT_VARS_KEY } from '@/executor/variables/resolver'
67
import type { SerializedBlock } from '@/serializer/types'
78
import { executeTool } from '@/tools'
89

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

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

29+
const contextVariables =
30+
(inputs[FUNCTION_BLOCK_CONTEXT_VARS_KEY] as Record<string, unknown> | undefined) ?? {}
31+
2832
const result = await executeTool(
2933
'function_execute',
3034
{
@@ -36,6 +40,7 @@ export class FunctionBlockHandler implements BlockHandler {
3640
blockData,
3741
blockNameMapping,
3842
blockOutputSchemas,
43+
contextVariables,
3944
_context: {
4045
workflowId: ctx.workflowId,
4146
workspaceId: ctx.workspaceId,

apps/sim/executor/utils/reference-validation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,14 @@ export function createCombinedPattern(): RegExp {
143143
*/
144144
export function replaceValidReferences(
145145
template: string,
146-
replacer: (match: string) => string
146+
replacer: (match: string, index: number, template: string) => string
147147
): string {
148148
const pattern = createReferencePattern()
149149

150-
return template.replace(pattern, (match) => {
150+
return template.replace(pattern, (match, _content, index) => {
151151
if (!isLikelyReferenceSegment(match)) {
152152
return match
153153
}
154-
return replacer(match)
154+
return replacer(match, index, template)
155155
})
156156
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { BlockType } from '@/executor/constants'
6+
import { ExecutionState } from '@/executor/execution/state'
7+
import type { ExecutionContext } from '@/executor/types'
8+
import { VariableResolver } from '@/executor/variables/resolver'
9+
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
10+
11+
function createBlock(id: string, name: string, type: string, params = {}): SerializedBlock {
12+
return {
13+
id,
14+
metadata: { id: type, name },
15+
position: { x: 0, y: 0 },
16+
config: { tool: type, params },
17+
inputs: {},
18+
outputs: {
19+
result: 'string',
20+
items: 'json',
21+
},
22+
enabled: true,
23+
}
24+
}
25+
26+
function createResolver(language = 'javascript') {
27+
const producer = createBlock('producer', 'Producer', BlockType.API)
28+
const functionBlock = createBlock('function', 'Function', BlockType.FUNCTION, {
29+
language,
30+
})
31+
const workflow: SerializedWorkflow = {
32+
version: '1',
33+
blocks: [producer, functionBlock],
34+
connections: [],
35+
loops: {},
36+
parallels: {},
37+
}
38+
const state = new ExecutionState()
39+
state.setBlockOutput('producer', {
40+
result: 'hello world',
41+
items: ['a', 'b'],
42+
})
43+
const ctx = {
44+
blockStates: state.getBlockStates(),
45+
blockLogs: [],
46+
environmentVariables: {},
47+
workflowVariables: {},
48+
decisions: { router: new Map(), condition: new Map() },
49+
loopExecutions: new Map(),
50+
executedBlocks: new Set(),
51+
activeExecutionPath: new Set(),
52+
completedLoops: new Set(),
53+
metadata: {},
54+
} as ExecutionContext
55+
56+
return {
57+
block: functionBlock,
58+
ctx,
59+
resolver: new VariableResolver(workflow, {}, state),
60+
}
61+
}
62+
63+
describe('VariableResolver function block inputs', () => {
64+
it('returns empty inputs when params are missing', () => {
65+
const { block, ctx, resolver } = createResolver()
66+
67+
const result = resolver.resolveInputsForFunctionBlock(ctx, 'function', undefined, block)
68+
69+
expect(result).toEqual({ resolvedInputs: {}, contextVariables: {} })
70+
})
71+
72+
it('resolves JavaScript block references through globalThis context variables', () => {
73+
const { block, ctx, resolver } = createResolver('javascript')
74+
75+
const result = resolver.resolveInputsForFunctionBlock(
76+
ctx,
77+
'function',
78+
{ code: 'return <Producer.result>' },
79+
block
80+
)
81+
82+
expect(result.resolvedInputs.code).toBe('return globalThis["__blockRef_0"]')
83+
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
84+
})
85+
86+
it('resolves Python block references through globals lookup', () => {
87+
const { block, ctx, resolver } = createResolver('python')
88+
89+
const result = resolver.resolveInputsForFunctionBlock(
90+
ctx,
91+
'function',
92+
{ code: 'return <Producer.result>' },
93+
block
94+
)
95+
96+
expect(result.resolvedInputs.code).toBe('return globals()["__blockRef_0"]')
97+
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
98+
})
99+
100+
it('uses separate Python context variables for repeated mutable references', () => {
101+
const { block, ctx, resolver } = createResolver('python')
102+
103+
const result = resolver.resolveInputsForFunctionBlock(
104+
ctx,
105+
'function',
106+
{ code: 'a = <Producer.items>\nb = <Producer.items>\nreturn b' },
107+
block
108+
)
109+
110+
expect(result.resolvedInputs.code).toBe(
111+
'a = globals()["__blockRef_0"]\nb = globals()["__blockRef_1"]\nreturn b'
112+
)
113+
expect(result.contextVariables).toEqual({
114+
__blockRef_0: ['a', 'b'],
115+
__blockRef_1: ['a', 'b'],
116+
})
117+
})
118+
119+
it('uses shell-safe expansions for block references', () => {
120+
const { block, ctx, resolver } = createResolver('shell')
121+
122+
const result = resolver.resolveInputsForFunctionBlock(
123+
ctx,
124+
'function',
125+
{ code: 'echo <Producer.result>suffix && echo "<Producer.result>"' },
126+
block
127+
)
128+
129+
expect(result.resolvedInputs.code).toBe(
130+
`echo "\${__blockRef_0}"suffix && echo "\${__blockRef_1}"`
131+
)
132+
expect(result.contextVariables).toEqual({
133+
__blockRef_0: 'hello world',
134+
__blockRef_1: 'hello world',
135+
})
136+
})
137+
138+
it('ignores shell comment quotes when formatting later block references', () => {
139+
const { block, ctx, resolver } = createResolver('shell')
140+
141+
const result = resolver.resolveInputsForFunctionBlock(
142+
ctx,
143+
'function',
144+
{ code: "# don't confuse quote tracking\necho <Producer.result>" },
145+
block
146+
)
147+
148+
expect(result.resolvedInputs.code).toBe(
149+
`# don't confuse quote tracking\necho "\${__blockRef_0}"`
150+
)
151+
expect(result.contextVariables).toEqual({ __blockRef_0: 'hello world' })
152+
})
153+
})

0 commit comments

Comments
 (0)