Run a function
Goal
Have something happen without anybody saving a record: a reminder sent every night, a batch of records tidied up once an hour, a long piece of work started by hand and left to finish. And be able to read, afterwards, exactly what each run did.
Why this exists
Everything else a solution's scripts do starts with somebody's Save — a plugin step runs inside a write, a Custom API runs when another system calls it. Some work is not about one write: it is about a time, or about a batch of records too large to handle while somebody waits.
A function is a BS Lang script that the Functions service runs on its own, in its own process, for as long as you allow it. It is a component of your solution, like a form or a script, so it is written in Dev and travels to Test and Production in the solution package.
Before you start
- A solution, with a publisher.
- The tables your function will read and write.
- If it calls an outside service or sends email, the environment variable that names it — see Call an outside service from a script.
Steps
- Open your solution and select Functions in the left rail, then New.
- On the Script tab, write what the function does. It can read and write records, read and write
files, call outside services, and
importyour solution scripts — the same things a plugin step can. The eye at the end of the tab strip lists every function you can call. - On the Settings tab, give it a Display name. The Logical name fills itself in.
- Decide when it runs. Every function can be run by hand. To have it also run by itself, tick Runs on a schedule and choose an interval — every 15 minutes, every 6 hours, every day. For a daily schedule you can pin a time of day and the time zone it is read in.
- Set the Maximum runtime — how long a run may go on before it is stopped. It defaults to 15 minutes and can be up to a day.
- Select Save. A script that does not parse is refused now, with the line it fails on.
- Select Run now. The Runs tab opens and follows the run: Queued, then Running, then how it ended. Select the run to read its log.
Writing a log
Nobody is watching while a function runs, so its log is how you find out what it did.
var due = queryRecords('par_membership', 'par_reminder_state', 'due');
log('Reminders due: ' + toJson(due.length()));
for (var i: int = 0; i < due.length(); i++) {
updateRecord('par_membership', due[i].id, { par_reminder_state: 'sent' });
log(due[i]);
}
- Text is written as it is, a number as you would write it, and a record or a list as
JSON — so
log(due[i])shows the whole record as the run saw it. - The Runs tab shows the lines while the run is still going, a few seconds behind.
- One run keeps at most 5,000 lines. The last one kept says the log is full.
- Keep log lines on the Settings tab switches logging off.
logstill works and keeps nothing, so you never have to edit the script to silence it — or to turn it back on.
Ending a run on purpose
stop('reason') ends the run there, as a success that says why:
if (due.length() == 0) {
stop('Nobody is due a reminder tonight.');
}
The run's Outcome reads Nobody is due a reminder tonight. That is different from a failure, and the run list shows it that way.
Starting a function from a script
A function does not need a clock. A Custom API, a plugin step or another function can start
one with startFunction, hand it an input, and get the run's id back at once — the work
happens afterwards, in the Functions service. This is how a request that needs an hour of work is
answered in a moment.
// A Custom API: POST reports { "period": "2026-10" }
var run = startFunction('par_sales_report', { period: getInput('period') });
setStatus(202);
setOutput('run', run);
// The function
var asked = functionInput();
// … the long work …
setResult({ period: asked.period, total: total });
// A second Custom API: GET reports/runs/{run}
var run = functionRun(getInput('run'));
if (run == null) { setStatus(404); } else { setOutput('status', run.status); setOutput('report', run.result); }
startFunction(name, input)answers the run's id. The input — a record, a list, a value, ornull— is what the function reads withfunctionInput(). A run started by its schedule or by Run now has no input, and readsnull.setResult(value)is what the run hands back. Only the last value is kept, and only if the run succeeds. Keep it small — a summary, or the key of a file — because it is read every time somebody asks how the run is doing.functionRun(id)answers{ id, function, status, outcome, result, queuedAt, startedAt, finishedAt }, ornullwhen there is no such run.- From a plugin step, the run is started inside the write: if the write is refused, the run never starts.
- Runs started by a script are not held to one at a time. Two people asking at once get two runs. The one-at-a-time rule is for the schedule and Run now.
- In the Runs tab, such a run reads How it started: A script, and selecting it shows its Input and Result above the log.
- If the function does not exist or is switched off,
startFunctionstops the script with the reason, so the caller is never told that work started when it did not.
Being told when it ends
A caller that would rather not keep asking can be told. startFunctionWithCallback is
startFunction with one more argument: the name of an environment variable that holds the
address to notify. When the run ends — however it ends — its outcome is posted there once.
// A Custom API: POST totals
var run = startFunctionWithCallback('par_invoice_totals', null, 'par_totals_done');
setStatus(202);
setOutput('run', run);
What arrives:
{ "run": "…", "function": "par_invoice_totals", "status": "Succeeded",
"outcome": null, "result": { "invoices": 3, "total": 165.5 }, "finishedAt": "…" }
- Never a URL in the script. The third argument names an environment variable, the same way
callServicedoes; an administrator decides where it points, per environment. A name the solution does not declare stops the script before anything starts. - Sent once, and not retried. In the Runs tab, selecting the run shows its Callback and
what came of it — Delivered (HTTP 200), the status the service answered, or why it could not be
called. A caller that missed it can still read the run with
functionRun. - The run's own status does not depend on it. A run that did its work succeeds even if the service it was meant to tell is down.
Writing a file
A function can write to object storage with the same calls a Custom API uses — putFile,
putFileAs, getFile, listFiles and deleteFile — even though nobody is signed in when it
runs.
var key = 'orders/' + today() + '.csv';
putFileAs('par-order-exports', key, csv, 'text/csv');
setResult({ bucket: 'par-order-exports', key: key, rows: orders.length() });
- The file goes into your organisation's storage and counts against its storage allowance. A write that would go past the allowance fails the run.
- In Admin → Storage the file reads as uploaded by
function:<name>— the function wrote it, not whoever pressed Run now. - This is how a function hands back more than a summary: write the data to a file and put the file's key in the result.
- Nothing deletes old files for you. A function that writes one a night should delete the ones it no
longer needs, as the
functions-nightly-export-to-storagesample does.
How a run ends
| Status | What happened |
|---|---|
| Succeeded | The script finished, or stopped itself with stop(...) |
| Failed | The script threw, a write it made was refused, it ran past its maximum runtime, or the service stopped while it ran. The Outcome says which |
| Cancelled | Somebody pressed Cancel run |
A failed run is never retried for you. A function may run for a long time, and repeating twenty-nine minutes of work behind your back would be worse than stopping. Read why it failed, fix what needs fixing, and press Run now.
Confirm it worked
- The run says Succeeded, and its log says what you expected.
- Open a record the function changed and look at its audit history: the change is recorded as made by the function, not by a person. A function runs as the platform, with nobody behind it.
- For a scheduled function, Next run on the Settings tab is the next time it will run by itself.
Things to know
- One run at a time. While a run is queued or running, Run now is refused, and a schedule that comes due is skipped until the next time. A nightly pass that ran long is never started twice over the same records.
- Run now runs what is saved. It is not offered while the page holds unsaved changes.
- Switched off is off. A function that is switched off does not run on its schedule or by hand.
- Changing the script does not move the schedule; changing the schedule does. Fixing a typo in the afternoon does not change tonight's run.
- Cancel stops a running script within a few seconds — even a loop that touches no record.
- A function keeps its 200 most recent runs.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Run now is greyed out | Unsaved changes, or the function is switched off | Save; switch it on |
| That function is already running | A run is queued or running | Wait for it, or select it and Cancel run |
| The run failed: Stopped after … minutes | It ran past its maximum runtime | Raise Maximum runtime, or have each run do less |
| The run failed: The Functions service stopped while this run was in progress | The service restarted during the run | Run it again |
| The log is empty | Keep log lines is off, or the script calls log nowhere | Switch it on; add log(...) |
| The run failed: There is not enough storage left | The organisation's storage allowance is used up | Delete files you no longer need, or upgrade the plan |
| The run failed: Object storage is not configured for the Functions service | The deployment gave the Functions service no storage settings | Ask your administrator |