Docs / CLI, MCP & files
a9script authoring pack
How a script is executed on this platform: the shape of its input, the name of every function it may call, the rules enforced while it runs, and the habits from general JS that do not work here.
Every example below was run with this input:
{
"event": {
"id": "evt_2f1c9a",
"kind": "webhook",
"tenantId": "6a1b0f6e-0000-4000-8000-000000000001",
"environmentId": "6a1b0f6e-0000-4000-8000-000000000002",
"endpoint": "orders",
"body": {
"id": "o-1042",
"customer": "ada@example.com",
"items": [
{
"sku": "A-1",
"price": 20
},
{
"sku": "B-2",
"price": 10
}
]
},
"headers": {
"content-type": "application/json",
"x-source": "shop"
},
"receivedAt": "2026-03-04T09:15:00.000Z"
},
"settings": {
"crmConnection": "crm",
"pageSize": 50,
"signingSecret": "shared-secret"
}
}
A script is a program, not a module
A script is a program. Its statements run once, from top to bottom, and when the last one finishes the run is over. There is no entry point: nothing calls main, nothing looks for a default export, and the platform never searches your code for a function to invoke.
There are no modules either. import and export are refused before the script runs, and require does not exist. Code you want to share lives in a library script: the platform puts it in front of yours before the run starts, so its functions and variables are simply in scope — no import statement, no namespace.
Everything else you would write in a program works. Declare functions and call them, use a wrapper function if you want a private scope — but neither is required, and wrapping the whole script in one buys nothing.
const order = input.event.body;
log("order received", { id: order.id });
function total(items) {
let sum = 0;
for (const item of items) {
sum += item.price;
}
return sum;
}
return { id: order.id, total: total(order.items) };
→ {“id”:“o-1042”,“total”:30}
What a script returns
A run has one result. Three rules decide it, in order:
- A top-level
returnwins — its value is the result, and the run stops there. - Otherwise, the final value of a top-level variable called
outputis the result. - Otherwise the run has no result, which is perfectly normal.
output only counts at the top level. A variable of that name inside a function is an ordinary local variable, and the platform never looks at it. The result becomes the run’s output: it is what the run detail shows, and — for a run triggered by a synchronous API call — what the caller can be answered with.
const output = "ignored";
return { ok: true };
→ {“ok”:true}
const output = { processed: 0 };
output.processed = input.event.body.items.length;
→ {“processed”:2}
function compute() {
const output = 42;
return output;
}
compute();
→ no result
log("nothing to report");
→ no result
input — the one global that carries data
input is the only global holding data. Its parts: input.event is what triggered the run; input.settings is the value the linked settings script returned — configuration as data, so environment-specific values never sit in the script (with no settings script linked it is undefined); and on a door that requires an identity, input.caller names the signed-in person — their id, email and roles, bound by the platform from the door’s own decision, absent everywhere else and never settable from any payload. Filter what you return by input.caller and you have an application backend.
input is frozen, at every depth. Assigning to any part of it throws a TypeError you can catch. Build what you need as a new value instead: an object literal for a few fields, JSON.parse(JSON.stringify(input.event.body)) for a deep copy.
What input.event carries depends on the door the run came through. These fields are always present: id, kind, tenantId, environmentId, body and receivedAt. kind is what to branch on when one script serves more than one door.
| Trigger | What causes it | input.event.body | Fields set |
|---|---|---|---|
webhook | An HTTP call arrived on one of the environment’s endpoints. The caller is answered immediately, before the script runs. | The request body — parsed when it is JSON, the raw text otherwise. | id, kind, tenantId, environmentId, endpoint, body, headers, receivedAt |
api_service | An HTTP call arrived on a synchronous endpoint, and the caller is waiting for this run’s answer — see respond. | The request body — parsed when it is JSON, the raw text otherwise. | id, kind, tenantId, environmentId, endpoint, body, headers, receivedAt |
scheduler | A schedule came due. Nothing else triggered it, so there is no caller and no payload from outside. | { schedule, cron, firedAt } — which schedule fired, its expression, and the minute it was due. | id, kind, tenantId, environmentId, body, receivedAt |
email_in | A message arrived in a mailbox the environment watches. One run per message. | The message envelope — sender, recipients, subject, the text and/or HTML body and the headers. A body too large to read whole arrives cut (or, rarely, without it) and the envelope carries truncated: true. Attachments are not delivered. | id, kind, tenantId, environmentId, endpoint, body, receivedAt |
system | Something went wrong in this environment — a run failed, queued work could not be delivered, something was marked as failing, or a lane began to fill, stall, refuse, wait or skip a schedule and later cleared. The script reacts to it. | The occurrence: { kind, tenantId, environmentId, scriptName?, entity?, runId?, taskId?, error?, triggeredBy?, metric?, lane?, at } — what broke and where, never the payload that caused it. A configured function’s failure names it in entity and carries no scriptName; a lane occurrence names its lane in lane (source, on for entered or cleared, its numbers) and no script. | id, kind, tenantId, environmentId, body, receivedAt |
manual | Someone ran the script on demand — from the editor’s Test button or from the command line. | Whatever input was supplied for the test run. | id, kind, tenantId, environmentId, body, receivedAt |
const order = input.event.body;
return order.customer;
→ “ada@example.com”
try {
input.event.body.id = "changed";
} catch (error) {
return error.name + ": " + error.message;
}
→ “TypeError: cannot mutate the read-only input”
const order = input.event.body;
return { id: order.id, customer: order.customer, status: "processed" };
→ {“id”:“o-1042”,“customer”:“ada@example.com”,“status”:“processed”}
Calling the platform
Everything the platform offers — logging, HTTP calls, checkpoints, and the functions configured for your tenant — is called by its bare name and answers on the spot. There is no await and no .then, and nothing to hand a callback to: the language has no async, and Promise is not defined. (Callbacks themselves are ordinary — map and sort take one; it is the PLATFORM that never asks for one.)
That is not a simplification of something asynchronous. A platform call really does pause the run: the platform saves the run’s entire state, does the work, and continues your script with the result filled in. A run that is resumed — on another worker, an hour later — picks up the saved result instead of calling again, which is why a retried run never sends the same request twice.
You can call the platform from anywhere, a map or sort callback included. Prefer a plain loop when the list is not small: the calls happen one at a time either way, and only a loop can break — when you have enough, when an answer is bad, when the list turns out longer than you expected. The one way out of a callback is throw, which abandons the whole iteration.
What each function does — its arguments, its result, an example — is in the function reference. This page only fixes how calling works.
The results below come from running these examples against a connection that answers 200.
const response = fetch({
connection: input.settings.crmConnection,
method: "GET",
path: "/contacts",
query: { pageSize: input.settings.pageSize }
});
log("contacts fetched", { count: response.body.items.length });
return "status " + response.status;
→ “status 200”
const items = input.event.body.items;
const skus = [];
for (const item of items) {
log("item", { sku: item.sku });
skus.push(item.sku);
}
return skus;
→ [“A-1”,“B-2”]
log("first batch done");
checkpoint();
log("this line runs in the next segment");
→ the run is saved here and continues later
When a call does not simply succeed
A call that fails is not always over. The platform can make it again on its own, and for one kind of answer it can put your whole run to sleep and come back to it — two different things, worth telling apart before you configure either.
What follows is what the platform does; a configured call can change its share of it, and a script’s own fetch gets exactly these defaults.
Repeating — the platform makes the same call again, moments apart, while your run waits. It repeats when no answer arrived at all (the connection failed, timed out, or was refused) and when the answer means not now:
408,425,429— the far end asking to be called again.- Any 5xx except
501,505, which say the request itself will never work — for a call that is safe to repeat (below).
By default it repeats 3 times — so up to 4 calls in all — waiting longer before each one, and honouring a Retry-After the far end sends. Everything else is an ANSWER, and you decide what it means: 400, 401, 404, 422 are never repeated, and neither is any other 4xx, because asking again cannot change them.
Which calls repeat on a 5xx depends on what the call DOES. A call whose method promises nothing happens twice — GET, HEAD, OPTIONS, PUT, DELETE — or one you mark idempotent is repeated on a 5xx like any other transient answer. A call that changes something (a POST or a PATCH you have not marked) is repeated only on 408, 425, 429, never on a 5xx: the far end may have done the work before it failed to answer, and doing it twice is worse than telling you. Name a status under retry.status when you know the far end rolls back on it. The same line runs through a failed connection: one that never reached the far end at all — refused, unreachable, a name that did not resolve, a certificate the platform will not trust — is repeated for every call, while a connection that died after the request may have gone out is repeated only for a call that is safe to repeat.
Waiting is the other thing, and it is not the same: on a status you name as a long wait, the run is SUSPENDED for as long as the far end asked — its worker released, so the wait costs nothing — and the call is made again when it wakes. A status you have named that way is never repeated on the spot; waiting wins, because repeating would spend the wait holding a worker.
One wait is cut off at 24 hours, whatever the far end asks for and whatever you configure. The longest honest answer a business system gives is “come back after the daily quota resets”, and waiting that out is exactly what this is for; past a day the far end is not busy, it is broken, and a run parked on it should end loudly enough that somebody looks. A longer wait is taken as 24 hours and the run’s log says both what was asked for and what was taken. The same ceiling holds a script’s own checkpoint, which refuses a longer wait instead: waking a day later than you asked would do the work at the wrong time. A run that wants to wake up next week is a schedule, not a wait.
Two budgets bound every call. The one you can set is about SILENCE: how long a call may go without receiving any data before it is cut off — 5 minutes unless you say otherwise. It is not a limit on how long the call may take, so a large download over a slow link finishes as long as the bytes keep coming. Above it the platform holds an absolute limit of 60 minutes on one call and all its repeats together, which nothing you configure can lengthen.
A configured call can override any of this — how many times to repeat, statuses to add, statuses never to repeat, and the silence budget. What it cannot do is exceed the 60 minutes limit or ask for more than 10 repeats.
What a configured call answers
The functions configured for your tenant are the calls you should be making: someone chose the connection, the credential and the wording, and can change them without touching your script. What they can also choose is how much of the answer you have to deal with.
By default a call hands you both halves of what came back: { status, body }, with the body already read as data when the far end sent JSON. A status outside 200–299 arrives the same way — it is an answer to branch on, not a failure.
Whoever configures the call can narrow that, and most well-configured calls do:
- The statuses it accepts. Any other answer stops the run where the call was made, saying what came back — the status, what the far end sent, and how many times the platform had already asked. Write no status check: if the next line runs, the call was answered.
- What the answer IS. The value at a path inside the body, or the one record found in a collection. That value is then what you get — not an envelope around it. Several records where one was asked for stops the run rather than picking one, because which is right is a business question.
- What nothing means. A search that matches nothing, a status that says the record is not there (
404,410), or anullwhere the value should be — a far end’s way of saying it made no record — can be an ordinarynullyou branch on, or it can stop the run, naming what was searched for. Stopping is what happens unless you say otherwise, so a create that quietly made nothing is never handed on as a value. Which one is a decision made in the configuration, not in your script.
So a call configured this way reads as the thing it fetches, and the code around it is business — the example below runs against exactly such a function.
You cannot tell from the call site which of these a function does — that is the point, and it is why the same call can be made stricter later without touching a script. Test the function and read what comes back: the Test panel shows exactly what a script receives.
const subject = findServiceRequest({ key: "SR-1" });
return subject;
→ “Login is broken”
A call, once answered, is never made again
The platform saves a run’s state at every call it makes, every time it pauses, and at short intervals while it computes in between. A run that is interrupted — by a restart, by a pause you asked for, by a wait on a busy system — continues from its last saved point, on any machine. It does not start over, and it does not repeat a call whose answer was recorded: the continuation reads the recording. What it may do again is the pure computation since that point — work that had reached nothing outside the run.
So the ordinary tools work. new Date() gives you the current moment, Date.now() the same moment as a number, Math.random() a random one. Read the clock twice and you get two readings, seconds apart if that is how long the work took. What you read is saved with the run at its next call or pause: a value read before a pause is still that value after it, however long the pause lasted.
input.event.receivedAt is still there and still useful — it is when the run’s trigger arrived, which is not the same thing as now, and for a scheduled run that was caught up late it can be well in the past.
What a script cannot see is the MACHINE it is running on. A date-and-time string must carry a timezone (2026-03-04T09:15:00Z), dates are UTC, and localeCompare takes the locale you want as an argument. Without that, the same script would mean different things on different nodes — and which node runs it is not something you chose.
A few constructs are excluded from the language, because they hide control flow the platform has to be able to save. They are refused before the script runs, with the line and column:
class · this · super · new.target · async/await · generators · import/export · tagged templates · getters/setters · with · eval · debugger · Function · directives · destructuring defaults · sparse arrays · computed object keys
const startedAt = Date.now();
const arrivedAt = new Date(input.event.receivedAt);
return { waitedMs: startedAt - arrivedAt.getTime() >= 0, year: arrivedAt.getUTCFullYear() };
→ {“waitedMs”:true,“year”:2026}
return ["Öl", "Zebra"].sort((a, b) => a.localeCompare(b, "sv")).join(",");
→ “Zebra,Öl”
const skus = input.event.body.items.map((item) => item.sku);
return { skus, joined: skus.join(", "), count: skus.length };
→ {“skus”:[“A-1”,“B-2”],“joined”:“A-1, B-2”,“count”:2}
Errors, and what catches them
throw ends the run and marks it failed. The error’s name, message and the line it came from are recorded with the run.
try/catch catches what you would expect — TypeError, ReferenceError, RangeError, and anything you throw yourself — and it also catches failed platform calls. A call that could not be made surfaces as a catchable error named PlatformError, carrying the platform’s own reason in its message. When the fault is the platform’s own rather than the call’s, that reason is an internal platform error (ref 1a2b3c4d): quote the ref to your operator, whose log holds what happened. A call that reached the other side and got an unhappy answer does not throw at all: an HTTP call returns its status for you to branch on.
What try/catch never catches is the run’s budget. When a run is out of steps or objects, or out of time where it cannot park, the platform ends it — there is nothing left to catch, and that is deliberate: a script must not be able to trap its own kill switch.
A run that fails ends with one of these, recorded on the run:
PARSE_ERROR— The script could not be read as a program. The line recorded with the run says where, unless the script nests too deeply to read at all.UNSUPPORTED_SYNTAX— The script used something the language excludes. The message names it; the line recorded with the run says where.UNCAUGHT_EXCEPTION— Something was thrown and nothing caught it — including your ownthrow.EXECUTION_LIMIT— The run used up its steps, which count over its whole life, or — in a run answering an operation or a tool call, or one stepped by hand — its segment’s time. A new segment brings fresh time, never fresh steps: move per-item work into a bulk call or a dataset, or split it across runs.HEAP_LIMIT— The run created more objects than one run may over its whole life. Every object, array, function and Map counts, including those a call answered with and those the run’s input brought — the message says how many — and none is given back, so batching inside the run does not help. Keep large data in a dataset or a file, or split the work across runs.SIZE_LIMIT— Something the run held was too large to save between steps: the variables of one function call, one array, object or Map, or the result. Keep large text in a file or a dataset and hold its name, not its contents.EXTERNAL_TIMEOUT— A platform call did not answer in time.EXTERNAL_FAILED— A platform call failed and nothing caught it. Wrap the call intry/catchto handle it yourself.RETRY_NOT_PERSISTED— The script asked to be paused in a run that cannot be saved and resumed.INTERNAL— The platform itself failed. Nothing in the script can cause or prevent this one. When the message carries a ref, quote it to your operator, whose log holds what happened.
try {
const missing = null;
return missing.id;
} catch (error) {
return error.name + ": " + error.message;
}
→ “TypeError: cannot read properties of null (reading ‘id’)”
try {
fetch({ connection: "unconfigured", method: "GET", path: "/contacts" });
} catch (error) {
logError("the call could not be made", { reason: error.message });
return error.name;
}
→ “PlatformError”
throw new Error("order " + input.event.body.id + " has no shipping address");
→ fails: UNCAUGHT_EXCEPTION — Error: order o-1042 has no shipping address
The result, and what JSON allows
A script’s result becomes the run’s output. A run that produces no result at all is not a failure; it simply has no output.
For a run triggered by a synchronous API call, respond(value) answers the caller. It can be called once, the run keeps going afterwards, and the run’s own result is still its output. A run triggered any other way may call respond too — nobody is listening, and nothing breaks.
The output is stored as JSON, so the usual JSON rules apply at that edge: properties whose value is undefined are dropped, and there is no form for a bigint or for an object that refers to itself. A result the platform cannot represent fails the run rather than being silently trimmed, and says which of these it hit:
cyclic— The result refers to itself, directly or through a chain of properties.bigint— A bigint has no JSON form. Convert it to a string or a number first.undefined— The valueundefinedhas no JSON form, so it cannot be stored as an output.unrepresentable— Part of the result has no JSON form — a function, for instance.
return JSON.stringify({ id: input.event.body.id, note: undefined });
→ ”{“id”:“o-1042”}”
respond({ received: input.event.body.id });
return "answered";
→ “answered”
Budgets
Every run is bounded, so nothing can run away. Time is budgeted per segment: when it is up the run parks and continues in a new segment, and a script can be given a segment of its own — longer or shorter — up to a ceiling the deployment sets. Steps and objects are counted over the whole run — a new segment, a checkpoint included, brings fresh time but never fresh steps or objects — so a run is long by waiting on its calls, not by computing. These are the defaults a script gets when it asks for nothing:
- Time for one segment of a run: 60000 ms — waiting on calls counts too; when it is up the run parks and continues in a new segment, while a run answering an operation or a tool call, or one stepped by hand, ends with
EXECUTION_LIMIT. - Steps one run may take over its whole life: 5000000 — a new segment brings fresh time, never fresh steps; past them the run ends with
EXECUTION_LIMIT. - Objects one run may create over its whole life: 100000 — every object, array, function and Map counts, every one a call answers with and every one the run’s input brought; none is given back, and past them the run ends with
HEAP_LIMIT. - Frames one run’s stack may hold: 40000 — each call nests a few; the call past them throws
RangeError: Maximum call stack size exceeded, whichtry/catchcatches.
What a script may call
Everything below is called by bare name. A call marked pauses does real work outside the script: the run is saved, and continues on the next line with the result. What each one does, its arguments and an example live in the part named beside each group — ask for that part when you are about to write the call.
Core — the functions-core part
log(message, data?)— pauseslogInfo(message, data?)— pauseslogError(message, data?)— pauseslogWarning(message, data?)— pauseslogVerbose(message, data?)— pauseslogDebug(message, data?)— pausesrespond(value)— pausesstop()— pausesecho(value)— pausesfetch(request)— pausesinput— read, not calledinput.event— read, not calledinput.settings— read, not called
Encoding — the functions-encoding part
base64Encode(text)— pausesbase64Decode(text)— pauseshexEncode(text)— pauseshexDecode(text)— pauses
Crypto — the functions-crypto part
uuid()— pausesrandomHex(bytes?)— pausessha256(text)— pauseshmacSha256(key, message)— pauses
Text — the functions-text part
slugify(text)— immediatetemplate(text, values)— immediate
Numbers — the functions-number part
formatNumber(value, options?)— pausesformatCurrency(value, currency, options?)— pauses
Structured data — the functions-structured part
get(data, path)— immediateset(data, path, value)— immediatejsonPath(data, expression)— immediateparseCsv(text, options?)— pausestoCsv(rows, options?)— pausesparseXml(text, options?)— pausestoXml(value, options?)— pauses
Documents — the functions-document part
createPdf(document)— pauses
Markdown — the functions-markdown part
markdownToHtml(markdown, options?)— pausessanitizeHtml(html, options?)— pausesescapeHtml(text)— pausesstripMarkdown(markdown)— pausesmarkdownToPdf(markdown, options?)— pauses
Date & time — the functions-datetime part
formatDate(date, pattern)— immediateaddDays(date, days)— immediatedateDiff(from, to)— immediatenow()— pausestoday()— pausesdateAdd(date, amount, unit, timeZone?)— pausesdateSubtract(date, amount, unit, timeZone?)— pausesdateDifference(from, to, unit, timeZone?)— pausesdateIsBefore(date, other, unit?, timeZone?)— pausesdateIsAfter(date, other, unit?, timeZone?)— pausesdateIsEqual(date, other, unit?, timeZone?)— pausesdateIsBetween(date, start, end)— pausesdateStartOf(date, unit, timeZone?)— pausesdateEndOf(date, unit, timeZone?)— pausesdateParse(text, pattern, timeZone?)— pausesdateFormat(date, pattern, timeZone?, locale?)— pausesdateGet(date, unit, timeZone?)— pausesdateSet(date, unit, value, timeZone?)— pausesisWeekend(date, timeZone?)— pausesisLeapYear(date, timeZone?)— pausesdaysInMonth(date, timeZone?)— pauses
Email — the functions-email part
sendMail(message)— pauses
Files — the functions-blob part
writeBlob(contentBase64, mime?)— pausesreadBlob(blobId)— pausesreadFile(path, options?)— pauseswriteFile(path, contentBase64, options?)— pauses
Mapping — the functions-mapping part
setMapping(namespace, source, target)— pausesgetMapping(namespace, source)— pausesdeleteMapping(namespace, source)— pausessetMappings(namespace, pairs)— pausesgetMappings(namespace, sources)— pauseslistMappings(namespace, page?)— pauses
Datasets — the functions-dataset part
dsPut(dataset, key, value)— pausesdsPutMany(dataset, rows)— pausesdsGet(dataset, key)— pausesdsGetMany(dataset, keys)— pausesdsPage(dataset, options?)— pausesdsCount(dataset)— pausesdsClear(dataset)— pausesdsRename(from, to)— pausesdsDiff(base, current)— pausesdsDiffKeys(base, current, kind, page?)— pausesdsStats(dataset, path)— pausesdsMapInto(source, mappingName, target, options?)— pausesdsEnrich(dataset, functionName, options)— pausesdsDuplicates(dataset, path, options?)— pausesdsToCsv(dataset, mappingName, options?)— pausesdsFromCsv(file, into, mappingName?, options?)— pausescsvLineRecord(file, line)— pausesupsertBySourceId(namespace, record, options)— pausesapplyLinks(dataset, spec)— pausesstageDiff(baseline, current, kinds, into)— pausessendReport(channel, options)— pausesimportAll(files, importFn, options?)— pauses
State — the functions-state part
getState(key)— pausessetState(key, value)— pauses
Resources — the functions-resource part
readResource(name)— pauses
Pagination — the functions-pagination part
fetchPage(functionName, arguments, cursor?)— pausesdsFetchInto(functionName, arguments, dataset, options?)— pauses
Run control — the functions-control part
checkpoint(waitMs?)— pauses
Config functions — the functions-functions part
MyFunction(arguments?)— pauses
Rules for calling the platform
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.
Never:
const contacts = input.event.body.items.map((item) =>
fetch({ connection: "crm", method: "GET", path: "/contacts/" + item.sku }),
);
return contacts.length;
Instead:
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.
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.
Never:
const file = writeBlob(btoa("email\nada@example.com\n"), "text/csv");
return parseCsv(atob(readBlob(file).contentBase64)).length;
Instead:
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.
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.
Never:
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;
Instead:
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.
Never:
const contacts = fetch({ connection: "crm", method: "GET", path: "/contacts" }).then(function (response) {
return response.body.items;
});
return contacts;
Instead:
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.