a9script

Docs / Reference

a9script function reference

Everything a script can call, generated from the platform’s function catalog. Do not edit this file — it is rendered from the catalog; change the catalog and run npm run docs:reference.

Two kinds of thing appear here. Some answer immediately, inside the run: the same answer every time, nothing paused — and the values the platform provides, like input. Platform calls do work outside the script; the run pauses at the call and continues with the result it was given, so a resumed run never calls twice. Scripts are written synchronously either way: there is no await in this language.

Patterns

What to know before writing one of these calls. Each shows the shape to avoid and the shape to write instead — some are refused outright, some simply cost more than they look like they do.

Call platform functions from a plain loop, not from inside a callback

fetch, checkpoint and every other platform function work anywhere — inside a map, filter, forEach or sort callback included. Write a loop anyway: the calls happen one at a time either way, and only a loop can break when the list turns out to be long, the budget short, or an answer bad. A callback’s only exit is throw, which abandons the whole iteration.

Instead of this:

const contacts = input.event.body.items.map((item) =>
  fetch({ connection: "crm", method: "GET", path: "/contacts/" + item.sku }),
);

return contacts.length;

write this:

const items = input.event.body.items;
const contacts = [];

for (const item of items) {
  contacts.push(fetch({ connection: "crm", method: "GET", path: "/contacts/" + item.sku }));
}

return contacts.length;

// The loop can stop. `break` on a budget, a count, a bad answer —
// none of which a callback can say. From inside a callback the only
// way out is `throw`, which abandons the whole iteration.

Why — A map reads as a transformation and hides that each item is a call. A loop puts the calls in front of you, lets you stop early, and is the shape to reach for whenever the list is not small and known.

Better still — When the loop’s body only moves rows to or from a dataset, there is no loop to write: dsPutMany stores a whole list in one call, dsGetMany reads many keys back in one, and dsPage walks staged rows a page at a time. When the loop exists to read every page of an API, dsFetchInto is the whole pull in one call — it fetches, stages and keeps its place across a pause, which a hand-written cursor loop cannot. And when it exists to reshape staged rows or to fill them in from a second system, dsMapInto and dsEnrich do the whole set the same way: a batch at a time, on the platform’s side, with every call made exactly once even across a pause.


Turn a stored file into rows with the file calls, not by reading it into the script

A script may hold only a few hundred kilobytes of a file, so reading a stored CSV and parsing its text passes on a sample and refuses on a real export. dsFromCsv reads the file where it is stored into a dataset, a batch at a time; dsToCsv writes one back out.

Instead of this:

const file = writeBlob(btoa("email\nada@example.com\n"), "text/csv");

return parseCsv(atob(readBlob(file).contentBase64)).length;

write this:

const file = writeBlob(btoa("email\nada@example.com\n"), "text/csv");

return dsFromCsv(file, "run:contacts", { keyPath: "email" }).rows;

Why — A file’s contents are kept outside a run on purpose: a handle costs nothing to carry, a megabyte costs the run.

Better still — Keep readBlob and parseCsv for a small file you want the text of.


Count and summarise where the rows are, not in the script

A loop that reads staged rows only to add them up pulls the whole set through the run to produce a handful of numbers. dsCount answers a total, dsStats groups by any field, dsDuplicates finds the collisions a unique target field would refuse — each one query, no rows moved. The same holds for the sync’s own account of itself: sendReport renders the report dataset — totals, breakdowns, and the failures by name — and sends it, without a script reading a row.

Instead of this:

dsPutMany("run:people", [
  { key: "u1", value: { dept: "Research" } },
  { key: "u2", value: { dept: "Research" } },
  { key: "u3", value: { dept: "Operations" } },
]);

const counts = {};
let at = 0;
for (;;) {
  const page = dsPage("run:people", { offset: at, limit: 200 });
  if (page.length === 0) { break; }
  for (const row of page) {
    counts[row.value.dept] = (counts[row.value.dept] || 0) + 1;
  }
  at = at + page.length;
}

return counts.Research;

write this:

dsPutMany("run:people", [
  { key: "u1", value: { dept: "Research" } },
  { key: "u2", value: { dept: "Research" } },
  { key: "u3", value: { dept: "Operations" } },
]);

const groups = dsStats("run:people", "dept");

// One GROUP BY where the rows are. Each group is the value as JSON
// text — "Research" arrives with its quotes.
return groups[0].count;

Why — Staged data lives outside the run so that its size is not the run’s problem. A tally written in script quietly pulls it back in — the one thing the dataset calls exist to avoid.


Take the result of a call directly — there are no promises

A platform call returns its result to the next line. There is no await (the word does not parse), no .then, and nothing to hand a callback to — Promise is not defined. Callbacks themselves are ordinary; the platform simply never asks for one.

Instead of this:

const contacts = fetch({ connection: "crm", method: "GET", path: "/contacts" }).then(function (response) {
  return response.body.items;
});

return contacts;

write this:

const response = fetch({ connection: "crm", method: "GET", path: "/contacts" });

return response.body.items;

Why — A call returns the answer itself — there is no promise, so there is no .then on it, and no callback for the platform to run later.

Core

log

Platform call — the run pauses here and continues with the result

log(message, data?)

Writes an info line to the run’s log — the record an operator reads afterwards.

Parameters

  • message (string) — The line’s text. A non-string value is recorded as JSON.
  • data (object, optional) — Structured payload kept with the line, for detail a sentence cannot carry.

Returns — Nothing.

Example

log("order received", { id: input.event.body.id });

logInfo

Platform call — the run pauses here and continues with the result

logInfo(message, data?)

Writes an info line — the explicit spelling of log.

Parameters

  • message (string) — The line’s text.
  • data (object, optional) — Structured payload kept with the line.

Returns — Nothing.

Example

logInfo("sync finished", { contacts: 42 });

logError

Platform call — the run pauses here and continues with the result

logError(message, data?)

Writes an error line. The run continues — an error line reports, it does not end anything; throw or return to end a run. A triggered run that logs one also turns its script’s health red until somebody acknowledges it; a Test never does.

Parameters

  • message (string) — What went wrong.
  • data (object, optional) — Structured payload kept with the line (the offending record, a status).

Returns — Nothing.

Example

logError("contact rejected by the CRM", { id: input.event.body.id, status: 422 });

logWarning

Platform call — the run pauses here and continues with the result

logWarning(message, data?)

Writes a warn line — something survivable that a human should still see. A triggered run that logs one turns its script’s health amber until somebody acknowledges it; a Test never does.

Parameters

  • message (string) — What is off.
  • data (object, optional) — Structured payload kept with the line.

Returns — Nothing.

Example

logWarning("contact has no address — skipped", { id: input.event.body.id });

logVerbose

Platform call — the run pauses here and continues with the result

logVerbose(message, data?)

Writes a verbose line. Kept in full while the run is recent; dropped from a SUCCEEDED run’s archived copy, so detail is cheap here.

Parameters

  • message (string) — The detail line.
  • data (object, optional) — Structured payload kept with the line.

Returns — Nothing.

Example

logVerbose("page fetched", { page: 3, items: 25 });

logDebug

Platform call — the run pauses here and continues with the result

logDebug(message, data?)

Writes a debug line. Like logVerbose, it is dropped from a succeeded run’s archived copy and kept in full on a failed one.

Parameters

  • message (string) — The debug line.
  • data (object, optional) — Structured payload kept with the line.

Returns — Nothing.

Example

logDebug("mapping the trigger payload", { raw: input.event.body });

respond

Platform call — the run pauses here and continues with the result

respond(value)

Answers the caller of a synchronous API call with value, as JSON. At most once per run — a second call throws. The run keeps going after it, and a run triggered by anything else (webhook, schedule, mail) simply has nobody listening.

Parameters

  • value (any) — The JSON-serialisable answer. respond(undefined) is a real, empty answer.

Returns — Nothing.

Example

respond({ ok: true, received: input.event.id });

stop

Platform call — the run pauses here and continues with the result

stop()

Does nothing — kept so the name resolves. A run ends with a top-level return (whose value becomes the run’s output) or by throwing.

Returns — Nothing.

Example

stop(); // does not end the run
return "nothing to do";

echo

Platform call — the run pauses here and continues with the result

echo(value)

Sends a value to the platform and back — a diagnostic. It answers identically in an on-demand Test run and in a dispatched one, which is what makes it useful when a script’s surroundings are in doubt. It carries no business meaning.

Parameters

  • value (any) — Any JSON-serialisable value; the same value comes back.

Returns — The value it was given.

Example

const pong = echo("ping");
log("round trip: " + pong);

fetch

Platform call — the run pauses here and continues with the result, and it does real network I/O

fetch(request)

Calls a configured HTTP connection. Authentication, retries, throttling, token refresh and log redaction are the platform’s job — the script names a connection and a path. A non-2xx answer RETURNS (branch on status); a call that could not be made throws, catchable, with its code in the message.

Parameters

  • request (object){ connection, method, path | url, query?, headers?, body?, timeoutMs? }. connection is the name of a configured HTTP connection; path is joined onto its base URL. Any other field is refused by name — the platform binds the tenant and environment itself.

Returns{ status, headers, body } — header names lowercased, body parsed when it is JSON.

Example

const response = fetch({
  connection: "crm",
  method: "GET",
  path: "/contacts",
  query: { updatedSince: input.settings.since }
});
if (response.status !== 200) {
  throw new Error("crm answered " + response.status);
}
log("contacts: " + response.body.items.length);

input

Value — read it directly, nothing is called

input

The run’s input — a global variable filled from OUTSIDE before the script starts: input.event is what triggered the run, input.settings is what the linked settings script returned. Read-only: the whole object is frozen, and assigning to it throws.

Value{ event, settings }.

Example

log("triggered by " + input.event.kind);

input.event

Value — read it directly, nothing is called

input.event

What started this run: the webhook or API call, the scheduled fire, the arrived mail. Read-only — the whole input is frozen, and assigning to it throws.

Value{ kind, id, endpoint?, body, headers?, receivedAt, tenantId, environmentId }body is the caller’s payload, kind says which door it came through.

Example

const order = input.event.body;
log("order " + order.id + " via " + input.event.kind);

input.settings

Value — read it directly, nothing is called

input.settings

The value the linked settings script returned, evaluated once before this run started — configuration as data, so environment-specific values never sit in the script. undefined when no settings script is linked.

Value — Whatever the settings script returned.

Example

const baseUrl = input.settings.crmBaseUrl;
log("using " + baseUrl);

Encoding

base64Encode

Platform call — the run pauses here and continues with the result

base64Encode(text)

Text to base64, read as UTF-8 — so accented letters and emoji encode as themselves.

Parameters

  • text (string) — The text to encode.

Returns — The base64 text.

Example

const encoded = base64Encode(input.event.body.customer);
log(encoded);

base64Decode

Platform call — the run pauses here and continues with the result

base64Decode(text)

Base64 back to text. Anything that is not base64, and anything whose bytes are not text, is refused — a half-decoded string full of replacement characters is corruption that looks like data.

Parameters

  • text (string) — The base64 to decode.

Returns — The decoded text.

Example

const decoded = base64Decode("aGVsbG8=");
log(decoded); // hello

hexEncode

Platform call — the run pauses here and continues with the result

hexEncode(text)

Text to lower-case hexadecimal, read as UTF-8 — two characters per byte.

Parameters

  • text (string) — The text to encode.

Returns — The hexadecimal text.

Example

log(hexEncode("hi")); // 6869

hexDecode

Platform call — the run pauses here and continues with the result

hexDecode(text)

Hexadecimal back to text. An odd number of characters, anything outside 0-9a-f, and bytes that are not text are all refused.

Parameters

  • text (string) — The hexadecimal to decode.

Returns — The decoded text.

Example

log(hexDecode("6869")); // hi

Crypto

uuid

Platform call — the run pauses here and continues with the result

uuid()

A fresh identifier, version 4, from real randomness. The value is kept the moment it is drawn, so a run that is saved and continued answers the same id — which is what makes it safe as an idempotency key for a call you must not make twice.

Returns — The identifier as text, e.g. f81d4fae-7dec-4d0e-a765-00a0c91e6bf6.

Example

const key = uuid();
log("idempotency key " + key);

randomHex

Platform call — the run pauses here and continues with the result

randomHex(bytes?)

Random bytes, written as hexadecimal — for a one-off token or a nonce. Real randomness, kept the moment it is drawn, exactly like uuid().

Parameters

  • bytes (number, optional) — How many bytes, from 1 to 1024. Defaults to 16.

Returns — Lower-case hexadecimal — two characters per byte, so 32 characters by default.

Example

const nonce = randomHex(8);
log("nonce " + nonce);

sha256

Platform call — the run pauses here and continues with the result

sha256(text)

The SHA-256 digest of a piece of text, read as UTF-8 — the same digest every other system computes for the same string.

Parameters

  • text (string) — The text to hash.

Returns — The digest as lower-case hexadecimal, 64 characters.

Example

const digest = sha256(input.event.body.id);
log("digest " + digest);

hmacSha256

Platform call — the run pauses here and continues with the result

hmacSha256(key, message)

Signs a message with a shared secret (HMAC-SHA-256) — the shape webhook signatures use, so this is how a script proves a payload came from it. To CHECK an incoming signature, configure the endpoint’s own signature check instead: it runs before the script and compares in constant time, which === in a script does not.

Parameters

  • key (string) — The shared secret, as text.
  • message (string) — What to sign, read as UTF-8.

Returns — The signature as lower-case hexadecimal, 64 characters.

Example

const signature = hmacSha256(input.settings.signingSecret, input.event.body.id);
log("signature " + signature);

Text

slugify

Answers immediately — nothing pauses

slugify(text)

Turns a line of text into a slug that is safe in a URL or a file name: lower case, words joined by dashes, accented letters folded to their plain form and everything else dropped.

Parameters

  • text (string) — The text to convert. Only text — a number or a missing value is refused rather than turned into the slug of the word for it.

Returns — The slug. Text with nothing worth keeping — punctuation alone, an emoji — answers an empty string.

Example

const slug = slugify(input.event.body.customer);
log("slug: " + slug);

template

Answers immediately — nothing pauses

template(text, values)

Fills {{key}} placeholders in a piece of text from an object of values. Nothing is escaped — the answer is plain text, so a value that has to be safe inside HTML or a URL must be made safe before it goes in.

Parameters

  • text (string) — The text to fill. {{name}} marks a placeholder and spacing inside the braces is ignored; a single brace is ordinary text.
  • values (object) — What to put in, looked up by key. Text, numbers and booleans are written out; a key the object does not carry, and a value that is null or missing, render as nothing; an object value is refused, since it has no sensible written form.

Returns — The filled text.

Example

const note = template("Hi {{name}}, order {{id}} is on its way.", {
  name: "Ada",
  id: input.event.body.id
});
log(note);

Numbers

formatNumber

Platform call — the run pauses here and continues with the result

formatNumber(value, options?)

Writes a number the way a reader in a given country expects — grouped thousands, the right decimal mark, as many decimals as you ask for.

Parameters

  • value (number) — The number to write. It must be finite.
  • options (object, optional){ locale, minimumFractionDigits, maximumFractionDigits }. locale is a language tag such as en-US or de-DE and defaults to en-US — stated, never taken from the machine, so the same script writes the same number everywhere. Any other option is refused by name.

Returns — The formatted text.

Example

log(formatNumber(1234.5, { locale: "de-DE" })); // 1.234,5

formatCurrency

Platform call — the run pauses here and continues with the result

formatCurrency(value, currency, options?)

Writes an amount of money: the symbol where that country puts it, and the number of decimals the currency itself uses — two for dollars and euros, none for yen.

Parameters

  • value (number) — The amount. It must be finite.
  • currency (string) — A three-letter currency code such as USD, EUR or JPY.
  • options (object, optional) — The same options formatNumber takes, with the same en-US default when no locale is given.

Returns — The formatted text.

Example

const total = input.event.body.items[0].price;
log(formatCurrency(total, "EUR", { locale: "de-DE" }));

Structured data

get

Answers immediately — nothing pauses

get(data, path)

Reads a value out of a shape by a path, without failing when part of the path is not there. get(order, "customer.address.city") answers the city, or nothing at all when the order carries no address — where order.customer.address.city would end the run.

Parameters

  • data (object | array) — The shape to read from. Anything else — a number, or nothing at all — simply answers nothing.
  • path (string) — Where to look: names joined by dots, positions in square brackets, as in items[0].sku. A name addresses a key of an object and [0] addresses an entry of an array. A path that is not written that way is refused, because no data can make it right.

Returns — The value at that place, or nothing when any step of the path is missing.

Example

const city = get(input.event.body, "shipping.address.city");
log(city === undefined ? "no city was given" : city);

set

Answers immediately — nothing pauses

set(data, path, value)

Answers a copy of a shape with one place changed. What you pass in is never touched — which is how a script changes something in the payload it was handed, since that payload is read-only.

Parameters

  • data (object | array) — The shape to derive from. Nothing at all is allowed too, and the answer is then just the shape the path describes.
  • path (string) — Where to write — the same paths get reads. A step that does not exist yet is created: a name makes an object, a position makes an array. A step that holds a plain value, or one whose kind disagrees with the path, is refused by name rather than overwritten.
  • value (any) — What to put there.

Returns — A new object or array. Everything the path did not touch is the very same value it was, not a copy of it, so changing one field of a large payload stays cheap. That sharing is also why a branch the answer still shares with the trigger’s payload cannot be assigned into — reach it with another set instead.

Example

const enriched = set(input.event.body, "status", "received");
log("status is now " + enriched.status);

jsonPath

Answers immediately — nothing pauses

jsonPath(data, expression)

Collects every value a shape holds at some place, as a list — for when the interesting values sit at a depth you would otherwise write a loop per level to reach.

Parameters

  • data (object | array) — The shape to search.
  • expression (string) — Starts at $, the whole value. .name and ['name'] step into a key, [0] into a position, [*] takes every entry, and ..name finds that name at any depth. Anything else is refused rather than quietly matching nothing.

Returns — A list of the matching values, at most 10000 of them — an expression selecting more is refused rather than answered in part, so narrow the path or read the shape in smaller pieces. A descent answers shallow-first: a value’s own match comes back before matches found deeper inside it. A branch the shape holds in two places matches once per place. Nothing matched answers an empty list.

Example

const skus = jsonPath(input.event.body, "$.items[*].sku");
log("the order names " + skus.length + " products");

parseCsv

Platform call — the run pauses here and continues with the result

parseCsv(text, options?)

Reads CSV text into a list of rows. Quoted fields, embedded commas, quotes and line breaks are all handled — this is a real CSV reader, not a split on commas.

Parameters

  • text (string) — The CSV to read.
  • options (object, optional){ delimiter, header }. delimiter defaults to a comma; header defaults to true, meaning the first line NAMES the columns and each row comes back as an object. Set it to false to get each row as a list of cells instead. Any other option is refused by name.

Returns — A list of rows: objects keyed by column name, or lists of cells when there is no header. Every cell is TEXT — a CSV says nothing about types, and reading 01234 as a number would lose the leading zero of a postcode or an SKU. Use Number(cell) where you want a number.

Example

const rows = parseCsv("sku,qty\nA-1,2\nB-2,1\n");
log("the file holds " + rows.length + " rows");
log("first SKU: " + rows[0].sku);

toCsv

Platform call — the run pauses here and continues with the result

toCsv(rows, options?)

Writes a list of rows as CSV text, quoting whatever needs quoting. A cell that begins with =, +, - or @ is written as text, so a spreadsheet opening the file cannot be made to run it as a formula.

Parameters

  • rows (array) — The rows: objects keyed by column name, or lists of cells. Text, numbers and booleans are all written; a missing value or null writes an empty cell.
  • options (object, optional){ delimiter, header, columns, guardFormulas }. columns names the columns to write and their order — otherwise the keys of the first row are used, so give it when later rows may carry keys the first does not. guardFormulas defaults to true and is the protection described above; turn it off only when the file is not going to be opened in a spreadsheet.

Returns — The CSV text, one line per row, ending in a line break.

Example

const text = toCsv([
  { sku: "A-1", qty: 2 },
  { sku: "B-2", qty: 1 }
]);
log(text);

parseXml

Platform call — the run pauses here and continues with the result

parseXml(text, options?)

Reads XML into an object tree. An element becomes a key, a repeated element becomes a list, and an attribute becomes a key with @ in front of it — so an attribute and a child element of the same name never collide.

Parameters

  • text (string) — The XML to read. Text that is not well-formed is refused rather than half-read.
  • options (object, optional){ attributePrefix, textKey, lists }. attributePrefix defaults to @; textKey defaults to #text and is where an element’s own text goes when it also carries attributes. lists names the elements that are ALWAYS a list — ["line"], or ["order.line"] where the same name means something else at another depth — which is how a script that walks repeated data stops depending on how many the sender sent. Any other option is refused by name.

Returns — The tree. Every value is TEXT, for the reason parseCsv gives: XML carries no types, and reading 1.50 as a number would quietly drop the cents. One thing to watch: an element that appears ONCE is its value alone, not a one-entry list — so a document that happens to carry a single entry hands you its first character where you expected an entry. Name that element in lists and it is a list however many times it appears; without it, check Array.isArray first.

Example

const tree = parseXml("<order id=\"o-1\"><line>A-1</line></order>", { lists: ["line"] });
log("order " + tree.order["@id"]);
log("first line: " + tree.order.line[0]);

toXml

Platform call — the run pauses here and continues with the result

toXml(value, options?)

Writes an object tree as XML. A key with @ in front of it becomes an attribute, a list becomes a repeated element, and everything else becomes an element.

Parameters

  • value (object) — The tree to write.
  • options (object, optional){ attributePrefix, textKey, indent } — the first two as parseXml takes them, so a tree read with one prefix can be written back with the same one. indent defaults to false; set it to true for XML a person is going to read.

Returns — The XML text.

Example

const xml = toXml({
  order: { "@id": input.event.body.id, total: "42.00" }
});
log(xml);

Documents

createPdf

Platform call — the run pauses here and continues with the result

createPdf(document)

Draws a PDF and answers it as text you can file or send. You say what the document HOLDS — a list of blocks: headings, paragraphs, lists, tables, rules, gaps and page breaks — and how it is laid out is decided for you: text wraps, pages break where they must, and a table repeats its column names at the top of every page it runs onto. A document is set in the standard PDF fonts, which draw the Western alphabet; a character outside it — Greek, Cyrillic, Chinese, an emoji — is refused by name rather than drawn as something else.

Parameters

  • document (object){ title, blocks }. blocks is the list, drawn in the order you write it, and every block names its kind: heading (with level 1, 2 or 3, and text), text (with text), list (with items, and ordered to number them), table (with headers and rows), divider, spacer (with height in points) and pageBreak. Wherever text is expected you may also pass a number or true/false, and nothing at all writes an empty line — but a shape is refused, because there is no reading of it a person would want. title is the name the document carries: a reader meets it in their PDF viewer’s title bar, never on the page, so a title to be SEEN is a heading block. Any other key, any unknown kind, and any row that does not fit its headers, is refused naming the block it is in.

Returns — The document as base64 text — the same shape writeBlob takes, so a report can be built and filed in two lines. Building the same document twice answers exactly the same text: nothing about the moment it was built goes into it, which is also why a date a reader needs belongs in a block they can see.

Example

const invoice = createPdf({
  title: "Invoice 4711",
  blocks: [
    { kind: "heading", level: 1, text: "Invoice 4711" },
    { kind: "text", text: "Thank you for your order." },
    { kind: "table", headers: ["item", "amount"], rows: [["Widget", 12.5], ["Delivery", 4]] },
    { kind: "divider" },
    { kind: "text", text: "Total: 16.50" }
  ]
});
log("the invoice is " + invoice.length + " characters of base64");

Markdown

markdownToHtml

Platform call — the run pauses here and continues with the result

markdownToHtml(markdown, options?)

Turns markdown into HTML — headings, lists, tables, links, code blocks, bold and italic, the dialect people write in. It does not make the result safe. Markdown is allowed to contain HTML, so anything dangerous in the markdown is still dangerous in the answer: if any part of it came from outside your own script — a form field, an email, a webhook — pass the answer through sanitizeHtml before it reaches a reader.

Parameters

  • markdown (string) — The markdown to read.
  • options (object, optional){ lineBreaks }. lineBreaks defaults to false, which is how markdown normally reads: a single newline is a space, and a blank line starts a paragraph. Set it to true when the text was typed in a box where people press Enter and expect a new line. Any other option is refused by name.

Returns — The HTML, as text. Characters that would otherwise be read as markup are escaped in ordinary text, but HTML written directly in the markdown is passed through as written — which is exactly why untrusted input needs sanitizeHtml.

Example

const html = markdownToHtml("# Report\n\nSales were **up**.");
log(sanitizeHtml(html));

sanitizeHtml

Platform call — the run pauses here and continues with the result

sanitizeHtml(html, options?)

Removes everything from HTML that could run, load or frame anything, and answers what is left. Works from a list of what is ALLOWED rather than a list of what is forbidden — so a tag nobody has heard of is removed too, which is what makes it safe against things nobody thought of yet.

Parameters

  • html (string) — The HTML to clean.
  • options (object, optional){ allowedTags, allowedAttributes }. Both replace the standard list rather than adding to it, so a reader of the call can see everything it permits: allowedTags is a list of tag names, allowedAttributes an object of tag name to the attributes that tag may keep. Passing an empty list for both answers plain text. Whatever you allow, a link or image address may still only use http, https, mailto or tel — widening cannot re-admit a dangerous one.

Returns — The cleaned HTML. A removed tag keeps the words it wrapped, so no content disappears — except for a script or a style block, whose contents go with it. Cleaning something already cleaned changes nothing.

Example

const safe = sanitizeHtml("<p>Hi</p><script>alert(1)</script>");
log("cleaned to: " + safe);

escapeHtml

Platform call — the run pauses here and continues with the result

escapeHtml(text)

Turns the five characters that change how markup is read — &, <, >, " and ' — into the codes that stand for them. Use it when you are building HTML yourself and want a value to appear as TEXT, exactly as written, rather than becoming part of the markup around it.

Parameters

  • text (string) — The text to escape.

Returns — The same text with those five characters replaced. Everything else — every alphabet, every accent, every emoji — is left exactly as it was.

Example

const name = escapeHtml("Ben & Jerry's <team>");
log("<p>Hello, " + name + "</p>");

stripMarkdown

Platform call — the run pauses here and continues with the result

stripMarkdown(markdown)

Answers the plain words of a markdown document, with all of its formatting removed — for a plain-text version of a message, a summary line, or anywhere a reader will not see markup.

Parameters

  • markdown (string) — The markdown to strip.

Returns — The text, with headings, emphasis, links and tags gone and the words kept. A link becomes the words it linked, not the address.

Example

const plain = stripMarkdown("# Title\n\nSome **bold** words.");
log(plain);

markdownToPdf

Platform call — the run pauses here and continues with the result

markdownToPdf(markdown, options?)

Draws a markdown document as a PDF, and answers it the way createPdf does. Headings become headings, lists become lists, tables become tables and a rule becomes a rule; bold and italic are flattened to their words, because a drawn document holds text rather than styled runs. Markdown has six heading levels and a document draws three, so a level 4, 5 or 6 heading is drawn at the smallest size rather than lost.

Parameters

  • markdown (string) — The markdown to draw.
  • options (object, optional){ title } — the name the document carries, exactly as createPdf takes it: a reader meets it in their PDF viewer’s title bar, never on the page. Any other option is refused by name.

Returns — The document as base64 text — the same shape writeBlob takes. The same rules apply as when you build a document by hand, including which characters a document can draw: markdown that nests far deeper than anyone writes is refused rather than read.

Example

const pdf = markdownToPdf("# Invoice 4711\n\n| item | amount |\n| --- | --- |\n| Widget | 12.50 |", { title: "Invoice 4711" });
log("the document is " + pdf.length + " characters of base64");

Date & time

formatDate

Answers immediately — nothing pauses

formatDate(date, pattern)

Writes a date out in a pattern of your own, always in UTC. The tokens are YYYY, MM, DD, HH, mm and ss; every other character of the pattern is written as it stands.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value from new Date(...). A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • pattern (string) — The shape of the answer, e.g. DD.MM.YYYY or YYYY-MM-DD HH:mm:ss. Keep punctuation between the tokens: a word that happens to spell one carries it.

Returns — The formatted text.

Example

log(formatDate(input.event.receivedAt, "YYYY-MM-DD HH:mm"));

addDays

Answers immediately — nothing pauses

addDays(date, days)

Moves a date forwards or backwards by whole days and answers the new moment, in UTC. The time of day is kept.

Parameters

  • date (string | number | date) — An ISO date, a datetime that carries its timezone, milliseconds since 1970, or a date value — the same dates formatDate takes.
  • days (number) — How many days to move. A negative count moves back.

Returns — The moved date as an ISO string, e.g. 2026-07-21T00:00:00.000Z.

Example

const due = addDays(input.event.receivedAt, 14);
log("payable by " + formatDate(due, "YYYY-MM-DD"));

dateDiff

Answers immediately — nothing pauses

dateDiff(from, to)

How many whole days lie between two dates, counted as UTC calendar days: positive when to is the later one, negative when it is the earlier.

Parameters

  • from (string | number | date) — The date to count from — an ISO date, milliseconds since 1970, or a date value.
  • to (string | number | date) — The date to count to, in any of the same forms.

Returns — The number of days. Two moments on the same UTC day answer 0; two hours either side of midnight answer 1, because the question is which day each one falls on.

Example

const age = dateDiff("2026-03-01", input.event.receivedAt);
log("the order is " + age + " days old");

now

Platform call — the run pauses here and continues with the result

now()

The current moment, in milliseconds. The same clock Date.now() reads, so the two can never disagree — and the reading is kept: a run that is saved and continued later still sees the moment it read.

Returns — Milliseconds since the start of 1970, as a number.

Example

const startedAt = now();
log("started at " + formatDate(startedAt, "YYYY-MM-DD HH:mm:ss"));

today

Platform call — the run pauses here and continues with the result

today()

Today’s date in UTC. The day now() falls on, written out.

Returns — The date as YYYY-MM-DD.

Example

log("the report covers " + today());

dateAdd

Platform call — the run pauses here and continues with the result

dateAdd(date, amount, unit, timeZone?)

Moves a moment forwards by whole units and answers the new moment. Months and years follow the calendar: one month after 31 January is 28 February, because there is no 31 February.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • amount (number) — How many units to move. A whole number; a negative one moves backwards.
  • unit (string) — Which unit to move by: year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — Whose calendar to walk, such as Europe/Berlin. Without it, UTC — where a day is always twenty-four hours. With it, a day is a day on that clock: 09:00 in Berlin plus one day is 09:00 the next morning even when the clocks moved overnight, while plus 24 hours is 10:00.

Returns — The new moment as ISO text, written in the timezone that was named — 2026-02-28T10:00:00.000Z when none was.

Example

const due = dateAdd(input.event.receivedAt, 1, "month");
log("payable by " + dateFormat(due, "DD.MM.YYYY"));

const sameTimeTomorrow = dateAdd(input.event.receivedAt, 1, "day", "Europe/Berlin");
log("and again at " + dateFormat(sameTimeTomorrow, "HH:mm", "Europe/Berlin"));

dateSubtract

Platform call — the run pauses here and continues with the result

dateSubtract(date, amount, unit, timeZone?)

Moves a moment backwards by whole units — dateAdd the other way. Note that the two do not always undo each other: one month after 31 January is 28 February, and one month before that is 28 January.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • amount (number) — How many units to move back. A whole number.
  • unit (string) — Which unit to move by: year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — Whose calendar to walk, such as Europe/Berlin. Without it, UTC — where a day is always twenty-four hours. With it, a day is a day on that clock: 09:00 in Berlin plus one day is 09:00 the next morning even when the clocks moved overnight, while plus 24 hours is 10:00.

Returns — The new moment as ISO text, written in the timezone that was named.

Example

const since = dateSubtract(today(), 7, "days");
log("looking back to " + since);

dateDifference

Platform call — the run pauses here and continues with the result

dateDifference(from, to, unit, timeZone?)

How many WHOLE units lie between two moments, counted the same way dateAdd moves them — so adding the answer to from never passes to.

Parameters

  • from (string | number | date) — The moment to count from. An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • to (string | number | date) — The moment to count to, in any of the same forms.
  • unit (string) — Which unit to count in: year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — Whose calendar to walk, such as Europe/Berlin. Without it, UTC — where a day is always twenty-four hours. With it, a day is a day on that clock: 09:00 in Berlin plus one day is 09:00 the next morning even when the clocks moved overnight, while plus 24 hours is 10:00.

Returns — The number of whole units, positive when to is the later moment and negative when it is the earlier one. Anything short of a whole unit is dropped: two moments twenty-three hours apart are 0 days apart. One thing to know about leap days: 29 February to 28 February the next year is 0 years, because the anniversary is 1 March. For calendar days — which day each moment falls on, rather than how much time passed — use dateDiff.

Example

const age = dateDifference(input.event.receivedAt, now(), "day");
log("the order is " + age + " days old");

dateIsBefore

Platform call — the run pauses here and continues with the result

dateIsBefore(date, other, unit?, timeZone?)

Is the first moment earlier than the second?

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • other (string | number | date) — The moment to compare against.
  • unit (string, optional) — Compare at this granularity instead of to the millisecond — "day" asks whether the first falls on an earlier day, so two moments on one day answer false. One of year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — Whose day (or week, or month) it is — 23:30 UTC is already the next day in Tokyo. Only meaningful together with a unit, and refused without one. Defaults to UTC.

Returnstrue or false.

Example

if (dateIsBefore(input.event.receivedAt, today())) {
  log("this one arrived before today");
}

dateIsAfter

Platform call — the run pauses here and continues with the result

dateIsAfter(date, other, unit?, timeZone?)

Is the first moment later than the second?

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • other (string | number | date) — The moment to compare against.
  • unit (string, optional) — Compare at this granularity instead of to the millisecond. One of year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — Whose day (or week, or month) it is — 23:30 UTC is already the next day in Tokyo. Only meaningful together with a unit, and refused without one. Defaults to UTC.

Returnstrue or false.

Example

log(dateIsAfter("2026-07-14", "2026-07-13"));

dateIsEqual

Platform call — the run pauses here and continues with the result

dateIsEqual(date, other, unit?, timeZone?)

Are the two the same moment? With a unit, whether they fall in the same one — which is how to ask “were these on the same day” without comparing text.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • other (string | number | date) — The moment to compare against.
  • unit (string, optional) — Compare at this granularity: "day" answers true for any two moments on the same day — whose day it is depends on the timezone. One of year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — Whose day (or week, or month) it is — 23:30 UTC is already the next day in Tokyo. Only meaningful together with a unit, and refused without one. Defaults to UTC.

Returnstrue or false.

Example

if (dateIsEqual(input.event.receivedAt, now(), "day")) {
  log("it arrived today");
}

dateIsBetween

Platform call — the run pauses here and continues with the result

dateIsBetween(date, start, end)

Is the moment inside the window? Both edges count as inside, so a window built from dateStartOf and dateEndOf holds every moment of the period and no more.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • start (string | number | date) — The first moment of the window.
  • end (string | number | date) — The last moment of the window.

Returnstrue or false. A window whose start is after its end is refused rather than answering false for everything, which would look exactly like an empty window.

Example

const from = dateStartOf(today(), "month");
const to = dateEndOf(today(), "month");
log(dateIsBetween(input.event.receivedAt, from, to));

dateStartOf

Platform call — the run pauses here and continues with the result

dateStartOf(date, unit, timeZone?)

The first moment of the day, month, year or other unit the date falls in. A week starts on Monday.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • unit (string) — Which period: year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.

Returns — The moment as ISO text, written in the timezone that was asked for. On a day whose clocks moved, this is that day’s real first moment — 2026-03-29T00:00:00.000+01:00 in Vienna, even though the day ends on +02:00.

Example

const monthStart = dateStartOf(now(), "month", "Europe/Vienna");
log("the month began at " + monthStart);

dateEndOf

Platform call — the run pauses here and continues with the result

dateEndOf(date, unit, timeZone?)

The LAST millisecond of the day, month, year or other unit the date falls in.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • unit (string) — Which period: year, month, week, day, hour, minute, second or millisecond — plurals work too.
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.

Returns — The moment as ISO text in the timezone that was asked for, e.g. 2026-02-28T23:59:59.999Z — the last millisecond, so dateIsBetween with it covers the whole period.

Example

const monthEnd = dateEndOf(now(), "month");
log("the month runs until " + monthEnd);

dateParse

Platform call — the run pauses here and continues with the result

dateParse(text, pattern, timeZone?)

Reads a date somebody else wrote, by the pattern it was written in — the way to take 14.07.2026 from a file or a form and turn it into a moment.

Parameters

  • text (string) — The text to read. It must match the pattern exactly, with nothing left over.
  • pattern (string) — The shape the text is in, from the number tokens YYYY, MM, DD, HH, mm, ss and SSS; everything else in the pattern must appear in the text as it stands. What the pattern does not name starts the period, so YYYY-MM reads as the first of that month at midnight, and a pattern that names no part of a date at all is refused rather than answering 1970. Month names and two-digit years can be written but not read — a name needs a language and 26 cannot say which century it means.
  • timeZone (string, optional) — Which timezone the text is written in — 2026-07-14 12:30 means a different moment in Vienna than in UTC. Defaults to UTC. A wall clock that never happened, because the clocks jumped over it, answers the moment the day reached it; one that happened twice answers the first of them.

Returns — The moment as ISO text, written in the timezone it was read in.

Example

const paidAt = dateParse("14.07.2026", "DD.MM.YYYY", "Europe/Vienna");
log("paid at " + paidAt);

dateFormat

Platform call — the run pauses here and continues with the result

dateFormat(date, pattern, timeZone?, locale?)

Writes a moment out in a pattern of your own, in the timezone you name — the same tokens formatDate uses, plus milliseconds and the names of months and days.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • pattern (string) — The shape of the answer. YYYY and YY for the year, MM for the month and MMM/MMMM for its name, DD for the day and DDD/DDDD for the weekday’s name, HH, mm, ss and SSS for the time. Every other character is written as it stands.
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.
  • locale (string, optional) — Which language the month and weekday names are written in — a language tag such as de-DE. Defaults to en-US, stated rather than taken from the machine, so the same script writes the same date everywhere. A tag nobody has data for is refused rather than quietly written in English.

Returns — The written text.

Example

log(dateFormat(now(), "DDDD, DD. MMMM YYYY", "Europe/Vienna", "de-DE"));

dateGet

Platform call — the run pauses here and continues with the result

dateGet(date, unit, timeZone?)

Reads one part of a date as a number — the year, the month, the day, the time, or which day of the week it is.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • unit (string) — Which part: year, month (1–12), day, hour, minute, second, millisecond, or weekday (1 for Monday through 7 for Sunday).
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.

Returns — The part as a number. Which one it is depends on the timezone: 23:30 UTC is already the next day, with a different weekday, in Vienna.

Example

if (dateGet(now(), "weekday", "Europe/Vienna") === 1) {
  log("it is Monday in Vienna");
}

dateSet

Platform call — the run pauses here and continues with the result

dateSet(date, unit, value, timeZone?)

Answers the same moment with one part changed — the way to say “nine o’clock that morning” or “the first of that month”.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • unit (string) — Which part to change: year, month, day, hour, minute, second or millisecond. The weekday cannot be set — move the date with dateAdd instead.
  • value (number) — What to set it to. A value the part cannot hold is refused by name — the 31st of a month with 30 days says so, rather than quietly becoming the 1st of the next one. Changing the MONTH to a shorter one pulls the day back to its last day, exactly as dateAdd does.
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.

Returns — The new moment as ISO text, written in the timezone that was asked for.

Example

const nine = dateSet(dateStartOf(now(), "day", "Europe/Vienna"), "hour", 9, "Europe/Vienna");
log("the reminder goes out at " + nine);

isWeekend

Platform call — the run pauses here and continues with the result

isWeekend(date, timeZone?)

Is the date a Saturday or a Sunday?

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.

Returnstrue or false. In the timezone asked for: Friday evening in UTC is already Saturday in Tokyo.

Example

if (isWeekend(now(), "Europe/Vienna")) {
  log("holding this until Monday");
}

isLeapYear

Platform call — the run pauses here and continues with the result

isLeapYear(date, timeZone?)

Does the year this date falls in have a 29 February?

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.

Returnstrue or false, by the full rule — 2000 was a leap year, 1900 was not. Which year the date falls in depends on the timezone: 23:30 UTC on New Year’s Eve is already the new year further east.

Example

log(isLeapYear("2024-05-05"));

daysInMonth

Platform call — the run pauses here and continues with the result

daysInMonth(date, timeZone?)

How many days the month this date falls in has.

Parameters

  • date (string | number | date) — An ISO date (2026-07-14), a datetime that carries its timezone (2026-07-14T12:30:00Z), milliseconds since 1970, or a date value. A datetime without a timezone is refused: it would mean a different moment on every machine that read it.
  • timeZone (string, optional) — A timezone name such as Europe/Vienna, or UTC. Defaults to UTC — never the machine’s own, so the same script answers the same everywhere. A name nobody has is refused rather than quietly read as UTC.

Returns — The number of days — 28, 29, 30 or 31.

Example

const days = daysInMonth(now());
log("this month has " + days + " days");

Email

sendMail

Platform call — the run pauses here and continues with the result, and it does real network I/O

sendMail(message)

Sends an email through one of the environment’s mail accounts. The message says what to send; which server and which credentials is the account’s configuration, so a script never holds a password. When the environment has exactly one account it is used by default — name one in account when there are several.

Parameters

  • message (object){ to, subject, text?, html?, cc?, bcc?, from?, replyTo?, account?, attachments? }. Recipients (to, cc, bcc) are addresses as text — comma-separated — or a list of them. text and html are the two ways to say the body; give both and the reader’s mail client picks. from replaces the account’s own address as the visible sender (“Alerts alerts@acme.example”). attachments is a list of { filename, contentBase64, contentType?, cid? } — contents as base64 text, the same shape createPdf answers; give an attachment a cid and the html body can show it inline as <img src="cid:...">. Any other field is refused by name.

Returns{ ok: true, mode, messageId? } — the send succeeded. mode is what the account did with it: deliver sent it as written, redirect sent it to the addresses configured on the account instead of the recipients you named, and record sent nothing and wrote the message into this run’s log. messageId is the identifier the mail server will show the message under, when there was a send and it stated one. Under record the answer also carries recorded{ to, cc, bcc, subject, body }, the message that was not sent. A send that cannot happen — no account, an unknown account name, several accounts and none named, contents that are not base64, a server that refuses — raises a catchable error saying which, naming the account.

Example

const report = createPdf({
  title: "Weekly report",
  blocks: [{ kind: "heading", level: 1, text: "Weekly report" }]
});
const receipt = sendMail({
  to: "ops@acme.example",
  subject: "Weekly report",
  text: "The report is attached.",
  attachments: [{ filename: "report.pdf", contentBase64: report, contentType: "application/pdf" }]
});
log("sent as " + receipt.messageId);

Files

writeBlob

Platform call — the run pauses here and continues with the result, and it does real network I/O

writeBlob(contentBase64, mime?)

Stores bytes as a file and answers a handle for them. Keep the handle and pass it on — the bytes stay in storage instead of being carried through the script, which is what makes files of any size safe to work with.

Parameters

  • contentBase64 (string) — The contents as base64 text — base64Encode makes it from text. Text that is not base64 is refused rather than stored short, because a file that lost its tail looks fine until someone opens it.
  • mime (string, optional) — What the file is, such as "application/pdf" or "text/csv". Stored with it and answered by readBlob.

Returns — The handle: sha256: and the fingerprint of the contents. It IS the contents, so storing the same bytes again answers the same handle and keeps one copy — including when another environment stores the same file, which still cannot read yours.

Example

const handle = writeBlob(base64Encode("sku,qty\nA-1,2\n"), "text/csv");
log("stored as " + handle);

readBlob

Platform call — the run pauses here and continues with the result, and it does real network I/O

readBlob(blobId)

Reads a stored file back: its contents, the type it was stored with, and its size. Read a file only when the script itself has to look inside it — passing the handle on costs nothing whatever the file weighs.

Parameters

  • blobId (string) — The handle writeBlob answered. A file belongs to the environment that stored it: a handle from anywhere else is refused exactly as one that was never stored, and it never answers an empty file.

Returns{ contentBase64, mime, bytes } — the contents as base64 text, the type THIS environment stored it with (missing when it was stored without one), and the true size in bytes. A file too large to carry in a script is refused rather than truncated: keep passing the handle instead.

Example

const handle = writeBlob(base64Encode("hello"), "text/plain");
const file = readBlob(handle);
log("the file holds " + file.bytes + " bytes");

readFile

Platform call — the run pauses here and continues with the result, and it does real network I/O

readFile(path, options?)

Reads a file out of one of this environment’s file stores, by the path it was put there under. Use it for the things somebody uploaded — a logo, a template, a fixture — and for what an earlier run left behind.

Parameters

  • path (string) — Where the file sits in its store. It may contain slashes, so "assets/logo.png" is one path and not two.
  • options (object, optional){ store } — which file store to read from. Left out, it means the store this environment marks as its default; an environment that marks none says so rather than picking one.

Returns{ contentBase64, mime, bytes } — the contents as base64 text, the type it was stored with (missing when it was stored without one), and its true size. A file too large to carry in a script is refused rather than truncated, and a path this environment does not hold is refused exactly as one that never existed.

Example

writeFile("greeting.txt", base64Encode("hello"));
const file = readFile("greeting.txt");
log("the file holds " + file.bytes + " bytes");

writeFile

Platform call — the run pauses here and continues with the result, and it does real network I/O

writeFile(path, contentBase64, options?)

Puts bytes into one of this environment’s file stores under a path, REPLACING whatever was there. What a run writes this way outlives it, which is what makes a file store the place for an artefact rather than a value.

Parameters

  • path (string) — Where to put it in the store. Writing the same path again replaces the file — an upload is not a create, so there is nothing to delete first.
  • contentBase64 (string) — The contents as base64 text, exactly as writeBlob takes them. base64Encode makes it from text.
  • options (object, optional){ store, mime } — which file store to write to, and what the file is. Without a store it means this environment’s default one. A store that only takes uploads refuses a script’s write, and a store that is full refuses too unless its author asked it to make room.

Returns{ path, bytes } — where it landed and how big it is. Deliberately NOT a handle: a stored file is addressed by its path, and the handle-taking calls (sending, attaching) take one from writeBlob.

Example

const stored = writeFile("reports/today.csv", base64Encode("sku,qty\nA-1,2\n"), { mime: "text/csv" });
log("wrote " + stored.bytes + " bytes to " + stored.path);

Mapping

setMapping

Platform call — the run pauses here and continues with the result, and it does real network I/O

setMapping(namespace, source, target)

Remembers which id in the target system corresponds to a source id — for systems that cannot store each other’s keys. Writing a source that is already mapped replaces its target: a re-synced record moved.

Parameters

  • namespace (string) — Which kind of thing is being mapped — one namespace per remote entity type, such as "users" or "invoices". Namespaces are separate maps: the same source id may map differently in each.
  • source (string) — The id on the source side.
  • target (string) — The id it corresponds to on the target side.

Returns — Nothing. The mapping is durable: every later run of this environment sees it.

Example

setMapping("users", "crm-4711", "hr-0815");
log("linked");

getMapping

Platform call — the run pauses here and continues with the result, and it does real network I/O

getMapping(namespace, source)

Answers the target id a source id was mapped to, or null when it never was.

Parameters

  • namespace (string) — The namespace the mapping lives in.
  • source (string) — The source-side id to look up.

Returns — The target id as text, or null for a source nobody mapped — the everyday answer for a record seen for the first time, so check it rather than treating it as a failure.

Example

const known = getMapping("users", "crm-4711");
if (known === null) { log("first sight — create, then setMapping"); }

deleteMapping

Platform call — the run pauses here and continues with the result, and it does real network I/O

deleteMapping(namespace, source)

Forgets a mapping — for the record that was DELETED in the far end. There is no new target id to write in that case, and a mapping pointing at an id that no longer exists makes every later sync update into nowhere.

Parameters

  • namespace (string) — The namespace the mapping lives in.
  • source (string) — The source-side id whose mapping should go.

Returnstrue when a mapping was removed, false when there was none — never an error, because the everyday caller is cleaning up and does not know which. After this, the next upsertBySourceId for that source CREATES rather than updates: the arm is chosen on whether a mapping exists.

Example

if (deleteMapping("users", "crm-4711")) { log("forgot the old link — the next sync will create"); }

setMappings

Platform call — the run pauses here and continues with the result, and it does real network I/O

setMappings(namespace, pairs)

Stores many mappings in one call — the bulk form of setMapping, and the one to use inside a sync loop. The whole list lands together or not at all.

Parameters

  • namespace (string) — The namespace every pair goes into.
  • pairs (array) — A list of { source, target } pairs. A pair whose source is already mapped replaces that target, exactly as setMapping would.

Returns — Nothing. Either every pair is stored or, on a refusal, none is.

Example

setMappings("users", [
  { source: "crm-1", target: "hr-a" },
  { source: "crm-2", target: "hr-b" }
]);

getMappings

Platform call — the run pauses here and continues with the result, and it does real network I/O

getMappings(namespace, sources)

Looks up many source ids at once and answers their targets in the same order — one call for a whole page of records instead of one per record.

Parameters

  • namespace (string) — The namespace to look in.
  • sources (array) — The source-side ids, as a list of text entries.

Returns — A list aligned with sources: at each position the mapped target id, or null where that source was never mapped — so sources[2] answers at position 2, always.

Example

const targets = getMappings("users", ["crm-1", "crm-2", "crm-3"]);
log(targets.filter((t) => t === null).length + " still unmapped");

listMappings

Platform call — the run pauses here and continues with the result, and it does real network I/O

listMappings(namespace, page?)

Reads a namespace’s mappings back, ordered by source id — for reports and reconciliation.

Parameters

  • namespace (string) — The namespace to list.
  • page (object, optional){ offset, limit } — where to start and how many to answer. Left out, the platform still answers a bounded page rather than everything: walk with offsets when a namespace may be large. Never delete while you walk: a removed row moves every later one up, and the next page skips as many as you took out — collect what to forget, finish the walk, then delete.

Returns — A list of { source, target } pairs, ordered by source.

Example

const pairs = listMappings("users", { offset: 0, limit: 100 });
log("first page holds " + pairs.length + " mappings");

Datasets

dsPut

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsPut(dataset, key, value)

Stores one keyed row in a named dataset — the platform-side staging ground for bulk data. Writing an existing key replaces that row’s value.

Parameters

  • dataset (string) — Which dataset. A name starting run: is PRIVATE TO THIS RUN — two runs staging under "run:pulled" never see each other — and such datasets are cleaned up after the run ends; every other name is durable and shared by the environment’s runs.
  • key (string) — The row’s key — the record’s own id, usually.
  • value (any) — Any JSON value up to 1 MiB serialized. A dataset holds at most 500000 rows and 512 MiB of values; a write that would cross either cap is refused as DATASET_FULL and nothing of it lands.

Returns — Nothing.

Example

dsPut("run:pulled", "user-1", { name: "Ada", active: true });
log("staged");

dsPutMany

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsPutMany(dataset, rows)

Stores a whole page of keyed rows in one call — the write to use when records arrive by the hundred. The batch lands entirely or not at all.

Parameters

  • dataset (string) — Which dataset — run: names are this run’s own.
  • rows (array) — A list of { key, value } rows. A key that already exists replaces its row; a cap refusal (DATASET_FULL) rolls back the WHOLE batch, so a dataset never holds half a page.

Returns — Nothing.

Example

dsPutMany("run:pulled", [
  { key: "user-1", value: { name: "Ada" } },
  { key: "user-2", value: { name: "Bob" } }
]);

dsGet

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsGet(dataset, key)

Reads one row of a dataset back by its key.

Parameters

  • dataset (string) — Which dataset — run: names are this run’s own.
  • key (string) — The row’s key.

Returns — The row’s value, or undefined for a key the dataset does not hold. A stored null comes back as null — distinguishable from a missing row.

Example

const user = dsGet("run:pulled", "user-1");
log(user === undefined ? "not staged" : "staged");

dsGetMany

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsGetMany(dataset, keys)

Reads many rows by key in one call, answering only the rows that exist.

Parameters

  • dataset (string) — Which dataset — run: names are this run’s own.
  • keys (array) — The keys to read, as a list of text entries.

Returns — A list of { key, value } rows — the same shape dsPage answers — holding the rows that were FOUND, in the order their keys were asked for. A key with no row is simply absent, so compare lengths to find the misses.

Example

const rows = dsGetMany("run:pulled", ["user-1", "user-2", "user-9"]);
log(rows.length + " of 3 are staged");

dsPage

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsPage(dataset, options?)

Reads a slice of a dataset, ordered by key — the loop-friendly way to work through staged rows without holding them all at once.

Parameters

  • dataset (string) — Which dataset — run: names are this run’s own.
  • options (object, optional){ offset, limit, where }. where narrows to rows whose value at one dot-path equals one plain value — { path: "profile.active", equals: true } — with text, numbers, true/false and null each matching only their own kind, and equals: null also matching rows where the path is absent. Left out, the platform still answers a bounded page rather than everything.

Returns — A list of { key, value } rows, ordered by key.

Example

const active = dsPage("run:pulled", { where: { path: "active", equals: true }, limit: 100 });
log(active.length + " active users in the first page");

dsCount

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsCount(dataset)

Answers how many rows a dataset holds — a dataset that does not exist counts 0.

Parameters

  • dataset (string) — Which dataset — run: names are this run’s own.

Returns — The row count as a number.

Example

log("staged " + dsCount("run:pulled") + " rows so far");

dsClear

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsClear(dataset)

Removes a dataset entirely — its rows and its name. Clearing what does not exist is a no-op.

Parameters

  • dataset (string) — Which dataset — run: names are this run’s own.

Returns — Nothing.

Example

dsClear("run:pulled");
log("staging ground empty");

dsRename

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsRename(from, to)

Renames a dataset — and REPLACES whatever already held the new name, in one step. This is how a run promotes its staging set: rename run:current to a durable name and it outlives the run, standing where the previous baseline stood.

Parameters

  • from (string) — The dataset to rename. Renaming one that does not exist is refused.
  • to (string) — Its new name. A dataset already under this name is replaced — gone with the rename, which is the point of a promotion. A run: name makes it this run’s own again.

Returns — Nothing.

Example

dsPut("run:pulled", "user-1", { name: "Ada" });
dsRename("run:pulled", "baseline");
log("promoted — next run diffs against this");

dsDiff

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsDiff(base, current)

Counts what separates two datasets — how many rows were added, changed or removed in current relative to base. Computed on the platform’s side from fingerprints stored with every write, so it costs the same however wide the rows are.

Parameters

  • base (string) — The dataset to compare against — last sync’s baseline, usually.
  • current (string) — The dataset holding the fresh rows — run: names are this run’s own.

Returns{ added, changed, removed }, each a count: keys only in current, keys in both whose values differ, keys only in base. A value that merely reordered its object keys has NOT changed. A dataset that does not exist compares as empty.

Example

dsPut("run:pulled", "u1", { name: "Ada" });
const delta = dsDiff("baseline", "run:pulled");
log(delta.added + " new, " + delta.changed + " changed, " + delta.removed + " gone");

dsDiffKeys

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsDiffKeys(base, current, kind, page?)

Lists the keys behind one of dsDiff’s counts — the rows to actually act on, one class at a time, page by page.

Parameters

  • base (string) — The dataset to compare against.
  • current (string) — The dataset holding the fresh rows.
  • kind (string)"added", "changed" or "removed" — which class of keys to list.
  • page (object, optional){ offset, limit }. Left out, the platform still answers a bounded page rather than everything — walk with offsets when a delta may be large.

Returns — The keys of that class, ordered. Feed them to dsGetMany for the rows to upsert, or use the removed ones to retire what the source no longer has.

Example

dsPut("run:pulled", "u1", { name: "Ada" });
const fresh = dsDiffKeys("baseline", "run:pulled", "added", { limit: 100 });
log(fresh.length + " new records to create");

dsStats

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsStats(dataset, path)

Groups a dataset’s rows by the value at one dot-path and counts each group — a one-call report over staged data, computed on the platform’s side.

Parameters

  • dataset (string) — Which dataset — run: names are this run’s own.
  • path (string) — The dot-path to group by, such as "dept" or "profile.role".

Returns — A list of { group, count } entries, most frequent first. Each group is the value AS JSON TEXT — "active" with its quotes, 42, true — and rows where the path is absent group under "null" together with stored nulls.

Example

const groups = dsStats("run:pulled", "dept");
for (const g of groups) { log(g.group + ": " + g.count); }

dsMapInto

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsMapInto(source, mappingName, target, options?)

Maps every row of a staged dataset into another one, using a JSON mapping somebody configured — the whole set in one call, without the records ever passing through the script. The platform works through it a batch at a time, pausing the run in between, so a set of any size costs short segments rather than one long one.

Parameters

  • source (string) — The dataset to read — run: names are this run’s own.
  • mappingName (string) — The configured JSON mapping to apply to each record. It is the same mapping a script could call by name, so what its preview showed is what these rows become; a function that is not a JSON mapping is refused.
  • target (string) — The dataset the mapped records are written to.
  • options (object, optional){ keyPath, onError }. keyPath takes the new row’s key from the MAPPED record — the target system’s own identity — instead of keeping the source key. onError is "reject" (the default: the row goes to <target>.rejected with a reason, and the rest of the set still runs) or "fail" (the first bad row ends the call).

Returns{ rows, rejected } — how many records were written and how many could not be mapped. The rejected ones are in a dataset called <target>.rejected, keyed by the row that failed and carrying the reason, so dsPage shows exactly what to fix. That dataset is emptied each time the call starts.

Example

dsPut("run:staged", "u1", { id: "u1", name: "Ada" });
const done = dsMapInto("run:staged", "MyMapping", "run:ready");
log(done.rows + " ready, " + done.rejected + " rejected");

dsEnrich

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsEnrich(dataset, functionName, options)

Fills staged rows in from a second system: one configured call per row, its answer merged into the row where you say. The calls happen one after another — never at the same time — so an integration is not throttled or blocked for asking too fast, and if the far end says it is being called too often the run waits and continues with the row it was on.

Parameters

  • dataset (string) — The dataset to enrich; its rows are updated in place.
  • functionName (string) — The configured function to call for each row — usually the call that reads one record of the other system.
  • options (object){ argPath, param, into, onError }. argPath says where in the record the value to look up with is, and param which parameter it binds to (left out: the function’s first). into names the field the answer is written to. onError is "reject" (the default: the row moves to <dataset>.rejected), "null" (the row stays, with nothing at into) or "fail" (the first failed lookup ends the call).

Returns{ rows, rejected } — how many records were enriched and how many were not. For a call, the ANSWER is what the far end sent back; a status outside 200–299 counts as no answer, because a row has nothing to branch on.

Example

dsPut("run:staged", "u1", { key: "ABC-1" });
const done = dsEnrich("run:staged", "MyFunction", { argPath: "key", into: "issue" });
log(done.rows + " records enriched");

dsDuplicates

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsDuplicates(dataset, path, options?)

Finds the values that more than one record shares — the check to run BEFORE importing into a field the target system keeps unique. Ask it with the target field’s width and it compares what that field would actually store, so two names that differ today but collide at 40 characters are found here instead of halfway through an import.

Parameters

  • dataset (string) — The dataset to check — run: names are this run’s own.
  • path (string) — Which field to check, as a dot-path — "email", "profile.login". Records with nothing there are not a collision.
  • options (object, optional){ length } — compare only the first N characters, the width of the field you are importing into. Left out, whole values are compared.

Returns — How many colliding groups there are, as a number. The groups themselves land in a dataset called <dataset>.duplicates — the shared value, how many records carry it, and their keys — so dsPage shows what to correct. Nothing is changed in the data: which of two colliding records is the right one is a business decision, not the platform’s.

Example

dsPut("run:staged", "u1", { email: "ada@example.com" });
const clashes = dsDuplicates("run:staged", "email", { length: 40 });
log(clashes === 0 ? "safe to import" : clashes + " collisions to fix first");

dsToCsv

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsToCsv(dataset, mappingName, options?)

Writes every row of a staged dataset into CSV files, using a columns table somebody configured — capped in size the way a bulk import wants them, each file complete with its own header line. The platform works through the set a file at a time, pausing the run in between, so a dataset of any size costs short pieces of work rather than one long one.

Parameters

  • dataset (string) — The dataset to write — run: names are this run’s own.
  • mappingName (string) — The configured columns table to render each record with. It is the same one a script could call by name, so what its preview showed is what these files contain; a function that is not a columns table is refused.
  • options (object, optional){ maxBytes, maxLines, maxLineBytes, where }. The first three cap one file — 10 MB, 10 000 records and 10 KiB per line by default; set them to what the system you are uploading to accepts. where narrows to rows whose value at one dot-path equals one plain value — { path: "active", equals: true }. A record whose line is longer than the cap is not split: it goes to <dataset>.rejected with a reason, and the rest still go out.

Returns — A list of files, in order: { blobId, name, lines, bytes, firstKey, lastKey } each — the handle to upload, a suggested file name, how many records it carries, how big it is, and the first and last dataset key in it. The same list is left in a dataset called <dataset>.chunks, and anything that could not be written is in <dataset>.rejected, keyed by the row that failed; both are emptied each time the call starts.

Example

dsPut("run:ready", "u1", { id: "u1", name: "Ada" });
const files = dsToCsv("run:ready", "MyCsv", { maxLines: 5000 });
log(files.length + " files to upload, first is " + files[0].name);

dsFromCsv

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsFromCsv(file, into, mappingName?, options?)

Reads a stored CSV file into a dataset, one batch of records at a time, without the file ever passing through the script. This is how a file that arrived as a mail attachment or came back from a download becomes rows you can work with — a real export weighs far more than a script may hold, so reading its contents and parsing them yourself works on a sample and refuses on the file that matters.

Parameters

  • file (string) — The handle of the stored file to read.
  • into (string) — The dataset the records are written to.
  • mappingName (string, optional) — The configured columns table to shape each record with — it decides which column becomes which field and what type it is. Left out, the file’s first line names the fields and every value stays text. You can pass the options in its place when there is no table.
  • options (object, optional){ keyPath, onError, maxLineBytes }. keyPath says which field keys each row (left out, the record’s own id is used). onError is "reject" (the default: the record goes to <into>.rejected with the line it was on and a reason) or "fail" (the first bad record ends the call).

Returns{ rows, rejected } — how many records were staged and how many could not be. The rejected ones are in a dataset called <into>.rejected, keyed by the line they were on, so dsPage shows exactly what to fix; that dataset is emptied each time the call starts. A file whose row does not fit its own header ENDS the call naming the line, because everything after such a row would land in the wrong columns.

Example

const file = writeBlob(btoa("email,name\nada@example.com,Ada\n"), "text/csv");
const done = dsFromCsv(file, "run:incoming", { keyPath: "email" });
log(done.rows + " contacts staged, " + done.rejected + " rejected");

csvLineRecord

Platform call — the run pauses here and continues with the result, and it does real network I/O

csvLineRecord(file, line)

Turns a line number in a file this run wrote back into the record it came from. Bulk imports answer with line numbers — “line 3 was rejected” — and this is what lets a report name the customer or the order instead of a number nobody can act on. It counts lines the way the file really has them, so a value carrying a line break is accounted for.

Parameters

  • file (string) — The handle of a file dsToCsv wrote in this run.
  • line (number) — The line number the other system reported, counting from 1.

Returns{ key, dataset, record } — the dataset key of the record that wrote that line, the dataset it is staged in, and the record itself. Answers null for the header line, for a line past the end of the file, and for a file this run did not write, so a report can walk whatever line numbers it was given.

Example

dsPut("run:ready", "u1", { id: "u1", name: "Ada" });
const files = dsToCsv("run:ready", "MyCsv");
const failed = csvLineRecord(files[0].blobId, 2);
log(failed === null ? "no record wrote that line" : "line 2 is record " + failed.key);

upsertBySourceId

Platform call — the run pauses here and continues with the result, and it does real network I/O

upsertBySourceId(namespace, record, options)

Creates a record in the other system the first time it is seen and updates it every time after — the platform remembers which is which. It looks up the id mapping, calls the one of your two functions that fits, and writes the mapping back when a create succeeds, so the next run knows.

Parameters

  • namespace (string) — Which kind of thing this is — one namespace per remote entity type, such as "people". The same namespace the mapping calls use.
  • record (object) — The source record itself. Its own id identifies it (id, Id or ID, or the field sourceIdPath names), and that id is what the mapping is kept under.
  • options (object){ createFn, updateFn, idPath, sourceIdPath, namePath, recordParam, targetParam }. createFn and updateFn name the configured functions that do the work — the create gets the record, the update gets the record and the target id the platform remembered. idPath says where the create’s ANSWER carries the new id (left out: its own id). namePath says which field names the record in the report.

Returns{ action, outcome, target, reason } — whether it was a create or an update, whether it worked, and the id in the other system. A record that could not be synced answers outcome: "failed" with the reason rather than ending the run, and a row naming it lands in the report dataset either way.

Example

const person = { id: "src-1", name: "Ada Lovelace" };
const done = upsertBySourceId("people", person, { createFn: "MyFunction", updateFn: "MyFunction" });
log(done.action + " " + person.name + ": " + done.outcome);

Platform call — the run pauses here and continues with the result, and it does real network I/O

applyLinks(dataset, spec)

Writes the relationships between records that are already in the other system — who reports to whom, which department something belongs to. It resolves both ends of every link from the id mappings, a whole page at a time, so the only calls made are the link writes themselves.

Parameters

  • dataset (string) — The staged rows to read the links from — run: names are this run’s own.
  • spec (object){ linkFn, from, to, namePath }. linkFn names the configured function that writes ONE link. from and to each say { namespace, path, param } — which mappings that side lives in, where the row carries its id, and which parameter of your function takes the resolved id (left out: from and to).

Returns{ linked, failed }. A row whose other end is not in the target yet is not an error — it becomes a report row naming the record, and the link is written on the run that creates the missing side. A row with no link at all is skipped silently, because most records have none.

Example

dsPut("run:people", "p1", { id: "p1", name: "Ada", managerId: "p2" });
const spec = { linkFn: "MyFunction", from: { namespace: "people", path: "id" }, to: { namespace: "people", path: "managerId" } };
const done = applyLinks("run:people", spec);
log(done.linked + " links written, " + done.failed + " waiting for the other side");

stageDiff

Platform call — the run pauses here and continues with the result, and it does real network I/O

stageDiff(baseline, current, kinds, into)

Stages the RECORDS behind a comparison, ready for the phase that acts on them. dsDiff says how many rows differ and dsDiffKeys says which keys; this puts the rows themselves where you can walk them, taking a removed record from the baseline, which is the only side that still has it.

Parameters

  • baseline (string) — The dataset to compare against — last sync’s copy.
  • current (string) — The dataset holding the fresh rows.
  • kinds (array) — Which differences to stage: any of "added", "changed" and "removed".
  • into (string) — The family of datasets to stage into: each class lands in its own — "run:delta" writes run:delta.added, run:delta.changed and run:delta.removed. They are emptied each time the call starts.

Returns — How many rows each class staged, as { added, changed, removed } — only the classes that were asked for. A class that matched nothing answers zero, which is an answer to branch on rather than a missing key.

Example

const delta = stageDiff("baseline", "run:pulled", ["added", "changed"], "run:delta");
log(delta.added + " to create, " + delta.changed + " to update");

sendReport

Platform call — the run pauses here and continues with the result, and it does real network I/O

sendReport(channel, options)

Turns the report dataset into something a person reads — how many records went through, broken down by what was done and how it ended, then the ones that need a human, by name and with the reason. The counting happens where the rows are, so reporting on ten thousand records costs what ten costs.

Parameters

  • channel (string)"email" sends it as mail, through one of your email functions — the mailbox and its password are that function’s business. "request" sends it through an http function instead, which is how it reaches a ticketing system or a chat webhook.
  • options (object){ fn, dataset, title, limit, bodyParam }. fn names the configured function that sends it, and must match the channel. dataset is the report to render (left out: report). title is the heading. limit caps how many failures are named before the body starts counting instead (left out: 20). bodyParam is the parameter your function takes the markdown body as (left out: body).

Returns{ total, failed, listed } — how many records the report covered, how many failed, and how many of those the body named. A send that does not go through fails the run rather than being reported: the thing that would carry the bad news is the thing that broke.

Example

dsPut("report", "created:u1", { name: "Ada Lovelace", action: "created", outcome: "ok" });
const done = sendReport("request", { fn: "MyReport", title: "Nightly people sync" });
log(done.total + " records reported, " + done.failed + " need attention");

importAll

Platform call — the run pauses here and continues with the result, and it does real network I/O

importAll(files, importFn, options?)

Uploads the files dsToCsv produced to a bulk import API and waits for each one to finish — the waiting costs nothing, because the run is suspended rather than held. When the far end answers with line numbers, they are turned back into the records that wrote them, so the report names people instead of positions in a file.

Parameters

  • files (array) — The list dsToCsv answered — its handles and names, passed straight through.
  • importFn (string) — The configured function that uploads ONE file. It takes the file’s handle as its file parameter, and it is where the multipart form is authored.
  • options (object, optional){ statusFn, pollMs, maxPolls, fileParam, jobParam, jobIdPath, donePath, doneValues, errorsPath, linePath, reasonPath }. statusFn names the function that asks how an import is going — left out, an upload that answers is treated as finished. The paths say where that system’s answer carries the job id, its state and the lines it rejected; the defaults are the common spellings (id, status, errors, line, reason).

Returns{ chunks, failed, rejected } — files sent, files the far end would not take, and records it rejected by line. Each file gets a report row, and each rejected line becomes a row naming the record it came from. An import that never finishes is given up on after maxPolls asks and reported as such, because a run must not wait for ever.

Example

dsPut("run:ready", "u1", { id: "u1", name: "Ada" });
const files = dsToCsv("run:ready", "MyCsv");
const done = importAll(files, "MyFunction");
log(done.chunks + " files imported, " + done.rejected + " records rejected");

State

getState

Platform call — the run pauses here and continues with the result, and it does real network I/O

getState(key)

Reads a small durable value this environment stored earlier — a delta token, a watermark, a cursor. What setState put there is what comes back, across runs.

Parameters

  • key (string) — The key setState stored under.

Returns — The stored value, or undefined for a key that was never set. A stored null comes back as null — the two stay distinguishable, so a cleared marker and a missing one read differently.

Example

const watermark = getState("orders-watermark");
if (watermark === undefined) { log("first sync — start from the beginning"); }

setState

Platform call — the run pauses here and continues with the result, and it does real network I/O

setState(key, value)

Stores one small durable value under a key — the place for delta tokens, watermarks and cursors between runs. A later set replaces the value.

Parameters

  • key (string) — The key to store under.
  • value (any) — Any JSON value. State is for SMALL things: a value over 64 KiB as JSON is refused, naming the limit — pages of records belong in a dataset.

Returns — Nothing.

Example

setState("orders-watermark", { since: "2026-07-01T00:00:00Z" });
log("watermark moved");

Pagination

fetchPage

Platform call — the run pauses here and continues with the result, and it does real network I/O

fetchPage(functionName, arguments, cursor?)

Reads ONE page from a configured function that was set up to page, and tells you how to ask for the next one. The hand-written way through a collection — reach for dsFetchInto when the pages are only going into a dataset anyway.

Parameters

  • functionName (string) — The configured function to call. It must have a pagination block: without one it answers a single result, and paging it is refused rather than guessed at.
  • arguments (object) — The arguments that function declares, exactly as a bare call passes them. They are sent again for every page — a page is the same call, asked for a different slice.
  • cursor (any, optional) — Where to continue: what the previous page answered as next. Left out, the walk starts at the first page.

Returns{ items, next, delta }. items is this page’s records as a list. next is what to pass as the cursor for the page after this one — ABSENT when this was the last page, which is how the walk ends. delta is the provider’s sync token when the function’s block names where it lives, usually only on the last page; store it with setState if the next sync should ask for changes only.

Example

const page = fetchPage("MyPagedFunction", {});
log(page.items.length + " records on the first page");
if (page.next === undefined) { log("that was all of them"); }

dsFetchInto

Platform call — the run pauses here and continues with the result, and it does real network I/O

dsFetchInto(functionName, arguments, dataset, options?)

Pulls EVERY page of a configured function into a dataset — one call instead of a cursor loop. The platform fetches a page, stages it, and pauses the run between pages, so a pull of a hundred pages costs a hundred short segments instead of one run holding a worker; if the far end says it is being called too often, the run waits and continues from the page it was on rather than starting over.

Parameters

  • functionName (string) — The configured function to walk. It must have a pagination block.
  • arguments (object) — The arguments that function declares — sent again for every page.
  • dataset (string) — Where the records are staged. A run: name is private to this run and cleaned up after it — dsRename it to a durable name to keep it, which is how a pulled set becomes the next sync’s baseline.
  • options (object, optional){ keyPath } — which field of a record is its key in the dataset. Left out, the record’s own id is used: id, Id or ID, whichever it has. A record with none of them, and no keyPath to say otherwise, is refused rather than given an invented key.

Returns{ pages, rows, delta } once the whole collection is in: how many pages were fetched, how many records were staged, and the provider’s sync token when its block names one. The pull stops early — with what it has — when the function’s configured page limit is reached.

Example

const pull = dsFetchInto("MyPagedFunction", {}, "run:contacts");
log("staged " + pull.rows + " records from " + pull.pages + " pages");
dsRename("run:contacts", "contacts");

Run control

checkpoint

Platform call — the run pauses here and continues with the result

checkpoint(waitMs?)

Cuts the run into segments: the platform saves the run, frees the worker, and continues where it left off. Parked time costs no execution budget and each segment starts with a fresh one, so a long job survives limits that would otherwise end it. Everything the script has in scope is preserved across the cut.

Parameters

  • waitMs (number, optional) — How long to stay parked before continuing. Omitted or 0 means continue as soon as a worker is free. At most 24 hours — a longer wait is refused, because a run that wants to wake up next week is a schedule, not a pause.

Returns — Nothing.

Example

for (let page = 1; page <= 50; page++) {
  const response = fetch({ connection: "crm", method: "GET", path: "/contacts", query: { page: page } });
  log("page " + page + ": " + response.body.items.length);
  checkpoint(); // hand the worker back between pages
}

Config functions

MyFunction

Platform call — the run pauses here and continues with the result, and it does real network I/O

MyFunction(arguments?)

Every function configured in this environment is callable by its own name — MyFunction stands for whichever one you mean. What it describes is done for you: for a call, the connection and its credentials, retries, and the wait when the far end says it is being called too often; for a text template, the values filled into the words somebody else wrote; for a message, the mailbox it is sent from and the body written both as formatted mail and as plain text; for a mapping, which field of a record belongs in which column, and what each one holds; for a prompt to a language model, the connection that holds the API key, the model, and the same wait-and-continue when the model answers that it is being asked too often. Whatever it is, the script says only what it wants.

Parameters

  • arguments (object, optional) — One value per parameter the function declares. A parameter with a default may be left out; a name it does not declare is refused rather than ignored. A mapping is the exception: it declares no parameters and takes the ONE value it maps — a list of records for a CSV, an object (or a list of them) for a JSON mapping.

Returns — Whatever kind of function it is answers its own shape. A call answers { status, body }, with the body already read as data when the far end sent JSON — a status outside 200–299 comes back the same way, an answer to branch on rather than a failure. Unless whoever configured it said otherwise: a call may name the statuses it accepts, and then any other answer stops the run where the call was made, saying what the far end sent — so you write no status check. It may also say what its answer IS — the value at a path, or the one record found — and then that value is what you get, or null when the answer is that there is none. If you are unsure which a function does, test it: what the Test panel shows is exactly what a script receives. A text template answers the filled string, exactly as it was written: nothing is escaped, and markdown is not rendered. A message answers { ok: true, mode, messageId } once it has been handed to the mail server, exactly as sendMail does — and mode says what the mail account did with it, which is deliver unless somebody configured the account to redirect or only record its mail. A CSV mapping answers whichever direction it was given: hand it a list of records and it answers the CSV text, hand it that text and it answers the records back. A JSON mapping answers the structure it was built to produce, filled from the object — or the list of objects — you gave it. A prompt to a model answers { text, json, model, usage, stopReason }text is the answer, json is that answer parsed for you in JSON mode (and a reply that is not JSON fails loudly rather than handing you a broken value). Treat the model’s answer as untrusted input: it is text a model wrote, not a value the platform vouches for, so validate it before you act on it.

Example

const answer = MyFunction({ key: "ABC-1" });
if (answer.status === 200) {
  logInfo("got " + answer.body.title);
}
Rendered from docs/guide/function-reference.md in the product's own repository, at build time. Found a problem on this page? Write to the address in the footer.