Running Custom Javascript
Running Custom JavaScript in a Workflow
The Run Custom Code action lets you write your own JavaScript inside a workflow. It's the most flexible action in Tadabase — calculate things, format text, transform data, call an external API, generate an ID, or branch on logic too complex for a Condition step.
Your code runs in a secure sandbox — it cannot read files on the server, run shell commands, or access the database directly. Within the sandbox you have a curated set of helpers and globals that cover almost everything app developers need.
When to use Custom Code (and when not to)
| Good fit | Use this instead |
|---|---|
| Math beyond simple arithmetic (rounding, percentages, weighted averages). | Equation/Formula fields for record-level math you need everywhere. |
| Date manipulation (add days, find next business day, compare timezones). | For simple "current date/time" stamps, the standard value picker is enough. |
| Calling an external API that doesn't have a Pipe or built-in connector. | If a Pipe or connector exists, use that — easier and reusable. |
| Transforming text (slugify, truncate, format names). | For display-only formatting, consider a Concat field on the record. |
| Looping over an array of items and aggregating. | For per-record looping, the workflow trigger already loops — you may not need code. |
Anatomy of a Custom Code step
- Inputs — values you declare in the left panel. Each input has a name and a source (a record field, a previous step's output, the logged-in user, an app variable, or a custom value).
- Code — the JavaScript you write in the editor.
- Outputs — anything you return to later steps. Use
returnData("name", value),setResult("name", value), orsetField("slug", value).
Data your code can read
Three top-level objects are always available:
| Variable | What's in it |
|---|---|
params | The inputs you declared in the step config. Reference them as params.your_input_name. Names must be identifier-safe (letters, numbers, underscores, no spaces). |
record | The source record's field values. Reference them as record.field_slug (use the field's slug, not its display name). |
context | App context — logged-in user, app details, app variables, and storage helpers. |
Convenience accessors are also provided:
getField("slug", default)— read a field from the source record.getLoggedInUser()orloggedInUser— the currently logged-in user.getAppVariable("slug", default)— an App Variable (best place to keep API keys).getAppDetails("key", default)— app metadata.
Getting data out
Three equivalent ways to push values into the step's output:
returnData("key", value); // most common
setResult("key", value); // exactly the same thing
setField("slug", value); // alias, reads better when you mean a field
// Bulk form:
setResult({ a: 1, b: 2, total: 99.50 });
// Or just return the object directly:
return { a: 1, b: 2 };
Anything you push out becomes available to later steps as an Action Response Value — pick this step from the value picker, then choose your output key.
Built-in helpers
Tadabase ships a curated set of helpers so you don't need to write boilerplate.
| Helper | What it covers | A few methods |
|---|---|---|
DateTime | Date and time math | now, today, format, parse, addDays, addHours, diffDays, isWeekend |
Str | String operations | capitalize, uppercase, lowercase, trim, slug, truncate, contains, replace |
Num | Numeric operations | round, floor, ceil, abs, format, random, randomInt |
Arr | Array operations | first, last, sum, avg, unique, chunk, sortBy, groupBy, pluck |
Obj | Object operations | keys, values, has, get (dot-paths), set, merge, pick, omit |
crypto | Random IDs/bytes | randomUUID(), getRandomValues(typedArray) |
fetch | HTTP requests | Standard fetch API; allowlisted hosts only. |
http | HTTP shorthand | http.get, http.post, http.put, http.patch, http.delete |
console | Logging | log, info, warn, error — appear in the run logs |
Example snippets
Compute a derived value from the record
const price = Number(record.price || 0);
const qty = Number(record.qty || 0);
const tax = 0.0875;
const subtotal = price * qty;
const total = Num.round(subtotal * (1 + tax), 2);
setResult("subtotal", subtotal);
setResult("total", total);
setResult("line", `${qty} × $${price} = $${total}`);
Later steps can reference subtotal, total, and line.
Call an external API and extract a field
// Input declared in UI: city = "New York"
const apiKey = getAppVariable("weather_api_key", "");
const r = await fetch(
`https://api.weather.example/v1/current?q=${encodeURIComponent(params.city)}`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
if (!r.ok) {
console.error("Weather API failed", r.status);
return { ok: false };
}
const data = await r.json();
returnData("temperature_f", data.temp_f);
returnData("condition", data.condition);
Generate a unique ID
const token = crypto.randomUUID();
setResult("token", token);
setResult("issued_at", DateTime.format(DateTime.now(), "YYYY-MM-DD HH:mm:ss"));
Set a branching status the next step can read
const user = getLoggedInUser();
if (!user) {
setResult("status", "anonymous");
return;
}
const days = DateTime.diffDays(user.created_at, DateTime.now());
if (days < 30) setResult("status", "new");
else if (days < 365) setResult("status", "active");
else setResult("status", "veteran");
Limits and rules
| Limit | What happens |
|---|---|
| 30 seconds wall clock | Code that takes longer fails with a timeout error. Move long-running work to a Pipe or paginate. |
| 128 MB memory | Code using more memory fails with a memory error. |
| Allowlisted HTTP only | Outbound fetch can only reach hosts an admin has allowlisted. If you see "Host not allowlisted," ask your workspace admin to add it. |
No require or import | Everything must be inline. No npm packages, no Node modules. |
No setInterval | Use recursive setTimeout if you need delays. The 30-second wall clock still applies. |
| No persistent state | Every run is fresh. To remember things between runs, write to a record, an App Variable, or client storage. |
Handling secrets and API keys
Never paste API keys or passwords directly into your code. Code is stored as part of the workflow config.
Instead, save the secret as an App Variable, then read it at runtime:
const apiKey = getAppVariable("stripe_secret_key", "");
Quick cheatsheet
| I want to… | Do this |
|---|---|
| Read a value I configured in Inputs | params.input_name |
| Read a field on the source record | record.field_slug or getField("field_slug", default) |
| Read the logged-in user | getLoggedInUser() or loggedInUser |
| Read an app variable / secret | getAppVariable("slug", default) |
| Pass a value to the next step | returnData("key", value) |
| Call an external HTTPS endpoint | const r = await fetch(url, { ... }) |
| Format a date | DateTime.format(DateTime.now(), "YYYY-MM-DD") |
| Round a number | Num.round(value, 2) |
| Sum an array of numbers | Arr.sum(numbers) |
Testing and debugging
- Use the Test Run button on the step to execute the code with sample inputs without saving the workflow.
console.logmessages appear in the step's run logs (and in the History view).- If your code throws, the workflow logs the error and stops the step. Later steps still run unless you've configured the workflow to halt on error.
We'd love to hear your feedback.