Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions apps/docs/content/docs/integrations/file.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ In Sim, the File block allows your agents to search, read, and extract text from

## Usage Instructions

Read workspace file objects, search indexed text across the workspace or selected folder scopes, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files, append content to existing files, compress files into a .zip archive, extract a .zip archive into the workspace, or manage the public share link for a file.
Read workspace file objects, search indexed text across the workspace or selected folder scopes, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files at relative paths, append content to existing files, compress files into a .zip archive, extract a .zip archive into the workspace, or manage the public share link for a file.



Expand Down Expand Up @@ -127,13 +127,13 @@ Fetch and parse a file from a URL with optional custom headers.

### File Write

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") unless overwrite is enabled.
Create a new workspace file at a relative path, either from text content or from an existing file. Missing folders are created automatically. If a file with the same name already exists, a numeric suffix is added (e.g., "data (1).csv") unless overwrite is enabled.

#### Input

| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `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 unless overwrite is enabled. |
| `fileName` | string | No | Relative workspace file path \(e.g., "Reports/2026/report.md"\). Missing folders are created automatically. 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 unless overwrite is enabled. |
| `folderPath` | string | No | Folder to create the file in. Omit for the workspace root. Canonical folder path, percent-encoded, e.g. "/Reports/Q3%20Results". The workspace root is "/". |
| `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. |
Expand All @@ -147,6 +147,7 @@ Create a new workspace file, either from text content or from an existing file.
| --------- | ---- | ----------- |
| `id` | string | File ID |
| `name` | string | File name |
| `vfsPath` | string | Canonical workspace path of the created file \(e.g., files/Reports/2026/report.md\) |
| `size` | number | File size in bytes |
| `url` | string | URL to access the file |
| `version` | number | Version number of the content this write recorded |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { ROOT_FOLDER_PATH } from '@/lib/folders/paths'
import { readFolderPaths } from '@/lib/folders/selection'
import { formatFileSize, getExtensionFromMimeType } from '@/lib/uploads/utils/file-utils'
import { containsReference } from '@/lib/workflows/sanitization/references'
import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path'
import { isFileInFolderScope } from '@/lib/workspace-files/folder-path-selection'
import { findSelectedWorkspaceFile } from '@/lib/workspace-files/selection'
import { getWorkspaceFileDisplayLabel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/workspace-file-display'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { getWorkflowSearchLabelHighlight } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/workflow-search-highlight'
import { useActiveCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value'
Expand Down Expand Up @@ -68,23 +68,6 @@ interface FileUploadProps {
onValueChange?: (value: UploadedFile | UploadedFile[] | null) => void
}

/**
* Label for a workspace file, prefixed with its folder so two files sharing a
* name are distinguishable.
*
* The stored folder path escapes a slash inside a folder name, so it is decoded
* into segments rather than split — otherwise a folder named `Q3/Q4` reads as
* two levels.
*/
function workspaceFileOptionLabel(file: { name: string; folderPath?: string | null }): string {
if (!file.folderPath) return file.name
try {
return `${parseWorkspaceFileFolderDisplayPath(file.folderPath).join(' / ')} / ${file.name}`
} catch {
return file.name
}
}

function byFolderThenName(
a: { name: string; folderPath?: string | null },
b: { name: string; folderPath?: string | null }
Expand Down Expand Up @@ -126,6 +109,7 @@ export interface UploadedFile {

interface SingleFileSelectorProps {
file: UploadedFile
displayName: string
options: Array<{ label: string; value: string; disabled?: boolean }>
selectedValue: string
onInputChange: (value: string) => void
Expand All @@ -145,6 +129,7 @@ interface SingleFileSelectorProps {
*/
function SingleFileSelector({
file,
displayName,
options,
selectedValue,
onInputChange,
Expand All @@ -156,7 +141,7 @@ function SingleFileSelector({
isDeleting,
workflowSearchHighlight,
}: SingleFileSelectorProps) {
const displayLabel = `${truncateMiddle(file.name, 20, 12)} (${workspaceFileSizeLabel(file.size)})`
const displayLabel = `${truncateMiddle(displayName, 20, 12)} (${workspaceFileSizeLabel(file.size)})`
const [searchQuery, setSearchQuery] = useState('')
const [isEditing, setIsEditing] = useState(false)
// When not editing, always show the file's display label. When editing, show the user's query.
Expand Down Expand Up @@ -736,7 +721,9 @@ export function FileUpload({
const renderFileItem = (file: UploadedFile, index: number) => {
const fileKey = file.path || ''
const isDeleting = deletingFiles[fileKey]
const displayName = truncateMiddle(file.name)
const matchedWorkspaceFile = findSelectedWorkspaceFile(file, workspaceFiles)
const fullDisplayName = getWorkspaceFileDisplayLabel(matchedWorkspaceFile ?? file)
const displayName = truncateMiddle(fullDisplayName)
const workflowSearchHighlight = getWorkflowSearchLabelHighlight({
activeSearchTarget,
blockId,
Expand All @@ -750,7 +737,7 @@ export function FileUpload({
key={fileKey}
className='relative rounded-sm border border-[var(--border-1)] bg-[var(--surface-5)] px-2 py-1.5 hover-hover:bg-[var(--surface-active)] dark:bg-[var(--surface-5)]'
>
<div className='truncate pr-6 text-sm' title={file.name}>
<div className='truncate pr-6 text-sm' title={fullDisplayName}>
<span className='text-[var(--text-primary)]'>
{formatDisplayText(displayName, { workflowSearchHighlight })}
</span>
Expand Down Expand Up @@ -810,7 +797,7 @@ export function FileUpload({
const isAccepted =
!acceptedTypes || acceptedTypes === '*' || isFileTypeAccepted(file.type, acceptedTypes)
return {
label: workspaceFileOptionLabel(file),
label: getWorkspaceFileDisplayLabel(file),
value: file.id,
// When cloud is required, local workspace files are also unpublishable.
disabled: !isAccepted || cloudUploadBlocked,
Expand All @@ -832,7 +819,7 @@ export function FileUpload({
const isAccepted =
!acceptedTypes || acceptedTypes === '*' || isFileTypeAccepted(file.type, acceptedTypes)
return {
label: workspaceFileOptionLabel(file),
label: getWorkspaceFileDisplayLabel(file),
value: file.id,
disabled: !isAccepted || cloudUploadBlocked,
}
Expand Down Expand Up @@ -958,6 +945,9 @@ export function FileUpload({
{hasFiles && !multiple && !isUploading && (
<SingleFileSelector
file={filesArray[0]}
displayName={getWorkspaceFileDisplayLabel(
findSelectedWorkspaceFile(filesArray[0], workspaceFiles) ?? filesArray[0]
)}
options={singleFileOptions}
selectedValue={selectedFileId}
onInputChange={handleComboboxChange}
Expand All @@ -974,7 +964,13 @@ export function FileUpload({
blockId,
subBlockId,
valuePath: [],
label: `${truncateMiddle(filesArray[0].name, 20, 12)} (${workspaceFileSizeLabel(filesArray[0].size)})`,
label: `${truncateMiddle(
getWorkspaceFileDisplayLabel(
findSelectedWorkspaceFile(filesArray[0], workspaceFiles) ?? filesArray[0]
),
20,
12
)} (${workspaceFileSizeLabel(filesArray[0].size)})`,
})}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { getWorkspaceFileDisplayLabel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/workspace-file-display'

const reportsFile = {
id: 'file-reports',
name: 'report.md',
key: 'workspace/workspace-1/report-reports.md',
path: '/api/files/serve/report-reports',
folderPath: 'Reports/2026',
}

describe('workspace file picker display', () => {
it('shows folder breadcrumbs while keeping root-level labels compact', () => {
expect(getWorkspaceFileDisplayLabel(reportsFile)).toBe('Reports / 2026 / report.md')
expect(getWorkspaceFileDisplayLabel({ name: 'root.md', folderPath: null })).toBe('root.md')
})

it('decodes escaped slashes in folder display paths', () => {
expect(
getWorkspaceFileDisplayLabel({
name: 'contract.pdf',
folderPath: 'Finance\\/Legal/2026',
})
).toBe('Finance/Legal / 2026 / contract.pdf')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folder-display-path'

interface WorkspaceFileDisplayRecord {
id: string
name: string
key: string
path: string
folderPath?: string | null
}

/** Formats a workspace file as a readable path while keeping root-level labels compact. */
export function getWorkspaceFileDisplayLabel(
file: Pick<WorkspaceFileDisplayRecord, 'name' | 'folderPath'>
): string {
if (!file.folderPath) return file.name

try {
return [...parseWorkspaceFileFolderDisplayPath(file.folderPath), file.name].join(' / ')
} catch {
return `${file.folderPath} / ${file.name}`
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const { stubInput } = vi.hoisted(() => {
vi.mock('@sim/emcn', () => ({
Button: ({ children }: { children?: ReactNode }) => <button type='button'>{children}</button>,
cn: (...classes: unknown[]) => classes.filter(Boolean).join(' '),
Info: ({ children }: { children?: ReactNode }) => <span data-info='true'>{children}</span>,
Input: () => null,
Label: ({ children }: { children?: ReactNode }) => <span>{children}</span>,
Tooltip: ({ children }: { children?: ReactNode }) => <>{children}</>,
Expand Down Expand Up @@ -108,3 +109,17 @@ describe('SubBlock password forwarding', () => {
expect(html).toContain('data-password="on"')
})
})

describe('SubBlock label guidance', () => {
it('renders configured tooltip content beside the field label', () => {
const html = renderSubBlock({
id: 'fileName',
title: 'File Path',
type: 'short-input',
tooltip: 'Missing folders are created automatically.',
})

expect(html).toContain('data-info="true"')
expect(html).toContain('Missing folders are created automatically.')
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type JSX, type MouseEvent, memo, useCallback, useMemo, useRef, useState } from 'react'
import { Button, cn, Input, Label, Tooltip } from '@sim/emcn'
import { Button, cn, Info, Input, Label, Tooltip } from '@sim/emcn'
import {
ArrowLeftRight,
ArrowUp,
Expand Down Expand Up @@ -258,6 +258,11 @@ const renderLabel = (
<Label className='flex items-baseline gap-1.5 whitespace-nowrap'>
{config.title}
{required && <span className='ml-0.5'>*</span>}
{config.tooltip && !wandState?.isPreview && (
<Info side='top' align='start' className='text-[var(--text-muted)]'>
{config.tooltip}
</Info>
)}
{labelSuffix}
{config.type === 'code' &&
config.language === 'json' &&
Expand Down
29 changes: 29 additions & 0 deletions apps/sim/blocks/blocks/file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,35 @@ describe('FileV5Block', () => {
).toBe(false)
})

it('keeps the saved fileName key while presenting nested paths', () => {
const pathSubBlock = FileV5Block.subBlocks.find((subBlock) => subBlock.id === 'fileName')
expect(pathSubBlock).toMatchObject({
id: 'fileName',
title: 'File Path',
placeholder: 'Reports/2026/report.md',
tooltip: 'Relative workspace path. Missing folders are created automatically.',
})
expect(
buildParams({
operation: 'file_write',
fileName: 'Reports/2026/report.md',
content: 'report',
_context: { workspaceId: 'workspace-1' },
})
).toEqual({
fileName: 'Reports/2026/report.md',
folderPath: undefined,
content: 'report',
contentType: undefined,
overwrite: false,
workspaceId: 'workspace-1',
})
expect(FileV5Block.outputs.vfsPath).toMatchObject({
type: 'string',
description: expect.stringContaining('Canonical workspace path'),
})
})

it('keeps the builder-configured search limit as a fixed hard cap', () => {
expect(
buildParams({
Expand Down
19 changes: 14 additions & 5 deletions apps/sim/blocks/blocks/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ export const FileV5Block: BlockConfig = {
description:
'Read, search, get content, fetch, write, append, compress, decompress, and manage sharing for files',
longDescription:
'Read workspace file objects, search indexed text across the workspace or selected folder scopes, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files, append content to existing files, compress files into a .zip archive, extract a .zip archive into the workspace, or manage the public share link for a file.',
'Read workspace file objects, search indexed text across the workspace or selected folder scopes, extract the text content of files, fetch and parse files from URLs with optional headers, write new workspace files at relative paths, append content to existing files, compress files into a .zip archive, extract a .zip archive into the workspace, or manage the public share link for a file.',
hideFromToolbar: false,
bestPractices: `
- Read returns workspace file objects in the "files" output and does NOT include their text. It accepts selected files, canonical file IDs, or one or more workspace folders expanded at run time. Use it to pick files or pass file references downstream (e.g. as attachments).
Expand All @@ -1125,7 +1125,7 @@ export const FileV5Block: BlockConfig = {
- Search is eventually consistent. Check "complete" and "indexStatus" when pending, failed, skipped, or partially indexed files matter to the task.
- Read, Get Content, Search, Append, Apply Edit, and Compress share a Folder scope. Pick folders, or switch the field to advanced and type canonical percent-encoded paths, comma-separated for several, including a reference from an earlier block such as /memory/<start.userId>.
- Use Fetch for external file URLs. Add headers for authenticated downloads, for example Slack private file URLs require an Authorization Bearer token.
- Use Write to create a new workspace file and Append to add content to an existing one. Write adds a numeric suffix when the name is taken; turn on "Overwrite Existing File" to replace the contents of the file at that exact path (folder and name) instead — a same-named file in another folder is left alone.
- Use Write to create a new workspace file and Append to add content to an existing one. Write accepts relative paths such as Reports/2026/report.md and creates missing folders automatically. Write adds a numeric suffix when the name is taken; turn on "Overwrite Existing File" to replace the contents of the file at that exact path (folder and name) instead — a same-named file in another folder is left alone.
- Use Compress to bundle one or more files into a single .zip archive stored in the workspace. The new archive is returned in the "files" output.
- Use Decompress to extract a .zip archive back into the workspace; the extracted files are returned in the "files" output, ready to chain into Get Content or downstream blocks.
`,
Expand Down Expand Up @@ -1363,9 +1363,11 @@ export const FileV5Block: BlockConfig = {
},
{
id: 'fileName',
title: 'File Name',
title: 'File Path',
type: 'short-input' as SubBlockType,
placeholder: 'File name (e.g., data.csv)',
placeholder: 'Reports/2026/report.md',
description: 'Relative workspace path. Missing folders are created automatically.',
tooltip: 'Relative workspace path. Missing folders are created automatically.',
condition: { field: 'operation', value: 'file_write' },
required: { field: 'operation', value: 'file_write' },
},
Expand Down Expand Up @@ -2220,7 +2222,10 @@ export const FileV5Block: BlockConfig = {
fileUrl: { type: 'string', description: 'External file URL for fetch' },
headers: { type: 'json', description: 'Request headers for fetch' },
fileType: { type: 'string', description: 'File type for fetch' },
fileName: { type: 'string', description: 'Name for a new file (write)' },
fileName: {
type: 'string',
description: 'Relative workspace path for a new file (write)',
},
content: { type: 'string', description: 'File content to write' },
writeFileInput: {
type: 'json',
Expand Down Expand Up @@ -2390,6 +2395,10 @@ export const FileV5Block: BlockConfig = {
type: 'string',
description: 'File name (write and append)',
},
vfsPath: {
type: 'string',
description: 'Canonical workspace path of the created file (write)',
},
size: {
type: 'number',
description: 'File size in bytes (write and append)',
Expand Down
Loading
Loading