File Storage
File Storage lets a deployed agent persist bytes beyond the current step or resource lifecycle. Agent steps call the typed ctx.sapiom.fileStorage capability; Sapiom supplies the authenticated cloud connection when the deployed agent runs.
The upload is a two-part operation: Sapiom reserves a file record and returns a presigned URL, then ordinary fetch sends the bytes directly to that URL.
Choose the file operation
Section titled “Choose the file operation”| What the agent needs | Recommended operation |
|---|---|
| Upload new bytes | fileStorage.upload(...), then PUT to uploadUrl |
| A short-lived URL for a private file | fileStorage.getDownloadUrl(fileId) |
| A durable link for an intentionally public file | fileStorage.getPublicUrl(fileId) |
| Change private/public access | fileStorage.setVisibility(fileId, visibility) |
| Page through stored-file metadata | fileStorage.list({ limit, offset }) |
| Remove a file | fileStorage.delete(fileId) |
Private is the default visibility. Choose public only when anyone with the URL should be able to retrieve the file.
Upload bytes
Section titled “Upload bytes”const bytes = new TextEncoder().encode(JSON.stringify(input.report));
const slot = await ctx.sapiom.fileStorage.upload({ contentType: "application/json", fileName: "report.json", fileSize: bytes.byteLength, visibility: "private",});
const uploaded = await fetch(slot.uploadUrl, { method: "PUT", headers: slot.requiredHeaders, body: bytes,});
if (!uploaded.ok) { throw new Error(`File upload failed with HTTP ${uploaded.status}`);}
return { fileId: slot.fileId, uploadUrlExpiresAt: slot.expiresAt,};fileSize is required and is a JavaScript number measured in bytes. Send the exact requiredHeaders with the PUT; the presigned URL expires at expiresAt. Do not return or store uploadUrl as the file’s durable identity—retain fileId.
The fileSize on later metadata is a string so large byte counts remain precise. The file record can move through statuses such as pending upload, uploaded, and deleted; do not assume that reserving a slot transferred any bytes.
Download or share a file
Section titled “Download or share a file”For a private file, mint a fresh short-lived URL near the time it will be used:
const { downloadUrl, expiresAt } = await ctx.sapiom.fileStorage.getDownloadUrl(input.fileId);
return { downloadUrl, expiresAt };For an intentionally public file, getPublicUrl constructs a durable permalink synchronously:
await ctx.sapiom.fileStorage.setVisibility(input.fileId, "public");const publicUrl = ctx.sapiom.fileStorage.getPublicUrl(input.fileId);
return { publicUrl };The public permalink resolves only while the file remains public and undeleted. Changing visibility is an access-control decision; do not make a file public merely to avoid refreshing a private download URL.
Test the behavior locally
Section titled “Test the behavior locally”Local Run stubs ctx.sapiom.fileStorage methods, but the ordinary fetch(slot.uploadUrl, ...) in the example remains real. The built-in upload URL is fixture data and does not receive bytes.
Use an injectable byte-transfer adapter for local tests, or stop the tested branch after slot reservation. Override the exact capability result the step consumes:
{ "version": 1, "steps": { "store-report": { "fileStorage.upload": { "fileId": "file-report-local", "uploadUrl": "https://storage.invalid/upload/file-report-local", "expiresAt": "2099-01-01T00:00:00.000Z", "requiredHeaders": { "content-type": "application/json" } } } }}Other exact paths include fileStorage.getDownloadUrl, fileStorage.getPublicUrl, fileStorage.list, fileStorage.setVisibility, and fileStorage.delete. After the run, assert the terminal output and require both unusedStubs and stubWarnings to be empty. Use a small production run to verify the real PUT, visibility, download, and deletion lifecycle.
Manage retention and usage
Section titled “Manage retention and usage”Delete files that are no longer needed, page through list() rather than assuming an unbounded response, and use setVisibility to narrow access when public sharing ends. File deletion is idempotent from the typed client.
Storage, transfer, and download behavior can have separate limits. Use the signed-in capability catalog for current availability and pricing, and inspect file metadata and the production run when a transfer fails.
© 2026 Sapiom, Inc.