Files
sim/apps/docs/content/docs/en/workflows/blocks/function.mdx
T
Vikhyath MondretiandClaude Opus 5 237f973a11 fix(condition): stop a secret value from breaking or forging a condition (#6705)
Condition expressions pasted every environment variable value into the
expression as source. Block references in the same expression go through a
proper escape and get quoted; env vars went through neither. That left three
defects:

- A bare string placeholder was a SyntaxError. `{{NAME}} === 'alice'` resolved
  to `alice === 'alice'`, so the form the Function block docs recommend could
  not be used here at all.
- Ordinary data broke the block. An apostrophe (`O'Brien`) or a newline in a
  legitimate value produced unparseable source and failed the run.
- The quoted form was injectable. A value of `x' || true || '` turned
  `'{{NAME}}' === 'bob'` into `'x' || true || '' === 'bob'`, forging a true
  branch out of a comparison that should be false.

Inline only structurally inert literals — numbers, booleans, and null, with
optional space/tab padding. Every other value keeps its `{{NAME}}` placeholder
and is bound as a string by the execution-boundary compiler, the same one
Function blocks and Custom Tools already use.

Legacy outcomes are preserved. `{{COUNT}} === 3` and `{{ENABLED}} === true`
still compare as literals, and an embedded `"Bearer {{API_KEY}}"` still
compares equal — now via compiled concatenation rather than a pasted value.
Padding is admitted rather than trimmed so the inlined text stays
byte-identical to the stored value, which is what keeps a padded number
correct both bare and quoted.

A resolved secret also no longer travels to the execution boundary inside the
condition source.

The one deliberate behavior change: a value whose text is itself a quoted JS
literal (a secret stored as `'foo'`, a plausible workaround for the bare-string
SyntaxError) now compares as the 5-character string rather than as source.
That form is the injectable one, so it cannot be kept.

Docs: state the placeholder type contract, which was described mechanically but
never in terms of what a reader gets. `{{KEY}}` in Function and Custom Tool code
always evaluates to a string, so a bare `if ({{FLAG}})` is always true and a list
has to be stored as JSON. This is what a customer hit after the resolver lift in
 #6247 moved Function blocks off source inlining.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:50:56 -07:00

406 lines
21 KiB
Plaintext

---
title: Function
description: The Function block runs your JavaScript, Python, or Shell code as a step and returns what it produces.
---
import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
import { BlockPreview, WorkflowPreview, FUNCTION_RESHAPE_WORKFLOW, FUNCTION_VALIDATE_WORKFLOW } from '@/components/workflow-preview'
import { FAQ } from '@/components/ui/faq'
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" />
## 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}}`:
<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
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
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}})` is always true, because 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
| Output | What it is |
| --- | --- |
| `<function.result>` | The value your code returns (object, array, string, number, …) |
| `<function.stdout>` | Anything printed with `console.log()` or `print()` |
## 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 | Python | Shell |
| --- | --- | --- | --- |
| **Execution** | Local when there are no imports; remote with imports | 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 |
<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
[Function base](/platform/enterprise/self-hosted) first. Any Python figures you
generate are captured as images automatically.
</Callout>
<Callout type="info">
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.
</Callout>
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.
## 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** stays in the local isolated runtime for speed 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.
<Callout type="info">
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.
</Callout>
### 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:
```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 csvkit
csvcut -n /tmp/input.csv
```
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.
### What is allowed
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.
## Scoping secrets for agent tools
When a Function block is used as an Agent tool, its code can read every workspace
secret by default — both `{{MY_SECRET}}` and `environmentVariables['MY_SECRET']`.
Use `{{MY_SECRET}}` when the value may appear in execution logs: a successful
double-brace substitution activates [execution-trace masking](/platform/credentials#execution-log-protection),
while direct `environmentVariables['MY_SECRET']` access alone does not activate
it by itself.
To narrow that, set **Secret access** to *Selected secrets* in the block's
tool configuration and pick the names the code may read. 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.
Leaving the default (*All secrets*) resolves the list at run time, so a secret
added next month is included automatically.
## Examples
### Reshape an API response
<WorkflowPreview workflow={FUNCTION_RESHAPE_WORKFLOW} />
The Function reads `<api.data>`, returns just the field the rest of the workflow needs, and exposes it as `<extract.result>`.
### Validate input before writing
<WorkflowPreview workflow={FUNCTION_VALIDATE_WORKFLOW} />
The Function sanitizes the form input and returns the clean value, which the API block sends as its body.
### A worked example: loyalty score
<Tabs items={['JavaScript', 'Python']}>
<Tab value="JavaScript">
```javascript title="loyalty-calculator.js"
const { purchaseHistory, accountAge, supportTickets } = <agent>;
const totalSpent = purchaseHistory.reduce((sum, p) => sum + p.amount, 0);
const purchaseFrequency = purchaseHistory.length / (accountAge / 365);
const ticketRatio = supportTickets.resolved / supportTickets.total;
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: <agent.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)
ticket_ratio = support_tickets["resolved"] / support_tickets["total"]
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(<agent>)
```
</Tab>
</Tabs>
## 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, write the result to a file or table with `outputPath`, `outputSandboxPath`, or `outputTable` instead of returning the whole payload inline.
<Callout type="warn">
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.
</Callout>
## Best Practices
{/* Keep the error-path guidance below prominent: fewer than 1% of deployed workflows connect an error port (platform data, 2026-06) — under-adopted, not under-needed. */}
- **Keep each function focused.** One transform per block is easier to read, test, and debug.
- **Handle errors.** Wrap risky code in `try`/`catch` and return a clear message, or let it throw to the error path.
- **Reference only what you need.** Pull a narrow field rather than a whole large object to keep values out of the request body.
- **Use stdout to debug.** `console.log()`, `print()`, and ordinary shell output land in `<function.stdout>` and the run logs.
<FAQ items={[
{ question: "What languages does the Function block support?", answer: "JavaScript, Python, and Shell. JavaScript is the default. Python remains a stable saved language choice; Shell and custom Sandbox controls appear when a remote sandbox provider is enabled. Python and Shell execution require that provider." },
{ question: "When does code run locally vs. in a sandbox?", answer: "JavaScript without external imports runs in a local isolated sandbox for speed. JavaScript that uses import or require, Python, and Shell run in the configured remote sandbox." },
{ question: "Does JavaScript still work without E2B or Daytona?", answer: "Yes. JavaScript without import or require runs in Sim's local isolated VM and does not require a remote provider. JavaScript with external imports, Python, Shell, and custom Sandboxes require E2B or Daytona and fail explicitly when it is unavailable." },
{ question: "How do I reference outputs from other blocks inside my code?", answer: "Use angle-bracket syntax directly, like <agent.content> or <api.data>, with no quotes around the tag — Sim replaces it with the real value before execution. For environment variables, use double curly braces: {{API_KEY}}." },
{ question: "What does the Function block return?", answer: "Two outputs: result and stdout. Use return in JavaScript, assign __sim_result__ in Python, or print an __SIM_RESULT__= marker in Shell to set result. Ordinary console, print, and command output goes to stdout." },
{ question: "Can I make HTTP requests from a Function block?", answer: "Yes. fetch() is available in JavaScript with async/await. In Python, use requests or httpx. In Shell, use curl or a CLI available on the selected sandbox." },
{ question: "Is there a timeout for Function block execution?", answer: "Yes, a configurable execution timeout. If your code exceeds it, the run is terminated and the block reports an error. Keep this in mind for external calls or heavy processing." },
]} />