Skip to main content

Write a file to storage from a script

Goal​

Have a Custom API build a file — a CSV export, a JSON drop for another system — and put it somewhere an outside system can collect it.

Why this exists​

The platform has three places a file can live, and they are not interchangeable.

ForExample
Web resourcesFiles that are part of the applicationA logo, a stylesheet
Record filesA file belonging to one recordA contact's photo, a signed contract
Object storageFiles belonging to no recordA monthly export, an integration drop

A record's file is attached to a record and travels with it. An export belongs to nobody's record — there is no row to hang it off — and that is what object storage is for.

Before you start​

You need a bucket name. You do not need to create it: the first write creates it.

Bucket names are lowercase letters, digits and hyphens, 3–63 characters. They are shared across all tenants, the way S3's are, so a plain name like exports may already be taken — prefix it with something of yours, as par-exports does below.

What a script can do​

putFile(bucket, key, text)Writes a file. Answers true, or false if it could not
putFileAs(bucket, key, text, contentType)The same, saying what kind of file it is
getFile(bucket, key)Reads it back as text — the empty string if there is none
fileExists(bucket, key)true or false
listFiles(bucket, prefix)The files under a prefix, each with key, size, contentType and uploadedAt
deleteFile(bucket, key)Removes one

The key is the file's path inside the bucket — claims/2026-09.csv. Slashes in it are just part of the name, but they read as folders in the storage screen, so grouping by one is worth doing.

Steps​

  1. Open your solution and choose Custom APIs, then New.

  2. Give it a route — claims/export/{period} — and choose Post.

  3. In the script, build the file as text and write it:

    var claims = runQuery({ entity: 'par_claim', select: ['par_name', 'par_amount'] });

    var csv: string = 'reference,amount\n';
    for (var i: int = 0; i < claims.length(); i++) {
    csv = csv + `${claims[i].par_name},${claims[i].par_amount}` + '\n';
    }

    putFileAs('par-exports', 'claims/' + getInput('period') + '.csv', csv, 'text/csv');
  4. Save, and call it.

Three things that catch people out​

  • length() needs its parentheses. claims.length is not an error — it is nothing, so a loop guarded by it never runs and your export comes out as a header row and no rows. This is the one mistake here that does not announce itself.
  • You cannot add a number to a string. 'Total: ' + amount is refused. Put the number in a backtick template instead — `Total: ${amount}` — which is the only way to turn one into text.
  • Say what kind of file it is. putFile stores it as plain text, and a browser shows plain text in the tab instead of saving it. Use putFileAs with text/csv, application/json or whatever it really is.

Confirm it worked​

Go to Admin → Storage, open the bucket, and the file is there with its size and type. Download it and check the contents.

What this is not for​

  • Big binary files. A script's file is text it built. Images, PDFs and archives are handled by the platform itself, which streams them.
  • A file that belongs to a record. Put a File or Image column on the table instead — it is served under the record's own permissions, and deleting the record takes the file with it.
  • Giving somebody a link. These functions hand your script the text; they do not mint a URL. A link that works without signing in is a key anybody can pass on, so it is a deliberate step elsewhere rather than something a script does in passing.

Troubleshooting​

"Bucket was not found." Reading or listing a bucket that does not exist yet — only writing creates one. Write first, or check the spelling.

Nothing in the file but the header. length without the parentheses. See above.

"Only numbers or strings are allowed in this expression." You added a number to a string. Use a backtick template.

"This run has no caller." The script is an asynchronous plugin step. Those run after the request that queued them has finished, so there is nobody to act as. Use a synchronous step, or a Custom API.

See also​

  • Publish a Custom API
  • The custom-api-object-storage-export sample solution, which is this page as something you can import and run.