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
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