Function

The Function block runs your own JavaScript, Python, or Shell code as one step of a workflow. Use it to reshape a value, run a calculation, call a CLI, or add logic no other block covers.

Reshape an API response

Fetch a response with an items array of user records, then return only active IDs:

const data = <api.data>;
return data.items.filter((item) => item.active).map((item) => item.id);

Later blocks read <extract.result> for a Function named Extract.

Language and runtime

JavaScript without imports runs in the local isolated VM unless a special file-path reference requires a remote mount. JavaScript with import or require, Python, and Shell run in the configured remote provider. Local VM workers run within the Sim app container on self-hosted deployments.

FeatureJavaScriptPythonShell
ExecutionLocal without imports or file-path mounts; remote otherwiseAlways remoteAlways remote
Return a valuereturn { … }Assign __sim_result__ = { … }Print __SIM_RESULT__={…}
HTTP requestsfetch() built inrequests or httpxcurl or an installed CLI
Best forquick transforms and JSONscripts, data science, charts, complex mathCLI workflows and system utilities

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 Function base first. Python can return plots as images; you can also write image files under /tmp/sim/outputs.

If no remote provider is configured, JavaScript without import or require continues to run in Sim's local isolated VM. Missing E2B or Daytona configuration does not disable that path. Remote-only code fails with an explicit configuration error; Sim does not reinterpret Python or Shell as JavaScript.

The dedicated Function base has the same runtime and universal package contract on E2B and Daytona. It includes this data-science stack; use a workspace sandbox when another dependency must be present:

  • Data and graphs: pandas, numpy, scipy, xarray, numba, networkx
  • ML and NLP: scikit-learn, gensim, nltk, spacy, textblob
  • Plots and images: matplotlib, seaborn, plotly, bokeh, kaleido, pillow, opencv-python, scikit-image, imageio, tifffile
  • Audio: librosa, soundfile
  • Web, parsing, and files: requests, beautifulsoup4, lxml, openpyxl, xlrd, python-docx, xmltodict, PyYAML, tomlkit, simplejson, orjson, SQLAlchemy
  • Math: sympy
  • Application helpers: rich, typer, click, tqdm, Jinja2, pydantic, python-dateutil, pytz, psutil, filetype, python-slugify, parsedatetime, pytimeparse
  • Generic CLIs: jq, yq, csvkit, zx, xmlstarlet, httpie, ripgrep, fd, bat, sqlite3, tar, gzip, bzip2, ZIP/XZ/7z, media/image tools, and standard network utilities

Vendor and service clients such as AWS CLI and GitHub CLI are not part of the universal base. Add them through the managed catalog. Database clients such as PostgreSQL, MySQL, and Redis can be added as system packages when available from the configured Debian repositories.

Configuration

Code

JavaScript is the default. The language field is part of the saved workflow and is never removed when Sandbox configuration changes. Python remains available as a language choice; Shell and custom Sandbox controls appear when a remote Function sandbox provider is enabled. Reference an earlier output directly, with no quotes around the tag, and read an environment variable with {{VAR}}:

const data = <api.data>;
return data.items.filter((i) => i.active).map((i) => i.id);
data = <api.data>
__sim_result__ = [item["id"] for item in data["items"] if item["active"]]
set -euo pipefail
active_ids=$(printf '%s' <api.data> | jq -c '[.items[] | select(.active) | .id]')
printf '__SIM_RESULT__=%s\n' "$active_ids"

JavaScript returns a value with return. Python runs as a normal module: assign the value for downstream blocks to __sim_result__. A full script with functions, imports, and an if __name__ == '__main__': guard works as written; legacy snippets with a top-level return remain supported. print() is logging and goes to stdout rather than becoming the result.

Shell returns structured data by printing a line beginning with __SIM_RESULT__=. A valid JSON payload becomes an object, array, number, boolean, or string; a non-JSON payload becomes a string. The marker line is removed from stdout. Other command output remains available in stdout.

Secret placeholders in code

When an environment variable is the complete JavaScript or Python expression, prefer the unquoted form:

const apiKey = {{API_KEY}};

Existing quoted and embedded forms are also supported, including "{{API_KEY}}", "Bearer {{API_KEY}}", and placeholders in template literals. Function and Custom Tool code use the same compiler at the execution boundary. It binds the secret separately from the source instead of pasting plaintext into your code, so quotes, backslashes, newlines, and string values such as "123" and "true" retain their exact contents and do not become JavaScript or Python literals of another type.

Placeholders are always strings

A placeholder is bound as a value, never parsed as source. That is what keeps a secret from running as code — but it also means {{KEY}} always evaluates to a string, whatever the value looks like. Convert it when your code needs another type:

const retries = Number({{MAX_RETRIES}});
const enabled = {{FEATURE_ON}} === 'true';
const patterns = JSON.parse({{PATTERN_LIST}});

In Python, use int(), == "true", and json.loads() the same way.

Two cases are easy to miss:

  • Booleans. A bare if ({{FEATURE_ON}}) tests string truthiness, so the string "false" is truthy. Compare against 'true' instead.
  • Lists and objects. Store the value as JSON so you can parse it back. ["^[A-Z]{2}-\\d{4}$", "^\\d{7,15}$"] becomes an array; a JavaScript array of regex literals does not. It arrives as one long string, and iterating it walks character by character.

Condition blocks differ in one way: a number, boolean, or null value compares as a literal there, so {{MAX_RETRIES}} === 3 is true. Every other value is a string, exactly as it is here.

JavaScript regex literals can contain a placeholder:

const matcher = /^{{PATH_PATTERN}}$/i;

The value is interpreted as raw regex pattern text and the literal's flags are preserved. If you need the value matched literally rather than as a pattern, escape regex metacharacters before constructing the expression.

In Shell, use the same {{KEY}} syntax and write "{{KEY}}" when the value should remain one scalar argument. Bare placeholders remain supported for existing workflows and retain Bash's native unquoted behavior, including word splitting and regex-pattern semantics. Placeholders also work inside quoted heredocs without enabling unrelated shell expansion:

cat > /tmp/request.txt <<'REQUEST'
Authorization: Bearer {{API_KEY}}
Home remains literal: $HOME
REQUEST

Sim supplies the rendered heredoc privately while preserving the quoted delimiter's literal $VAR, backtick, and command-substitution behavior.

Outputs

OutputWhat it is
<function.result>The value your code returns (object, array, string, number, …)
<function.stdout>Captured console.log(), print(), or Shell output, subject to the diagnostic limits below
<function.files>Files your code wrote to /tmp/sim/outputs, ready to attach or upload

Local JavaScript stdout is truncated after 200,000 characters by default, with a [stdout truncated] marker. Operators can change that default with IVM_MAX_STDOUT_CHARS.

Remote execution has a separate 10 MiB combined process-output budget for stdout, stderr, result text, and structured error text. Exceeding it fails the block. Export large remote results as files under /tmp/sim/outputs; the separate 20-file and 50 MiB export limits still apply.

Files

Reading. Use an indexed file reference ending in .path to mount the file under /tmp/sim/inputs:

import pandas as pd

frame = pd.read_csv(<gmail.attachments[0].path>)
frame.describe().to_csv('/tmp/sim/outputs/summary.csv')

.path is a special Function reference, not a field stored on the file object. It resolves to the file's location on the sandbox filesystem, so any language can open it — pandas, ffmpeg, a CLI. It is the counterpart to .base64, which inlines the contents instead and works only in JavaScript. Both appear in the reference dropdown next to .name and .size.

Writing. Anything your code writes to /tmp/sim/outputs comes back as <function.files>, a list of file objects any file-accepting block takes directly — attach them to an email, upload them to storage, or save them to the workspace with the File block. There is nothing to turn on.

The separate Function tool supports explicit outputSandboxPath exports. That tool option exports only the named paths and disables automatic directory harvesting for the call; it is not a field on the Function block.

Referencing .path runs the block in the remote sandbox, since the local JavaScript VM has no filesystem — expect the slower start of a remote run even for plain JavaScript. Referencing the file itself (<gmail.attachments[0]>, .name, .url) does not, and stays local. Up to 20 files come back per run, 50 MiB total, nested no more than 11 directories deep; a run that exceeds any of these fails rather than returning part of what your code wrote.

Returned files live with the execution rather than in your workspace, and a text file containing a resolved secret value is refused rather than returned — there is nowhere on an execution file to record that it carries one. Keep secrets out of execution-file exports. For an explicitly authorized persistent destination, use a compatible workspace File action; the separate Function tool also has workspace output controls.

Sandboxes

A sandbox is a named environment your workspace maintains: a language, a list of pip or npm dependencies, optional Debian/APT system packages, and optional managed CLI tools. Select one on a Function block and its code can import the dependencies and run the commands it declares. Leave the selection empty to use the dedicated Function base.

Create and edit sandboxes in Settings → Sandboxes. Only workspace admins can create or edit them. On sim.ai they need an active Max or Enterprise plan; self-hosted deployments turn them on with SANDBOXES_ENABLED (see self-hosted enterprise). The section is usable only when the deployment also has a remote provider and immutable Function base configured. The Function block hides its custom Sandbox selector when that runtime is unavailable.

  1. Name the sandbox — bigquery-etl, scraping, whatever the job is.
  2. Pick the language. This selects pip or npm for the dependency list. Python and JavaScript blocks list matching sandboxes; Shell can use either kind.
  3. Paste your dependencies, one per line. Version pins are optional.
  4. Add system packages by Debian/APT package coordinate, one per line. Use this for ordinary command-line utilities such as jq and ffmpeg.
  5. Select any managed CLI tools that require a specialized, verified installer. The searchable catalog is grouped by cloud, Kubernetes, infrastructure, deployment, data and storage, and security tools. Every entry uses a pinned, integrity-checked vendor artifact.

Dependencies:

google-cloud-bigquery==3.25.0
pyairtable>=3.0
pandas

System packages:

shellcheck
pandoc
graphviz

Then open the block's advanced options and choose the sandbox under Sandbox.

The default and custom behavior is intentionally explicit:

  • JavaScript without imports or file-path mounts stays in the local isolated runtime and ignores the sandbox selection.
  • 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.
  • Python and Shell always run remotely. With no selection they get only the Function base; with a sandbox they get its dependencies, system packages, and managed CLI tools.

When a Function block next loads its sandbox options, a successful lookup that confirms its selected sandbox was deleted clears that selection. Fetch or auth failures leave the workflow unchanged.

Two sandboxes with the same language, dependency list, system packages, and managed CLI tools share one build, so duplicating a set costs nothing. Editing any part of that install specification starts a new build. Runs already in flight keep using the old one. Deleting a sandbox frees its build once nothing else uses it.

Build status

On sim.ai, each sandbox specification is prebuilt into a reusable image, so runs pay no install cost. The status row in Settings shows Queued, Building, Ready, or Failed. A failed build reports what went wrong — a package that does not exist, a version that has no match, a resolver conflict — with the installer log behind a disclosure.

Running a block before its sandbox is Ready stops the run and shows you the status. A failed build is retried periodically on its own. To retry immediately, open the three-dot menu at the end of the failed status row and choose Retry build. Save or discard unsaved sandbox edits first so the retry always uses the visible spec.

On a self-hosted deployment using Daytona, dependencies, system packages, and managed CLI tools install inside the sandbox at the start of every run instead, adding startup time per execution. Prebuilt images require E2B.

System packages, managed CLIs, and one-off installs

Use System packages for CLIs available from the sandbox's Debian/APT repositories. Entries may include an architecture or exact version, such as package:architecture=version. They are validated, deduplicated, and installed when the sandbox is built or prepared.

The Managed CLI tools selector covers CLIs that need more than a normal APT installation, such as a pinned vendor archive, PATH setup, or command verification. Service and vendor CLIs are intentionally not part of the universal Function base. For example, after adding Google Cloud CLI to a sandbox, a Shell Function can run bq directly:

set -euo pipefail
bq query --use_legacy_sql=false --format=json 'SELECT CURRENT_DATE() AS today'

For a command needed only once, install it at the start of the Shell script. The remote sandbox is ephemeral, so the install applies only to that Function call and adds to its execution time:

set -euo pipefail
python -m pip install --quiet humanize
python -c "import humanize; print(humanize.intcomma(1234567))"

Use a custom sandbox instead when reproducibility or startup time matters. Put ordinary Debian utilities in System packages; use the managed selector only for one of its specialized installers. Arbitrary install commands are not saved in a sandbox specification.

Dependency restrictions

Dependency fields accept package names and version specifiers only. URLs, git+ references, -e, local paths, --index-url, and npm aliases are rejected, with the offending line number reported.

System packages use Debian coordinates in the form package[:architecture][=version]. Flags, URLs, paths, whitespace, globs, and shell syntax are rejected. A sandbox may declare up to 50 dependencies, 50 system packages, and 10 managed CLI tools.

Secret access for Agent tools

When a Function block is used as an Agent tool, All secrets lets its code read all secrets available in that run's resolved environment, using either {{MY_SECRET}} or environmentVariables['MY_SECRET']. The execution's identity, workspace access, and secret grants determine which secrets are available. Prefer {{MY_SECRET}} when the value may appear in execution logs. For protected secrets, a successful double-brace substitution activates execution-trace masking. A recognized direct environmentVariables['MY_SECRET'] read activates it too, but only when Sim can recognize the read in the code beforehand — a name built at runtime, or a file that reassigns environmentVariables itself, is not recognized. See the recognition limits.

To narrow that, set Secret access to Selected secrets in the block's tool configuration and pick the names the code may read. Selecting a name does not grant access to an otherwise unavailable secret. Two things change:

  • Only those secrets are injected. {{OTHER_SECRET}} no longer resolves either.
  • The selected names are added to the tool's description, so the model knows what it can reference. Values are bound server-side only when the Function runs; they are not included in the model request. If a Function result contains an exact secret value, the Agent model receives {{NAME}} in its place. The raw runtime result and local side effects are not rewritten.

Secrets marked Unredacted follow their configured visibility policy. See the credentials reference before changing it.

Leaving the default (All secrets) resolves the available list at run time, so a newly added secret is included when the execution has access to it.

Examples

Normalize input before writing

The Function trims the name and email, lowercases the email, and returns the object that the API block sends as its body. Add validation before the write when the destination requires a particular format.

Calculate a loyalty score

The Start input customer is an object containing name, purchaseHistory (records with amount), accountAge in days, and supportTickets with total and resolved.

loyalty-calculator.js
const { purchaseHistory, accountAge, supportTickets } = <start.customer>;

const totalSpent = purchaseHistory.reduce((sum, p) => sum + p.amount, 0);
const purchaseFrequency = accountAge > 0 ? purchaseHistory.length / (accountAge / 365) : 0;
const ticketRatio = supportTickets.total > 0 ? supportTickets.resolved / supportTickets.total : 0;

const spendScore = Math.min((totalSpent / 1000) * 30, 30);
const frequencyScore = Math.min(purchaseFrequency * 20, 40);
const supportScore = ticketRatio * 30;
const loyaltyScore = Math.round(spendScore + frequencyScore + supportScore);

return {
  customer: <start.customer.name>,
  loyaltyScore,
  loyaltyTier: loyaltyScore >= 80 ? 'Platinum' : loyaltyScore >= 60 ? 'Gold' : 'Silver',
};
loyalty-calculator.py
def calculate_loyalty(data):
    purchase_history = data["purchaseHistory"]
    account_age = data["accountAge"]
    support_tickets = data["supportTickets"]

    total_spent = sum(p["amount"] for p in purchase_history)
    purchase_frequency = len(purchase_history) / (account_age / 365) if account_age > 0 else 0
    ticket_ratio = support_tickets["resolved"] / support_tickets["total"] if support_tickets["total"] > 0 else 0

    spend_score = min(total_spent / 1000 * 30, 30)
    frequency_score = min(purchase_frequency * 20, 40)
    support_score = ticket_ratio * 30
    loyalty_score = round(spend_score + frequency_score + support_score)
    tier = "Platinum" if loyalty_score >= 80 else "Gold" if loyalty_score >= 60 else "Silver"

    return {
        "customer": data["name"],
        "loyaltyScore": loyalty_score,
        "loyaltyTier": tier,
    }

if __name__ == "__main__":
    __sim_result__ = calculate_loyalty(<start.customer>)

Large inputs

Sim hands a Function block its code, parameters, and resolved references in one request, so very large values are kept by reference rather than inlined.

Prefer a narrow reference over a whole large value: use <api.data.id> instead of <api.data>. If a JavaScript function without imports does reference a whole large value, Sim rewrites it to a lazy server-side read automatically.

Files are metadata-first: reading <file.name> or <file.url> does not load the file's contents. Read content on demand with the sim.files helpers (JavaScript without imports only):

const file = <readfile.file>;
const text = await sim.files.readText(file);
const chunk = await sim.files.readTextChunk(file, { offset: 0, length: 1024 * 1024 });
const bytes = await sim.files.readBase64Chunk(file, { offset: 0, length: 1024 * 1024 });

sim.files.readText, readBase64, and the …Chunk variants stream from execution storage under memory caps. sim.values.read(ref) and sim.values.readArray(ref) read large value and array references. Chunk offset and length are byte-based, so for exact Unicode parsing prefer smaller structured references. For large generated data in a remote Function block, write files under /tmp/sim/outputs and pass them to a compatible File or Table action. The separate Function tool exposes outputPath, outputSandboxPath, and outputTable for explicit persistence; those are not builder fields.

The lazy sim.files and sim.values helpers are available only in JavaScript functions without imports. JavaScript with imports, Python, and Shell do not support them yet.

Errors and timeout

An uncaught error follows the connected error port or fails the run. Catch an error only when your code can return a meaningful recovery result. console.log(), print(), and ordinary Shell output appear in stdout and the run logs.

Function uses the workflow's remaining execution time when a deadline exists. Without a workflow deadline, it still has a fallback timeout of 300,000 ms (5 minutes) by default, configurable through EXECUTION_TIMEOUT_FREE. An explicit internal timeout can replace that fallback or shorten the workflow budget; it cannot extend the workflow deadline. There is no separate timeout field in the current block editor. Exceeding the budget terminates execution and reports a block error.