# Function (/workflows/blocks/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.

<BlockPreview type="function" />

## Reshape an API response [#reshape-an-api-response]

<WorkflowPreview workflow="FUNCTION_RESHAPE_WORKFLOW" />

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

```javascript
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 [#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.

| Feature | JavaScript | Python | Shell |
| - | - | - | - |
| **Execution** | Local without imports or file-path mounts; remote otherwise | 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 |

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](/platform/enterprise/self-hosted) 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 [#configuration]

### Code [#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}}`:

<Tabs items="['JavaScript', 'Python', 'Shell']">
  <Tab value="JavaScript">
    ```javascript
    const data = <api.data>;
    return data.items.filter((i) => i.active).map((i) => i.id);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    data = <api.data>
    __sim_result__ = [item["id"] for item in data["items"] if item["active"]]
    ```
  </Tab>

  <Tab value="Shell">
    ```bash
    set -euo pipefail
    active_ids=$(printf '%s' <api.data> | jq -c '[.items[] | select(.active) | .id]')
    printf '__SIM_RESULT__=%s\n' "$active_ids"
    ```
  </Tab>
</Tabs>

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 [#secret-placeholders-in-code]

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

```javascript
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 [#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:

```javascript
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](/workflows/blocks/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:

```javascript
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:

```bash
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 [#outputs]

| Output | What 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 [#files]

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

```python
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.

<Callout type="warn">
  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.
</Callout>

## Sandboxes [#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](/platform/enterprise/self-hosted)). 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 [#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 [#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:

```bash
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:

```bash
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-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 [#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](/platform/credentials#execution-log-protection). 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](/platform/credentials#execution-log-protection).

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 [#examples]

### Normalize input before writing [#normalize-input-before-writing]

<WorkflowPreview workflow="FUNCTION_VALIDATE_WORKFLOW" />

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 [#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`.

<Tabs items="['JavaScript', 'Python']">
  <Tab value="JavaScript">
    ```javascript title="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',
    };
    ```
  </Tab>

  <Tab value="Python">
    ```python title="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>)
    ```
  </Tab>
</Tabs>

## Large inputs [#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):

```javascript
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 [#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.

## Related docs [#related-docs]

* [Passing files](/files/passing-files)
* [Sandbox setup](/platform/self-hosting/sandboxes)
* [Credentials and execution-log protection](/platform/credentials#execution-log-protection)
