Parsware
Tous les articles

Parsware Platform

Files from a script

A server-side script can write a file to storage and read it back, and it can put a file into one record's File field. They're two different places for two different kinds of file. We exported expense claims as a CSV into a bucket, then attached a receipt to one claim and found it on the claim's form.

Cet article n’est disponible qu’en anglais.

The Attach a receipt to a claim script in the BS editor, calling putRecordFile with the claim, the Receipt field and the file the caller sent, next to the title "Files from a script"

A plugin step or a Custom API can work with files. There are two kinds, and they go in two different places:

  • A file that belongs to no record: an export, a file another system drops off. It goes into storage, in a bucket, under a key.
  • A file that belongs to one record: a receipt, a signed contract, a photo. It goes into that record's File or Image field.

We'll do one of each, with the sample solution in samples/solutions/custom-api-object-storage-export (it imports as Object Storage Export). We put it in our Test environment, because it has its own Expense claim table.

The claims

The sample has an Expense claim table with a reference, a claimant, an amount and a date. We added three claims in its app:

The Expense claims list in the Platform Shell: EXP-201, Dana Hughes, 84.5; EXP-202, Sam Taylor, 312; EXP-203, Robin Park, 46.2

The rest of the sample is five Custom APIs:

The Custom APIs of the sample: Attach a receipt to a claim, POST; Export claims to storage, POST; Fetch one export, GET; Read a claim's receipt, GET; List the exports, GET. All under /api/par/v1/claims.

Write a file to storage

Export claims to storage reads every claim, builds a CSV and stores it:

The Export claims to storage script: runQuery reads the claims, a loop adds one line per claim to a csv string, then putFileAs writes it to the par-exports bucket as text/csv

The line that writes it:

var written = putFileAs('par-exports', key, csv, 'text/csv');

A script's file is text it built. BS Lang has no bytes, so a script can write a CSV, some JSON or a letter, but not an image or a PDF.

Use putFileAs rather than putFile for anything somebody will download. putFile stores text/plain, and a browser shows that in the tab instead of saving it.

You don't have to create the bucket first. The first write creates it.

We called the API for September:

POST /api/par/v1/claims/export/2026-09   →  { "bucket": "par-exports", "key": "claims/2026-09.csv", "rows": 3 }

And the file was in Storage in the admin area, with its size and type:

The Storage screen for the par-exports bucket: one file, claims/2026-09.csv, 103 B, text/csv

Read it back, and list what's there

Fetch one export reads the file with getFile, and List the exports uses listFiles:

GET /api/par/v1/claims/exports/2026-09   →  reference,claimant,amount
                                            EXP-201,Dana Hughes,84.50
                                            EXP-202,Sam Taylor,312.00
                                            EXP-203,Robin Park,46.20

GET /api/par/v1/claims/exports           →  { "keys": "claims/2026-09.csv;", "count": 1 }

A key with nothing at it isn't an error. getFile answers an empty string, so a script can ask "is last month's export there?" and decide. This one refuses in its own words:

GET /api/par/v1/claims/exports/2026-08   →  400  No export has been written for that period yet.

The storage functions:

Function What it does
putFile(bucket, key, text) Writes text as text/plain
putFileAs(bucket, key, text, type) Writes text with the type you give
getFile(bucket, key) Reads it back, or "" if there's nothing there
fileExists(bucket, key) Whether there's a file at that key
listFiles(bucket, prefix) The files under a prefix, with key, size, type and date (up to 200)
deleteFile(bucket, key) Deletes it

Attach a file to a record

A receipt isn't an export. It belongs to one claim, so it goes in the claim's Receipt field, not in a bucket. Then it's read with the claim, deleted with the claim, and only someone who can read the claim can read it.

Attach a receipt to a claim finds the claim by its reference and puts the file there:

The Attach a receipt to a claim script: it looks up the claim with runQuery, refuses when there is none, then calls putRecordFile with par_claim, the claim's id, par_receipt, and the file name, type and base64 content from the request

putRecordFile('par_claim', found[0].id, 'par_receipt',
  receipt.fileName, receipt.contentType, receipt.contentBase64);

The script can't build an image, so it doesn't. The caller sends the file as base64 in the request, and putRecordFile decodes it on the platform's side. The script only carries it.

We sent a photo of a receipt for EXP-201:

POST /api/par/v1/claims/EXP-201/receipt
{ "receipt": { "fileName": "receipt-exp-201.png", "contentType": "image/png", "contentBase64": "iVBORw0…" } }

Then we opened the claim in the app:

The EXP-201 claim form: the claim's details on the left, and a Receipt section on the right with receipt-exp-201.png, 7.4 KB, and Replace, Download and Remove buttons

The receipt is on the claim, with Download beside it, exactly as if someone had uploaded it on the form.

The record's own rules still apply. putRecordFile needs the same right to change the claim that the form would, and the field's size limit is the platform's. Read a claim's receipt uses getRecordFile to answer the file's name, type and size:

GET /api/par/v1/claims/EXP-201/receipt   →  { "fileName": "receipt-exp-201.png", "contentType": "image/png", "sizeBytes": 7592 }

A reference that isn't a claim is refused in the script's words:

POST /api/par/v1/claims/EXP-999/receipt  →  400  There is no claim with that reference.

As whoever is calling

Every one of these runs as the caller. A script can't write to a bucket, or to a record, that the person or app calling the API couldn't write to themselves. An API is never a way around permissions.

Three things we fixed

We found three problems writing this article, all fixed in the same change:

  • listFiles failed. The storage service had started answering its lists a page at a time, and the script side still expected a plain list, so every listFiles stopped with a server error. It now reads the page.
  • A script's refusal was reworded. "There is no claim with that reference." reached the caller as "The request does not match this API's contract: There is no claim with that reference..", blaming a request that was fine. A refusal now arrives exactly as the script wrote it, which matters most on a portal, where a visitor reads it.
  • The editor underlined working code. It didn't know the file functions at all, so every putFileAs and putRecordFile was marked Function not found, and receipt.fileName was marked wrong too. Both are fixed, and there's now a test that fails if the server gains a function the editor doesn't know about.

Where it works

The file functions are available in Custom APIs and plugin steps, which run on the server. The storage ones work in scheduled jobs too. They aren't in form rules or command buttons, which run in the browser.

Try it

Import the sample, add a few claims, and call the export for this month. Then find the file under Storage. Attach a small image to one claim and open the claim.

Next: one script, two interpreters, and how we keep the browser and the server agreeing about BS Lang.