Skip to content
Open
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
35 changes: 31 additions & 4 deletions apps/docs/content/docs/workflows/blocks/function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,42 @@ workspace path instead, or keep the secret out of the output.

## Language

JavaScript without imports runs in a fast local sandbox. JavaScript with `import` or `require`, Python, and Shell run in the configured remote sandbox provider.
JavaScript without imports, mounted files, or a selected sandbox runs in a fast local sandbox. JavaScript with `import` or `require`, Python, and Shell run in the configured remote sandbox provider.

| Feature | JavaScript | Python | Shell |
| --- | --- | --- | --- |
| **Execution** | Local when there are no imports; remote with imports | Always remote | Always remote |
| **Execution** | Local by default; remote with imports, mounted files, or a selected sandbox | Always remote | Always remote |
| **Return a value** | `return { … }` | Assign `__sim_result__ = { … }` | Print `__SIM_RESULT__={…}` |
| **HTTP requests** | `fetch()` built in | `requests` or `httpx` | `curl` or an installed CLI |
| **Best for** | quick transforms and JSON | scripts, data science, charts, complex math | CLI workflows and system utilities |

### Local JavaScript data APIs

`Buffer`, `atob`, `btoa`, `TextEncoder`, and `TextDecoder` are available without
imports. For example, encode a JSON payload as UTF-8 base64:

```javascript
const payload = { message: 'Hello 🌍' }
return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')
```

`Buffer` is a Uint8Array-backed implementation of common Node Buffer operations,
including UTF-8, base64, hex, concatenation, and numeric reads and writes. It does
not provide every API from the latest Node release; import `node:buffer` to use
the remote runtime's native implementation. All local Buffer allocations,
including `allocUnsafe`, are zero-filled and count toward the isolate's memory
limit.

`TextEncoder` encodes UTF-8; `TextDecoder` supports encoding labels, streaming
decoding, and fatal decoding errors. `atob` and `btoa` operate on binary strings,
not Unicode text. Use `Buffer` for Unicode base64 conversion.

These data APIs do not add filesystem access, Node modules, timers, streams, or
`FormData` to the local runtime. Import the required Node module or select a
remote sandbox when those capabilities are needed.

### Remote runtimes

<Callout type="info">
Python and Shell require a remote sandbox. They are enabled by default on sim.ai;
on a self-hosted instance, build and configure the provider's dedicated
Expand Down Expand Up @@ -236,8 +263,8 @@ Then open the block's advanced options and choose the sandbox under **Sandbox**.

The default and custom behavior is intentionally explicit:

- **JavaScript without imports** stays in the local isolated runtime for speed and
ignores the sandbox selection.
- **JavaScript without imports** stays in the local isolated runtime for speed
unless a sandbox is selected or files are mounted.
- **JavaScript with `import` or `require`** runs remotely. With no selection it
uses the Function base; with a sandbox it gets that sandbox's npm packages and
system packages and managed CLI tools.
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/blocks/blocks/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
'This is a core workflow block. Execute custom JavaScript, Python, or Shell code within your workflow. JavaScript without imports runs locally for fast execution, while code with imports, Python, and Shell run in a remote sandbox.',
bestPractices: `
- JavaScript code without external imports runs in a local VM for fastest execution.
- Local JavaScript includes Buffer, atob, btoa, TextEncoder, and TextDecoder for data conversion without imports. Use Buffer.from(text, 'utf8').toString('base64') for Unicode text; btoa only accepts binary strings.
- JavaScript code with import/require statements runs in a remote sandbox.
- Python code always runs in a remote sandbox.
- Shell code runs CLI commands in a remote sandbox.
Expand Down Expand Up @@ -63,7 +64,7 @@ IMPORTANT FORMATTING RULES:
1. Reference Environment Variables: Use the exact syntax {{VARIABLE_NAME}}. In JavaScript and Python, prefer the unquoted form when the placeholder is the complete expression (for example, 'const apiKey = {{SERVICE_API_KEY}};'). Quoted and embedded string forms such as '"Bearer {{SERVICE_API_KEY}}"', template literals, and JavaScript regex literals are also supported. In Shell, prefer '"{{SERVICE_API_KEY}}"' when the secret should be one scalar argument; use a bare placeholder only when Bash word-splitting or pattern semantics are intentional. Sim binds the resolved value separately from the source at execution time, preserving its exact string contents.
2. Reference Input Parameters/Workflow Variables: Use the exact syntax <variable_name>. Do NOT wrap it in quotes (e.g., use 'userId = <userId>;' not 'userId = "<userId>";'). This includes parameters defined in the block's schema and outputs from previous blocks.
3. Function Body ONLY: Do NOT include the function signature (e.g., 'async function myFunction() {' or the surrounding '}').
4. Imports: Standard Node.js built-in modules (e.g., 'crypto', 'fs') are always available. Third-party packages are available ONLY when the block has a sandbox selected — the sandbox's package list is appended below when one is. Never import a package that is not on that list.
4. Runtime APIs: Buffer, atob, btoa, TextEncoder, and TextDecoder are available without imports. Use Buffer.from(text, 'utf8').toString('base64') for Unicode text; btoa only accepts binary strings. Importing Node.js built-in modules (e.g., 'crypto', 'fs') runs the code in a remote sandbox. Third-party packages are available ONLY when the block has a sandbox selected — the sandbox's package list is appended below when one is. Never import a package that is not on that list.
5. Output: Ensure the code returns a value if the function is expected to produce output. Use 'return'.
6. Clarity: Write clean, readable code.
7. No Explanations: Do NOT include markdown formatting, comments explaining the rules, or any text other than the raw JavaScript code for the function body.
Expand Down
84 changes: 84 additions & 0 deletions apps/sim/lib/execution/function-globals.smoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @vitest-environment node
*
* Exercises the actual worker with SIM_HELPERS_SMOKE=1 and a compatible Node build.
*/
import { describe, expect, it } from 'vitest'
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'

function run(code: string, timeoutMs = 5000) {
return executeInIsolatedVM({
code,
params: {},
envVars: {},
contextVariables: {},
timeoutMs,
requestId: 'function-globals-smoke',
})
}

describe.skipIf(process.env.SIM_HELPERS_SMOKE !== '1')('Function globals in a real isolate', () => {
it('uses Buffer and text codecs without imports or a remote sandbox', async () => {
const result = await run(`
const payload = { text: 'Hello 🌍' }
const encoded = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')
const decoded = new TextDecoder().decode(new TextEncoder().encode(payload.text))
return { encoded, decoded, binary: atob(btoa('hello')), zero: Buffer.allocUnsafe(32).every(b => b === 0) }
`)
expect(result.error).toBeUndefined()
expect(result.result).toEqual({
encoded: Buffer.from(JSON.stringify({ text: 'Hello 🌍' }), 'utf8').toString('base64'),
decoded: 'Hello 🌍',
binary: 'hello',
zero: true,
})
})

it('keeps prototypes and host capabilities isolated between executions', async () => {
const first = await run(`
Buffer.prototype.polluted = true
TextEncoder.prototype.polluted = true
return Buffer.from.constructor('return typeof process + ":" + typeof require')()
`)
expect(first.error).toBeUndefined()
expect(first.result).toBe('undefined:undefined')
const next = await run(
'return [Buffer.prototype.polluted === undefined, TextEncoder.prototype.polluted === undefined]'
)
expect(next.error).toBeUndefined()
expect(next.result).toEqual([true, true])
})

it('preserves user-code error locations and catches invalid conversions', async () => {
const result = await run("const data = Buffer.from('hello')\nthrow new Error('expected error')")
expect(result.error).toMatchObject({
line: 2,
lineContent: "throw new Error('expected error')",
})
const caught = await run("try { btoa('🌍'); return false } catch { return true }")
expect(caught.error).toBeUndefined()
expect(caught.result).toBe(true)
})

it('retains timeout enforcement and allows subsequent executions', async () => {
const result = await run('while (true) { new TextEncoder().encode("hello") }', 100)
expect(result.error).toBeDefined()
expect(result.termination).toBe('timeout')
const next = await run('return Buffer.from("ok").toString()')
expect(next.error).toBeUndefined()
expect(next.result).toBe('ok')
})

it('charges Buffer allocations to the existing isolate memory limit', async () => {
const result = await run(
'const buffers = []; while (true) { buffers.push(Buffer.alloc(16 * 1024 * 1024)) }'
)
expect(result.error).toBeDefined()
expect(result.error?.message).toMatch(
/memory|disposed|cancelled|Array buffer allocation failed/i
)
const next = await run('return Buffer.alloc(4).length')
expect(next.error).toBeUndefined()
expect(next.result).toBe(4)
})
})
7 changes: 7 additions & 0 deletions apps/sim/lib/execution/isolated-vm-worker.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const MAX_FETCH_OPTIONS_JSON_CHARS =

const SANDBOX_BUNDLE_DIR = path.join(__dirname, 'sandbox', 'bundles')
const SANDBOX_BUNDLE_FILES = {
'function-globals': 'function-globals.cjs',
pptxgenjs: 'pptxgenjs.cjs',
docx: 'docx.cjs',
'pdf-lib': 'pdf-lib.cjs',
Expand Down Expand Up @@ -199,6 +200,7 @@ async function executeCode(request, executionId) {

let context = null
let bootstrapScript = null
let globalsScript = null
let runtimeBindingsScript = null
let userScript = null
let logCallback = null
Expand All @@ -215,6 +217,10 @@ async function executeCode(request, executionId) {

await jail.set('global', jail.derefInto())

/** Evaluate pure JavaScript inside this isolate; never share host constructors. */
globalsScript = await isolate.compileScript(getBundleSource('function-globals').source)
await globalsScript.run(context, { timeout: timeoutMs })
Comment thread
waleedlatif1 marked this conversation as resolved.

logCallback = new ivm.Callback((...args) => {
const message = args.map((arg) => stringifyLogValue(arg)).join(' ')
appendStdout(`${message}\n`)
Expand Down Expand Up @@ -567,6 +573,7 @@ async function executeCode(request, executionId) {
userScript,
runtimeBindingsScript,
bootstrapScript,
globalsScript,
...externalCopies,
fetchCallback,
brokerCallback,
Expand Down
92 changes: 64 additions & 28 deletions apps/sim/lib/execution/sandbox/bundles/build.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/usr/bin/env bun
/**
* Builds isolate-compatible bundles for the document-generation libraries.
* Builds isolate-compatible bundles for Function globals and document libraries.
*
* Each library is bundled with `target=browser, format=iife` so it can be
* evaluated inside a V8 isolate that has no Node APIs (`require`, `process`,
* `fs`). The emitted files attach their exports to `globalThis.__bundles[name]`
* and are checked in so production images don't need the bundler at runtime.
* Document libraries target browsers and register on `globalThis.__bundles`.
* Function globals use neutral resolution so dependencies provide pure JavaScript
* fallbacks instead of assuming native browser codecs. Both emit IIFEs checked
* in so production images don't need the bundler at runtime.
*
* Every bundle is evaluated in a bare context before it is written: the
* bundler can emit a reference to a runtime helper it never defines (Bun does
Expand All @@ -19,7 +19,11 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createLogger } from '@sim/logger'
import { evaluateSandboxBundle } from '@/lib/execution/sandbox/bundles/verify'
import { build } from 'esbuild'
import {
evaluateFunctionGlobals,
evaluateSandboxBundle,
} from '@/lib/execution/sandbox/bundles/verify'
import type { SandboxBundleName } from '@/lib/execution/sandbox/types'

const logger = createLogger('SandboxBundleBuild')
Expand All @@ -45,11 +49,11 @@ const ENTRIES_DIR = join(HERE, '.entries')
const APP_SIM_ROOT = join(HERE, '..', '..', '..', '..')

interface BundleSpec {
/** Key on `globalThis.__bundles`. */
name: SandboxBundleName
/** Bundle identity; document libraries register on `globalThis.__bundles`. */
name: SandboxBundleName | 'function-globals'
/** Short filename written under `bundles/<file>.cjs`. */
outFile: string
/** Source of the entry file bun will bundle. */
/** Source of the entry file to bundle. */
entry: string
}

Expand All @@ -65,6 +69,17 @@ if (typeof globalThis.process === 'undefined') globalThis.process = __processPol
`

const BUNDLES: ReadonlyArray<BundleSpec> = [
{
name: 'function-globals',
outFile: 'function-globals.cjs',
entry: `
import { Buffer } from 'buffer/'
import { TextEncoder, TextDecoder } from '@exodus/bytes/encoding.js'
import atob from 'core-js-pure/actual/atob'
import btoa from 'core-js-pure/actual/btoa'
Object.assign(globalThis, { Buffer, TextEncoder, TextDecoder, atob, btoa })
`,
},
{
name: 'pdf-lib',
outFile: 'pdf-lib.cjs',
Expand Down Expand Up @@ -106,31 +121,52 @@ async function main(): Promise<void> {
const entryPath = join(ENTRIES_DIR, `${spec.name}.entry.ts`)
writeFileSync(entryPath, spec.entry, 'utf-8')

const result = await Bun.build({
entrypoints: [entryPath],
target: 'browser',
format: 'iife',
minify: true,
sourcemap: 'none',
root: APP_SIM_ROOT,
})

if (!result.success) {
for (const log of result.logs) {
logger.error(String(log))
let code: string
if (spec.name === 'function-globals') {
const result = await build({
entryPoints: [entryPath],
platform: 'neutral',
mainFields: ['module', 'main'],
format: 'iife',
bundle: true,
minify: true,
write: false,
legalComments: 'eof',
})
if (result.outputFiles.length !== 1) {
throw new Error('Expected one Function globals bundle')
}
throw new Error(`Failed to build sandbox bundle: ${spec.name}`)
}
code = result.outputFiles[0].text
} else {
const result = await Bun.build({
entrypoints: [entryPath],
target: 'browser',
format: 'iife',
minify: true,
sourcemap: 'none',
root: APP_SIM_ROOT,
})

if (result.outputs.length === 0) {
throw new Error(`No output produced for sandbox bundle: ${spec.name}`)
if (!result.success) {
for (const log of result.logs) {
logger.error(String(log))
}
throw new Error(`Failed to build sandbox bundle: ${spec.name}`)
}
if (result.outputs.length === 0) {
throw new Error(`No output produced for sandbox bundle: ${spec.name}`)
}
code = await result.outputs[0].text()
}

const code = await result.outputs[0].text()
const banner = `// sandbox bundle: ${spec.name}\n// generated by apps/sim/lib/execution/sandbox/bundles/build.ts\n// do not edit by hand. run \`bun run build:sandbox-bundles\` to regenerate.\n`
const banner = `/**\n * Sandbox bundle: ${spec.name}\n * Generated by apps/sim/lib/execution/sandbox/bundles/build.ts.\n * Do not edit by hand. Run \`bun run build:sandbox-bundles\` to regenerate.\n */\n`
const output = banner + code
try {
evaluateSandboxBundle(output, spec.name)
if (spec.name === 'function-globals') {
evaluateFunctionGlobals(output)
} else {
evaluateSandboxBundle(output, spec.name)
}
} catch (error) {
throw new Error(
`Sandbox bundle ${spec.name} does not evaluate in a bare isolate context: ${String(error)}`
Expand Down
8 changes: 5 additions & 3 deletions apps/sim/lib/execution/sandbox/bundles/docx.cjs

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions apps/sim/lib/execution/sandbox/bundles/function-globals.cjs

Large diffs are not rendered by default.

Loading
Loading