Getting Started Snippets
Custom Code Snippets — Getting Started
This is the first article in the Custom Code Snippets library — a set of copy-paste-ready JavaScript examples for the Run Custom Code workflow action. Each snippet is short, real, and uses only what the sandbox actually exposes. Pick the one closest to what you need, paste it into the Code editor, and tweak.
If you have not read it yet, the Running Custom JavaScript in a Workflow article is the full reference for how the sandbox works (globals, helpers, limits, HTTP rules). This snippets library is the cookbook that sits on top of that reference.
The full snippet library
Each of these articles is a topic-focused page of copy-pasteable examples:
- Custom Code Snippets — Getting Started (this article) — the starter template, what every script can read, and the most common pitfalls.
- Custom Code Snippets — Strings & Names — building names, slugs, titles, templated emails.
- Custom Code Snippets — Numbers, Math & Money — totals, tax, currency formatting, percent change.
- Custom Code Snippets — Dates & Times — formatting dates, business days, "days until," buckets.
- Custom Code Snippets — Arrays & Lists — summing, grouping, deduping, picking the latest item.
- Custom Code Snippets — Validation & Sanitization — emails, phone numbers, required-field checks, allowlists.
- Custom Code Snippets — Branching & Status — tiering, lifecycle status, coalescing values.
- Custom Code Snippets — Identifiers & Tokens — UUIDs, short codes, idempotency keys, signed payloads.
- Custom Code Snippets — External APIs — calling Slack, retrying, building URLs, forwarding webhooks.
- Custom Code Snippets — Connected Records — averaging, picking the most recent, building lists.
- Custom Code Snippets — Logged-in User & App Context — role gates, app variables, audit stamps.
- Custom Code Snippets — Client-Side UX — toasts, modals, redirects, clipboard.
Quick refresher — what every script can see
| Variable | What's in it | How to read it |
|---|---|---|
params |
The named inputs you configured on the step (Custom Value, Record Field, Logged-in User, App Variable, previous step output, etc.). | params["email"] — always bracket notation with a quoted key. |
record |
The current record's fields, when the workflow is record-scoped. IDs are Hashids-encoded. | record.first_name — use the field's slug, not its display name. |
loggedInUser |
The signed-in user, if any. getLoggedInUser("field", default) is the safe accessor. |
loggedInUser.email or getLoggedInUser("email", "") |
appVariables |
App-level variables (great for storing API keys and config). getAppVariable("slug", default) is the safe accessor. |
getAppVariable("stripe_key", "") |
appDetails |
App metadata (name, slug, base URL, …). getAppDetails("key", default) is the safe accessor. |
getAppDetails("name", "Our App") |
Helpers always present in the sandbox: Str, Num, Arr, Obj, DateTime, plus fetch, http.get/post/put/patch/delete, crypto.randomUUID, crypto.getRandomValues, console.log/info/warn/error, setTimeout, URL, URLSearchParams, TextEncoder, TextDecoder, atob, btoa.
Getting values out of your code
Three equivalent APIs — all write into the same internal output:
returnData("key", value); // most common
setResult("key", value); // exactly the same thing
setField("slug", value); // alias, reads naturally when targeting a record 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.
Starter template
Drop this into a new step to see how the pieces fit together. It echoes one input and reads one record field.
// Inputs you configured (left rail) are on `params`.
// Current record fields are on `record`.
// Use returnData("key", value) to pass values to the next step.
const greeting = `Hello, ${params["name"] || record.first_name || "friend"}!`;
console.log("Built greeting:", greeting);
returnData("greeting", greeting);
Configure one input named name (Custom Value or Record Field). Click Test Run to see the output and the console.log line in the run log.
Hello world — read one input, return one value
// Input: who (custom_val, e.g. "world")
const who = params["who"] || "world";
returnData("message", `Hello, ${who}!`);
Returning multiple values at once
// Single call — easier to read for many keys
setResult({
ok: true,
greeting: `Hi, ${params["name"] || "there"}`,
timestamp: DateTime.format(DateTime.now(), "YYYY-MM-DD HH:mm:ss"),
source: "custom-code-step"
});
Early return — fail fast on bad input
const email = params["email"];
if (!email) {
returnData("ok", false);
returnData("error", "email is required");
return; // stops the script — later returnData calls never run
}
returnData("ok", true);
returnData("email", email.toLowerCase().trim());
Logging while you build
console.log messages show up in the step's run log and in the workflow History view. Use them while you build, then remove them once the script is stable.
console.log("record.id =", record.id);
console.log("params keys =", Object.keys(params));
console.warn("about to call external API");
console.error("something went wrong"); // still continues unless you throw
Common pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
params["foo"] is undefined |
Input name typo, or input not configured. | Add an input named foo in the left rail. Names must match [A-Za-z_$][A-Za-z0-9_$]* — no spaces or leading digits. |
Wrote params.foo and got nothing back |
Some sources resolve as bracket-only. | Always read inputs as params["foo"]. It's the consistent, supported pattern. |
record is {} |
The workflow isn't record-scoped, or the trigger doesn't carry a record. | Pass the values you need explicitly via params instead. |
Host not allowlisted from fetch |
The domain isn't on SANDBOX_HTTP_ALLOWLIST. |
Ask your workspace admin to add the host. |
fetch returns "method not allowed" |
Used a method other than GET/POST/PUT/PATCH/DELETE. | Use one of the allowed methods. |
| Step times out at 30 s | Long-running loop or slow API. | Paginate, move heavy work to a Pipe or scheduled workflow, or to your own API. |
| Output shows nothing in the next step | Forgot to push a value out. | Call returnData("key", value) (or setResult), or return { ... } from the top of the script. |
JSON.stringify error / serialization fail |
The returned value contains a function or a circular reference. | Build a plain object first; only return JSON-safe values. |
setInterval is not defined |
Not exposed in the sandbox. | Use a recursive setTimeout chain — and remember the 30-second wall clock. |
require is not defined |
No Node modules in the sandbox. | Inline the logic, or call an external service via fetch. |
| Secret leaks in logs | Hard-coded API key or console.log(secret). |
Read secrets via getAppVariable("..."); don't log them. |
Naming rules for inputs (don't get caught by this)
Each input name must match the regex /^[A-Za-z_$][A-Za-z0-9_$]*$/. The PHP-side resolver silently drops any input whose name doesn't match.
| Name | OK? | Why |
|---|---|---|
email |
Yes | Letters only. |
customer_email |
Yes | Letters and underscore. |
email1 |
Yes | Digits allowed after the first character. |
1st_email |
No | Cannot start with a digit. |
customer email |
No | No spaces. |
customer-email |
No | No hyphens. |
ID encoding — what's special about IDs
Any key on a record object that equals id, record_id, or ends with _id is Hashids-encoded before it reaches the sandbox. When you call back into Tadabase APIs, pass the encoded ID as-is — don't try to decode it.
Always Test Run before activating
The step's Test Run button executes the code with sample inputs without saving the workflow. Use it. The modal shows logs, the result object, execution time, and any client-side actions you queued — separately, not as one JSON blob.
Next up
When you're ready, pick the snippet topic closest to your task — strings, numbers, dates, arrays, validation, branching, identifiers, external APIs, connected records, logged-in user, or client-side UX — and start adapting from there.
We'd love to hear your feedback.