Skip to main content

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​

  1. Open your solution and select Functions in the left rail, then New.
  2. On the Script tab, write what the function does. It can read and write records, read and write files, call outside services, and import your solution scripts — the same things a plugin step can. The eye at the end of the tab strip lists every function you can call.
  3. On the Settings tab, give it a Display name. The Logical name fills itself in.
  4. 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.
  5. 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.
  6. Select Save. A script that does not parse is refused now, with the line it fails on.
  7. 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. log still 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, or null — is what the function reads with functionInput(). A run started by its schedule or by Run now has no input, and reads null.
  • 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 }, or null when 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, startFunction stops 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 callService does; 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-storage sample does.

How a run ends​

StatusWhat happened
SucceededThe script finished, or stopped itself with stop(...)
FailedThe 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
CancelledSomebody 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​

SymptomLikely causeFix
Run now is greyed outUnsaved changes, or the function is switched offSave; switch it on
That function is already runningA run is queued or runningWait for it, or select it and Cancel run
The run failed: Stopped after … minutesIt ran past its maximum runtimeRaise Maximum runtime, or have each run do less
The run failed: The Functions service stopped while this run was in progressThe service restarted during the runRun it again
The log is emptyKeep log lines is off, or the script calls log nowhereSwitch it on; add log(...)
The run failed: There is not enough storage leftThe organisation's storage allowance is used upDelete files you no longer need, or upgrade the plan
The run failed: Object storage is not configured for the Functions serviceThe deployment gave the Functions service no storage settingsAsk your administrator

See also​