Skip to content
Go To Dashboard

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.

What the agent needsRecommended operation
Upload new bytesfileStorage.upload(...), then PUT to uploadUrl
A short-lived URL for a private filefileStorage.getDownloadUrl(fileId)
A durable link for an intentionally public filefileStorage.getPublicUrl(fileId)
Change private/public accessfileStorage.setVisibility(fileId, visibility)
Page through stored-file metadatafileStorage.list({ limit, offset })
Remove a filefileStorage.delete(fileId)

Private is the default visibility. Choose public only when anyone with the URL should be able to retrieve the file.

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.

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.

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.

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.