Workspace Files
Immutable scanned blobs with a three-step upload, byte-range download, and deferred deletion.
Workspace Files are workspace-owned blobs. Metadata is created first (pending), bytes are written
once, completion verifies size and SHA-256, then a scanner must mark the object ready before
download or attachment. Content is not rewritten in place.
The browser uses /api/files with a signed-in session and workspaceId. API clients use
/api/v1/files with a workspace API key. Both path prefixes share the same handlers.
In Command Center, open Files in the composer's workspace context strip to upload and select files. The panel expands below the context strip, above the message field, and includes Artifacts and Activity. Select Files again to close it; closing the panel keeps the current attachments selected.
Choose Load more files to continue beyond the first page. Refresh files reloads the pages you have already opened, preserving selections that are still available. If loading fails, the panel shows the error and you can refresh or retry loading more files.
Upload lifecycle
status | Meaning |
|---|---|
pending | Metadata exists. Only this state accepts PUT .../content. |
pending_scan | Bytes persisted and checksums match. A scan lease is required before ready. |
ready | Scanner returned clean. Download, Command attachments, and presentation inputs require this. |
quarantined | Scanner returned infected. Content cannot be opened. |
deletion_requested | Soft-delete. Blob remains until GC. New references are rejected. |
deleted | GC removed the blob and marked the row. |
Create (POST) with name, mimeType, sizeBytes (0–100 MiB), optional sha256 (64 hex). The
response includes uploadUrl and completeUrl derived from the request path (session URLs keep
?workspaceId=).
Then PUT the raw body to .../{id}/content. Content-Length above 100 MiB is 413 before the
body is stored. Missing body: 400. Only pending files accept content.
Then POST .../{id}/complete with optional sizeBytes / sha256. The handler completes the
upload and, while status is pending_scan, runs scanFile in the same request. HTTP status is
202 with the serialized file (often ready after a clean scan). Replay of the same
Idempotency-Key returns the stored 202 without a second scan.
The UI client polls while any listed file is pending_scan, for up to two minutes per polling
session. If scanning is still pending, it shows a message; choose Refresh files to check again
and restart polling. This limit stops browser polling, not the server scanner. Only ready files
are selectable as chat attachments.
Authorization
Files metadata and bytes require workspace.restricted.read; existing stricter session controls
still apply. Organization visibility alone does not grant this access. Each request checks current authority.
An image column in Tables retains a ready raster file by record ID.
The file cannot be requested for deletion while any record uses it. Clear those cells first.
Raster images, including server-validated images uploaded as application/octet-stream, also stay available
while an active workflow, saved run or pinned output retains them. Generic binary files are not classified as images.
Clearing the last table cell does not release those owners;
remove the owning run or pin before requesting deletion. Expired temporary references do not block deletion.
GET .../{id}/content?preview=true serves verified PNG, JPEG, WebP or GIF inline through the private
authenticated proxy; the default remains an attachment download. Responses are private and not cached.
| Surface | Principal | Workspace id |
|---|---|---|
/api/files | Browser session | Query workspaceId, else the session workspace |
/api/v1/files | Workspace API key (Bearer) | Always the key's workspace |
Session access is membership-checked. A workspaceId the user does not belong to is rejected.
v1 missing/invalid bearer: 401 authentication_required. Personal or copilot key: 403
authorization_failed (A workspace API key is required.). A file id in another workspace is
404 (Workspace file {id} was not found.).
Preview capabilities: files.list, files.get, files.read (bounded chunk, default 32 KiB, max
64 KiB), files.delete (confirmation and idempotency required). files.delete also asserts write
readiness.
Storage and scanning
Default blob store is S3 (WORKSPACE_FILE_BLOB_STORE unset or s3). postgres is allowed only
outside NODE_ENV=production. Production requires the private S3 store.
S3 profiles: minio (local), railway (non-local Confluye), aws (legacy, rejected on Confluye
deployments). Minio/railway require endpoint and credentials. Railway rejects
WORKSPACE_FILES_S3_SSE. Object key is the file id (optional WORKSPACE_FILES_S3_PREFIX).
Scanner: WORKSPACE_FILE_SCANNER clamav (default in production) or noop (non-production only).
ClamAV uses CLAMAV_HOST (default 127.0.0.1), CLAMAV_PORT (3310), CLAMAV_TIMEOUT_MS (30s).
Scan lease default is 5 minutes; a stale running scan is marked Scanner lease expired. before
a new claim. Infected → quarantined. Scanner exception → scan row error and the HTTP complete
fails.
encryptionKeyId is stored as null on create. Serialized file objects omit storage keys.
Download
GET .../{id}/content returns the raw stream (not the JSON envelope): 200 or 206 with
accept-ranges: bytes, content-type from metadata, content-disposition attachment (quotes
and newlines in the name become _), etag "sha256-{hex}". Range: bytes={start}-{end} only
(one explicit range). An end beyond the file length is limited to the last byte; an
out-of-bounds start returns 416. Non-ready files: 423 conflict.
Deletion and GC
DELETE .../{id} is idempotent at the service layer: already deletion_requested or deleted
returns the current row. HTTP 202. Blobs are not removed in that request.
GC (runWorkspaceFileGc): orphans older than 7 days with no live reference (lease null or
unexpired) in pending / pending_scan / ready / quarantined are marked
deletion_requested. Rows already deletion_requested for 30 days have the blob deleted
then status deleted. runDueWorkspaceFileGc runs only when the deployment environment is
production, at most once per UTC day after a complete batch (default batch 100).
Active references (run, message, document, skill, mail, artifact, research_source)
block GC mark/remove. Command job attachments use resourceType: "run" with a default 24h
lease.
Workflow file references
Form Trigger, Webhook and manual Start inputs store new bytes in Workspace Files. Downstream items
carry kind: "workspace-file", fileId, filename, MIME type, extension, byte size and SHA-256 in
binary. They contain no storage keys or permanent download URLs. Pending and quarantined files cannot
reach a consumer. Extraction and HTTP request bodies resolve references with the execution's workspace
authorization; the Binary inspector downloads through the signed-in session.
Accepted submissions, workflow runs and pinned outputs retain their files without a fixed 24-hour lease. References transfer before submission ownership is released. Removing the owner releases its references; existing deletion and GC retention still apply. Removing an editor selection only clears that selection. Manual simulations acquire temporary ownership before reading, then atomically transfer it to the saved run. A deleted or inaccessible referenced file causes an explicit execution error. CSV extraction rejects malformed quotes and inconsistent column counts.
See the workflow file walkthrough.
Idempotency and retries
Mutations require header Idempotency-Key (withApiIdempotency, required: true). Missing or
invalid key: 400 idempotency_key_invalid. Same key, different body (including file id on
complete/content/delete): 409 idempotency_replay_mismatch. Successful replay sets
Idempotency-Replayed: true and does not re-execute.
Blob put accepts the same key for content uploads. Complete of an already pending_scan or
ready file is a no-op unless completion metadata conflicts (409 size/checksum mismatch).
Degradation
| Condition | Behavior |
|---|---|
| Preview/production primary S3 or ClamAV probe fails | 503; retryable true for files_primary_store_unreachable / files_scanner_unreachable |
| Config missing (S3 parse fail or scanner not ClamAV+host) | Availability blocked (or setup_required when local) |
| Production backup not ready or restore not approved | Capability blocked (files_backup_store_not_configured / files_restore_test_required) |
| Infected scan | quarantined; open content 423 |
| Active scan lease | Cannot scan a file in … / already has an active scan (409) |
| Content before persist | Cannot complete … Upload content has not been persisted. (409) |
