Code
In the admin: Workflows › a Workflow › a step
Three steps run code of your own: a PostgreSQL function, a piece of JavaScript, or a SQL statement. Prefer them in that order. A function keeps the logic in PostgreSQL, under permissions and audit; JavaScript transforms data without touching anything; SQL bypasses permissions altogether.
| Step | Runs | Permissions | Result, as {{step.data}} |
|---|---|---|---|
| Execute function | a PostgreSQL function exposed as a resource | the executor's EXECUTE | its return value |
| Execute JavaScript | JavaScript in an isolated sandbox | none needed: it can't reach any data | what the code returns |
| Execute SQL | a raw SQL statement | none checked | the rows returned |
Execute function
| Field | |
|---|---|
| Function | Required. An ACTIVE function resource the executor may call |
| Arguments | A JSON object: argument names and their values, with {{…}} |
It's the same call as POST /api/{schema}/function/{name} in the Data API: arguments by name, in one transaction, with an audit entry. The executor needs EXECUTE on the function, otherwise the step fails with FUNCTION_EXECUTION_DENIED. A function that raises an exception fails the step with its message. See Calling functions.
The result is the value for a function returning one value, or the rows for a set-returning function.
Acme's Order completed creates the invoice this way:
| Field | Value |
|---|---|
| Function | create_invoice_from_work_order |
| Arguments | {"work_order": "{{start.data.id}}"} |
The new invoice's id is then {{create_invoice.data}}.
A function call never starts Database event workflows, including through the rows the function writes.
Execute JavaScript
| Field | |
|---|---|
| Code | The body of a function that receives trigger and results, and returns plain JSON |
triggeris the run's input, the same value as{{start.data}}.resultsholds every step that has run, by Step ID, each as its result envelope:results.read_orders.data,results.start.data.
// The selected customers' ids, without duplicates - a filter's _in takes the list as it is
return { ids: [...new Set(trigger.ids)] }
// A one-line summary per overdue order for the SLA email
return results.read_overdue.data.map(o => `${o.number}: due ${o.due_at}`).join("\n")
The sandbox
The code runs in its own V8 isolate (isolated-vm), separate from TotalBase's:
- no
require, noprocess, no file system, no network, no timers, no database; - 5 seconds at most per step;
- 32 MiB of memory, for all the JavaScript steps of a run;
- each step starts with a fresh context: nothing one step defines is visible to the next.
What the code returns is converted to JSON inside the sandbox, so only plain data leaves it: functions and class instances are dropped, and a circular value fails the step. An exception, a timeout or running out of memory fails the step with INTERNAL_ERROR and the error's message.
The code is taken literally: {{…}} isn't replaced inside it. Read values through trigger and results instead.
For HTTP calls, records, emails and files, use their own steps: JavaScript is for reshaping data between them.
Execute SQL
| Field | |
|---|---|
| Execute as | The user the statement is recorded under, in the SQL Console's history. Empty, the run's own user: its trigger's Execute as, or who ran it |
| Statement | The SQL to run |
It exists because a workflow is built by an administrator, who could run the same SQL in the SQL Console. Use it for what the other steps can't express, such as a maintenance statement on a schedule:
DELETE FROM vehicle_positions WHERE recorded_at < now() - interval '90 days'
The statement is taken literally: {{…}} isn't replaced inside it, so it can't take values from the trigger or earlier steps. For work that depends on them, write a PostgreSQL function and call it with Execute function, where the values are passed as arguments and permissions apply.
The result is the rows the statement returned, if any.