code runs a TypeScript function on an isolated service, away from the call.
Reach for it when an API response needs reshaping before anything else can use
it, when a value has to be computed, or when a decision is more than one
comparison.
The contract
source_ts must export default async function handler(input) and return
an object. The object is what publish_fields reads out of, exactly as an
http operation’s JSON response is. A return value that is not an object or an
array is wrapped, so a bare return 42 publishes at the path result.
input is the whole interface — the same six roots the rest of the tree
templates, as plain properties:
system_vars and vars are read-only here as everywhere else, and writing to
input.userdata changes nothing — the way to write is to return the value and
publish it.
Secrets are the one root with a rule attached: reach them by a literal name —
input.secrets.STRIPE_KEY or input.secrets["STRIPE_KEY"]. The names a script
needs are found by scanning its source, and only those are sent to the sandbox,
so a name assembled at runtime resolves to undefined.
Config
The TypeScript is compiled at publish, not on the call, so a live call never
waits for a build. Compiling is also how the operation is validated: a script
that does not compile is a publish error naming the line. Types are stripped,
not checked — annotations are for you, not for us, and a wrong one is not an
error.
The sandbox
Every run gets a fresh V8 isolate, created for that run and thrown away afterwards. Nothing persists between two runs, or between twocode operations
in the same tree — pass values through tooldata.
What is there: the standard JavaScript built-ins — JSON, Math, Date,
Promise, Map, Set, RegExp, Intl with full timezone data — plus two
globals the platform adds, console and fetch.
What is not: require, import, and every Node API. There is no process,
no Buffer, no URL or URLSearchParams, no TextEncoder, no crypto, no
structuredClone, no atob, and no timers — setTimeout does not exist.
Build query strings by hand, and note that request signing (an HMAC over the
body, say) cannot be done here.
fetch
The sandbox’s fetch is a small shim, not the browser API:
body is JSON-stringified for you. There is no Response object,
no streaming, and redirects are never followed.
It is guarded the same way an http operation is,
and it throws on refusal:
fetch failed: URL must be http(s)— any other scheme, includingfile:fetch failed: destination 169.254.169.254 is not allowed (private/internal)— private, loopback, link-local, CGNAT, unique-local, multicast and reserved addresses, checked at connection time so a rebinding DNS answer cannot slip through
http operation when the call is the point: it is visible in the
tree, its request and response show up in a test run, and it publishes without
you writing any plumbing. Use fetch when a script genuinely needs a second
round trip to finish its job.
Limits
Going over time or memory fails the operation —
execution exceeded 10000ms —
and on_error decides what happens next.
Where console output goes
console.log, .info, .warn and .error are captured per run.
On a test run they are the whole point: each code operation in the trace shows
its captured lines beside its return value, which is how you debug a script
against a real response. If the script throws, the last ten lines are appended
to the error the trace shows.
On a live call they are not shown to you. They go to the platform’s own
logs, and the call detail page does not carry them. Debug in a test run.
Worked examples
Reshaping an API response
An earlierhttp published the raw slots array. This turns it into something
a say can use.
first_id is deliberately not published: a publish path whose value is null
fails the operation, and on a morning with no slots it would be. Publish only
what is always there, or return a placeholder instead of null.
Business hours
{{system_vars.time}} is "7:26 PM" — a string for a human, useless to
compare. This is where the hours logic belongs:
The sandbox’s own clock runs in UTC, so
getHours() and toLocaleString()
without a timeZone give you UTC, not the agent’s zone. Always name the zone —
either from input.vars as above, or as a literal "Asia/Kolkata".
input.system_vars.now already carries the agent’s offset, so new Date(...)
parses the right instant.open into tooldata and branch on it:
"true" — see
conditionals.
Combining two earlier operations
Twohttp operations published a balance and a credit note. Neither knows about
the other; this does the arithmetic.
amount_due and settled into tooldata, then read
{{tooldata.amount_due}} in the say and branch on {{tooldata.settled}}.
Next
Conditionals
Branching on what a script computed.
The HTTP operation
The other half of most tools.
Templating and data
Publishing a return value, and choosing a store.
Testing and publishing
Where a script’s
console output is readable.