Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
de8027b
feat(function): mount referenced files into the code sandbox
icecrasher321 Aug 29, 2026
190da5d
Merge remote-tracking branch 'origin/staging' into staging-v83
icecrasher321 Aug 29, 2026
88cd9f0
test(execution): cover the sim.* helpers in a real isolate
icecrasher321 Aug 29, 2026
23fcf74
chore: regenerate tool metadata and integration docs
icecrasher321 Aug 29, 2026
7ff3f65
Merge remote-tracking branch 'origin/staging' into staging-v83
icecrasher321 Aug 29, 2026
81d00b8
Merge remote-tracking branch 'origin/staging' into staging-v83
icecrasher321 Aug 29, 2026
c9d2f2f
fix(function): address review findings on sandbox file I/O
icecrasher321 Aug 29, 2026
9870c0d
docs(function): include files in the FAQ's output answer
icecrasher321 Aug 29, 2026
ceb987b
fix(function): scan every harvested output for resolved secrets
icecrasher321 Aug 29, 2026
1928fad
Merge remote-tracking branch 'origin/staging' into staging-v83
icecrasher321 Aug 29, 2026
d5e562a
fix(function): correct provenance, mount caps and runtime file plumbing
icecrasher321 Aug 29, 2026
a360221
Merge remote-tracking branch 'origin/staging' into staging-v83
icecrasher321 Aug 29, 2026
ce16587
fix(file): send one write source, and bound mounts by what they were …
icecrasher321 Aug 29, 2026
c994282
fix(file): accept the picker shape, and bound a mount while it downloads
icecrasher321 Aug 29, 2026
df197a0
Merge remote-tracking branch 'origin/staging' into staging-v83
icecrasher321 Aug 29, 2026
43f37cf
fix(file): clamp a declared mount cap, and demand identity not full m…
icecrasher321 Aug 29, 2026
cc88c8a
fix(file): answer null for a key a file input cannot be classified by
icecrasher321 Aug 29, 2026
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
9 changes: 5 additions & 4 deletions apps/docs/content/docs/integrations/file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,16 @@ Fetch and parse a file from a URL with optional custom headers.

### File Write

Create a new workspace file. If a file with the same name already exists, a numeric suffix is added (e.g., "data (1).csv").
Create a new workspace file, either from text content or from an existing file. If a file with the same name already exists, a numeric suffix is added (e.g., "data (1).csv").

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `fileName` | string | Yes | File name \(e.g., "data.csv"\). If a file with this name exists, a numeric suffix is added automatically. |
| `content` | string | Yes | The text content to write to the file. |
| `contentType` | string | No | MIME type for new files \(e.g., "text/plain"\). Auto-detected from file extension if omitted. |
| `fileName` | string | No | File name \(e.g., "data.csv"\). Required when writing text; optional when storing a file, which keeps its own name unless this overrides it. If the name already exists, a numeric suffix is added automatically. |
| `content` | string | No | The text content to write to the file. Provide exactly one of content or fileInput. |
| `fileInput` | file | No | An existing file to store in the workspace, such as one produced by an earlier tool. Use this for anything that is not text — PDFs, images, audio, archives. Provide exactly one of content or fileInput. |
Comment thread
icecrasher321 marked this conversation as resolved.
| `contentType` | string | No | MIME type for new files \(e.g., "text/plain"\). Auto-detected from the file extension, or taken from the stored file, if omitted. |

#### Output

Expand Down
47 changes: 45 additions & 2 deletions apps/docs/content/docs/workflows/blocks/function.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,49 @@ Sim supplies the rendered heredoc privately while preserving the quoted delimite
| --- | --- |
| `<function.result>` | The value your code returns (object, array, string, number, …) |
| `<function.stdout>` | Anything printed with `console.log()` or `print()` |
| `<function.files>` | Files your code wrote to `/tmp/sim/outputs`, ready to attach or upload |
Comment thread
icecrasher321 marked this conversation as resolved.
Comment thread
icecrasher321 marked this conversation as resolved.

## Files

**Reading.** Reference a file's `path` and it is mounted for you:

```python
import pandas as pd

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

`.path` 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
Comment thread
icecrasher321 marked this conversation as resolved.
`<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 one exception is a call that names an explicit `outputSandboxPath`. That asks
for particular paths to be exported and answers with that export's own result, so
the output directory is not harvested alongside it — choose one or the other rather
than expecting both in the same run.

<Callout type="info">
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, 50MB 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>

<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. Write such a file to a
workspace path instead, or keep the secret out of the output.
</Callout>

## Language

Expand Down Expand Up @@ -401,8 +444,8 @@ The lazy `sim.files` and `sim.values` helpers are available only in JavaScript f
{ 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: "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}}. To read a file, reference its path — <gmail.attachments[0].path> mounts it and resolves to a location any language can open." },
{ question: "What does the Function block return?", answer: "Three outputs: result, stdout, and files. 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. Anything your code writes to /tmp/sim/outputs comes back in files as a file object later blocks can accept directly." },
{ 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." },
]} />
44 changes: 41 additions & 3 deletions apps/sim/blocks/blocks/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const APPEND_FILE_FIELD = ['appendFile', 'appendFileName'] as const
const COMPRESS_FILE_FIELD = ['compressFile', 'compressFileId'] as const
const DECOMPRESS_FILE_FIELD = ['decompressFile', 'decompressFileId'] as const
const SHARE_FILE_FIELD = ['shareFile', 'shareFileId'] as const
/* Text and file are mutually exclusive sources, so the clause names whichever
one the card actually carries. */
const WRITE_CONTENT_FIELD = ['content', 'writeFile', 'writeFileId'] as const

export const FileBlock: BlockConfig<FileParserOutput> = {
type: 'file',
Expand Down Expand Up @@ -921,7 +924,7 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
file_fetch: [{ text: 'Fetch and parse', field: 'fileUrl', core: true }],
file_write: [
{ text: 'Create', field: 'fileName', core: true },
{ text: 'containing', field: 'content' },
{ text: 'containing', field: WRITE_CONTENT_FIELD },
],
file_append: [
{ text: 'Append', field: 'appendContent', core: true },
Expand Down Expand Up @@ -1031,7 +1034,25 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
type: 'long-input' as SubBlockType,
placeholder: 'File content to write...',
condition: { field: 'operation', value: 'file_write' },
required: { field: 'operation', value: 'file_write' },
},
{
id: 'writeFile',
title: 'File',
type: 'file-upload' as SubBlockType,
canonicalParamId: 'writeFileInput',
acceptedTypes: '*',
placeholder: 'Store an existing file',
mode: 'basic',
condition: { field: 'operation', value: 'file_write' },
},
{
id: 'writeFileId',
title: 'File',
type: 'short-input' as SubBlockType,
canonicalParamId: 'writeFileInput',
placeholder: 'File from an earlier block',
mode: 'advanced',
condition: { field: 'operation', value: 'file_write' },
},
{
id: 'contentType',
Expand Down Expand Up @@ -1206,9 +1227,22 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
const operation = params.operation || 'file_read'

if (operation === 'file_write') {
// Writing stores one file, so the single form.
const fileInput = normalizeFileInput(params.writeFileInput, { single: true })
// The contract counts any defined `content` as "text was provided", and
// an untouched Content box serializes as an empty string — so sending it
// unconditionally would make every file write collide with its own empty
// text box. The selected file is what disambiguates: with one present,
// an empty Content box means "not used" and is dropped, while a
// non-empty one is still forwarded so the contract can report that both
// were filled. With no file, `content` always goes through, which keeps
// writing a deliberately empty text file possible.
const contentText = typeof params.content === 'string' ? params.content : undefined
const omitContent = Boolean(fileInput) && !contentText
return {
fileName: params.fileName,
content: params.content,
...(omitContent ? {} : { content: params.content }),
...(fileInput ? { fileInput } : {}),
Comment thread
icecrasher321 marked this conversation as resolved.
Comment thread
icecrasher321 marked this conversation as resolved.
contentType: params.contentType,
workspaceId: params._context?.workspaceId,
}
Expand Down Expand Up @@ -1432,6 +1466,10 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
fileType: { type: 'string', description: 'File type for fetch' },
fileName: { type: 'string', description: 'Name for a new file (write)' },
content: { type: 'string', description: 'File content to write' },
writeFileInput: {
type: 'json',
description: 'An existing file to store in the workspace, instead of text content',
},
contentType: { type: 'string', description: 'MIME content type for write' },
appendFileInput: { type: 'json', description: 'File to append to' },
appendContent: { type: 'string', description: 'Content to append to file' },
Expand Down
33 changes: 33 additions & 0 deletions apps/sim/blocks/blocks/function.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { USER_FILE_ACCESSIBLE_PROPERTIES } from '@/lib/workflows/types'
import { FunctionBlock } from '@/blocks/blocks/function'

describe('Function block file surface', () => {
it('has no file configuration fields', () => {
// Files reach the sandbox by being referenced in code as
// <block.file.path> — the same way every other block output is referenced.
// A dedicated field would be a second way to say the same thing, and would
// need a home in the panel that the reference syntax does not.
const ids = FunctionBlock.subBlocks.map((subBlock) => subBlock.id)

expect(ids).not.toContain('files')
expect(ids).not.toContain('uploadedFiles')
expect(ids).not.toContain('collectOutputFiles')
expect(FunctionBlock.inputs).not.toHaveProperty('files')
expect(FunctionBlock.inputs).not.toHaveProperty('collectOutputFiles')
})

it('returns harvested files so downstream blocks can consume them', () => {
expect(FunctionBlock.outputs.files).toMatchObject({ type: 'file[]' })
})

it('offers path alongside base64 as a referenceable file property', () => {
// This is what puts `.path` in the tag dropdown: block-outputs.ts maps the
// list into `${path}.${prop}` suggestions.
expect(USER_FILE_ACCESSIBLE_PROPERTIES).toContain('path')
expect(USER_FILE_ACCESSIBLE_PROPERTIES).toContain('base64')
})
})
8 changes: 8 additions & 0 deletions apps/sim/blocks/blocks/function.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CodeIcon } from '@/components/icons'
import { isSandboxesEnabled } from '@/lib/core/config/env-flags'
import { CodeLanguage, getLanguageDisplayName } from '@/lib/execution/languages'
import { SANDBOX_OUTPUT_DIR } from '@/lib/execution/remote-sandbox/sandbox-paths'
import type { BlockConfig } from '@/blocks/types'
import type { CodeExecutionOutput } from '@/tools/function/types'

Expand All @@ -17,6 +18,9 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
- Shell code runs CLI commands in a remote sandbox.
- To import third-party packages or add curated CLI tools, create a sandbox in Settings > Sandboxes and select it under the block's advanced options. Without one, only the default image's packages and commands are available.
- Can reference workflow variables using <blockName.output> syntax as usual within code. Avoid XML/HTML tags.
- To read a file from an earlier block, reference its path: <blockName.files[0].path> mounts the file and resolves to its location on the sandbox filesystem, which any language can open. Use <blockName.files[0].base64> instead when you only want the contents inline in JavaScript.
- Anything the code writes to ${SANDBOX_OUTPUT_DIR} is returned as \`files\`, ready to attach to an email or upload without any extra step.
- Referencing a file path runs the block in the remote sandbox, so it is slower to start than a plain local JavaScript run.
`,
docsLink: 'https://docs.sim.ai/workflows/blocks/function',
category: 'blocks',
Expand Down Expand Up @@ -174,5 +178,9 @@ try {
type: 'string',
description: 'Console log output and debug messages from function execution',
},
files: {
type: 'file[]',
description: `Files the code wrote to ${SANDBOX_OUTPUT_DIR}, ready to attach or upload downstream`,
},
},
}
15 changes: 15 additions & 0 deletions apps/sim/executor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,21 @@ export interface ExecutionContext {
*/
toolBindingLabelCache?: Map<string, string | null>

/**
* Files produced during this execution, indexed by {@link UserFile.id}, so a
* model can name one by id in a tool argument and the runtime can hydrate it
* into the full object.
*
* Needed because a file an agent has just seen — a Gmail attachment fetched
* moments ago in the same turn — lives only in that turn's tool results, not
* in any block state or workspace row, so nothing else can resolve it. The
* index only *selects*; every read is still authorized on its own.
*
* A Map for the same reason as {@link toolBindingLabelCache}: `blockCtx` is a
* shallow clone per block execution, so only a shared reference survives.
*/
executionFilesById?: Map<string, UserFile>
Comment thread
icecrasher321 marked this conversation as resolved.

blockStates: ReadonlyMap<string, BlockState>
executedBlocks: ReadonlySet<string>

Expand Down
Loading
Loading