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.
A code operation has no template syntax. Write input.system_vars.now, never {{system_vars.now}}. Braces in the TypeScript are neither substituted nor flagged, so a {{args.id}} inside a string literal ships to your API as literal braces on every call.This is also the one place braces survive on purpose: a script that needs to build a mustache template for something downstream can.
Secrets are the one root with a rule attached: reach them by a literal nameinput.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 two code 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 timerssetTimeout 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:
A non-string 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, including file:
  • 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
Prefer an 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 earlier http 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.
Publish open into tooldata and branch on it:
Note the JSON boolean on the right rather than the string "true" — see conditionals.

Combining two earlier operations

Two http operations published a balance and a credit note. Neither knows about the other; this does the arithmetic.
Publish 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.