Attach a file to a record from a script
Goal
Let an outside caller — a portal, a partner's system — attach a file to a record through a Custom API, so that your script decides which record it lands on and whether the caller may.
Why this exists
A record's File and Image columns already have endpoints of their own, and a form uses
them. A portal cannot: a portal talks to the platform through Custom APIs and nothing else,
because the check that keeps one customer away from another's records has to sit in a script
beside portalCustomerId(), not in a website's request builder.
So a script can now put a file into a record's field and read one back. The bytes travel as base64 text, because a script has no other way to hold them — the caller encodes, the platform decodes, and the script only passes the string through.
What a script can do
putRecordFile(table, id, field, fileName, contentType, contentBase64) | Stores the file in the record's field. Answers true, or false if it could not |
getRecordFile(table, id, field) | The file as an object — fileName, contentType, sizeBytes, contentBase64 — or nothing if the field is empty |
Both act as the caller: storing needs Update on the record and reading needs Read, exactly as the record's own file endpoints do. The field's size cap and the image-only rule are the platform's, not the script's.
Steps
-
Give the table a File column —
par_attachment— and, if a list should show the name without fetching the file, a Text column beside it for the name. -
In the Custom API's request schema, take the file as three properties:
"attachmentName": { "type": "string", "maxLength": 260 },"attachmentType": { "type": "string", "maxLength": 200 },"attachmentBase64": { "type": "string", "maxLength": 5600000 }Or, since portals round 10, as one object — which is what a drawn screen’s file picker sends, and the shape
getRecordFileanswers:"par_document": {"type": "object","additionalProperties": false,"required": ["fileName", "contentType", "contentBase64"],"properties": {"fileName": { "type": "string", "maxLength": 260 },"contentType": { "type": "string", "maxLength": 200 },"sizeBytes": { "type": "integer" },"contentBase64": { "type": "string", "maxLength": 5600000 }}}getInput('par_document')then hands the script the object whole, andfile.fileNamereads a member of it. If the caller is a Parsware portal screen, use this shape — see Let visitors attach a file. -
In the script, create the record first, then the file — a record file belongs to a record:
var id = createRecord('par_request', {par_name: getInput('subject'),par_attachment_name: getInput('attachmentName')});if (!isEmpty(getInput('attachmentBase64'))) {putRecordFile('par_request', id, 'par_attachment',getInput('attachmentName'), getInput('attachmentType'), getInput('attachmentBase64'));} -
To hand a file back, check the record is the caller's first, then read it:
var file = getRecordFile('par_request', id, 'par_attachment');if (isEmpty(file)) {setStatus(404);} else {setOutput('fileName', file.fileName);setOutput('contentType', file.contentType);setOutput('contentBase64', file.contentBase64);}
Three things that catch people out
- Check ownership before you read.
getRecordFileruns as the calling application, which may well be allowed to read every record in the table. The question "is this record this caller's?" is your script's to ask, and it is asked by reading the record and comparing. - The name on the row is a snapshot. The file row keeps the real name; a Text column beside it lets a list say "📎 report.pdf" without a file call per row. Write it when you store the file, and it stays right until somebody replaces the file through another door.
- Big files do not come through a script. A file over the field's cap is stored in object
storage and streamed by the record's endpoint;
getRecordFilerefuses it rather than materialising hundreds of megabytes inside an interpreter.
Confirm it worked
Open the record in its app: the File column shows the file, and downloading it gives back what the caller sent. The audit history records the change with the file's name and hash.
Troubleshooting
"expects the file's content as base64 text; what was passed does not decode." The caller
sent the raw bytes, or a data URL with its data:…;base64, prefix still on. Send the base64
alone.
"The file is N bytes; the limit for this field is M bytes." Over the cap with no object storage configured to spill into. Ask the caller for something smaller, or configure a spill bucket.
"cannot carry a N-byte file; a script reads files up to M bytes." The file was spilled to object storage. Serve it through the record's file endpoint, which streams.