Actana

References

Passing files between blocks — the UserFile shape and downloadFileFromStorage.

When a workflow block emits a file, it emits a UserFile — a small JSON shape that other blocks can consume. Knowing the shape lets you pass files end-to-end through a workflow without bouncing through the upload API.

The UserFile shape

UserFile lives in lib/uploads/utils/file-utils.ts:

FieldNotes
nameDisplay name. Same as workspace_files.originalName.
keyObject-storage key. The unambiguous identity of the file.
contextOne of the file contextsworkspace, chat, execution, etc.
mimeTypeFrom workspace_files.contentType.
sizeBytes.
pathOptional. Local path inside a sandbox or worker.
urlOptional. Pre-resolved serve URL.

Most blocks accept any of path, url, or key and resolve to a serve URL via resolveFilePathFromInput:

// blocks/blocks/file.ts
if (typeof record.key === 'string' && record.key.trim() !== '') {
  const key = record.key.trim()
  const context = typeof record.context === 'string'
    ? record.context
    : inferContextFromKey(key)
  return `/api/files/serve/${encodeURIComponent(key)}?context=${context}`
}

Resolution order: pathurlkey (with context either explicit or inferred from the key prefix).

Producing a UserFile from a block

Two patterns:

Block input (basic / advanced mode)

// Basic — file-upload UI
{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' }

// Advanced — reference an upstream block's file output
{ id: 'fileRef',    type: 'short-input', canonicalParamId: 'file', mode: 'advanced' }

Inside tools.config, normalize the two inputs into a single canonical param:

import { normalizeFileInput } from '@/blocks/utils'

const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true })
if (file) params.file = file

Tool output (file producers)

Tools that produce files use FileToolProcessor in transformResponse to materialize bytes into the file store and return a UserFile:

import { FileToolProcessor } from '@/executor/utils/file-tool-processor'

const processor = new FileToolProcessor(context)
const file = await processor.processFileData({
  data: base64Content,
  mimeType: 'application/pdf',
  filename: 'doc.pdf',
})
// file is a UserFile — return it from transformResponse.

The processor inserts a workspace_files row with the right context (typically execution) and returns the resulting UserFile.

Reading file bytes inside a server route

Server-side code that needs the file contents (parsing, sending to an external API) calls downloadFileFromStorage:

import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'

const buffer = await downloadFileFromStorage(userFile)
// buffer: Buffer — feed to a parser, hash, or external API.

When an integration block accepts a file, the recommended pattern is:

  1. Accept the file via the basic/advanced input pair above.
  2. Route the tool call to an internal API route (/api/tools/{service}/upload) instead of calling the external API directly.
  3. In the internal route, use downloadFileFromStorage to load bytes, then forward to the external API.

This keeps storage credentials server-side and avoids bouncing the file through the browser.

Inferring context from a key

Files written through the upload API typically encode the context into the storage prefix (e.g. chat/{chatId}/..., execution/{runId}/...). inferContextFromKey parses the key and returns the matching context, so callers that only have the key can still serve the file correctly.

Never construct a serve URL by hand from a key without setting context. The serve route uses context to authorize the read; an unknown context returns 403 even for valid keys.

Source

  • apps/actana/lib/uploads/utils/file-utils.tsUserFile, inferContextFromKey
  • apps/actana/lib/uploads/utils/file-utils.server.tsdownloadFileFromStorage
  • apps/actana/blocks/utils.tsnormalizeFileInput
  • apps/actana/executor/utils/file-tool-processor.tsFileToolProcessor
  • apps/actana/blocks/blocks/file.ts — reference resolver

On this page