Docs / The language & scripts
a9script authoring pack — mistakes that do not work here
Each is a habit from general JS, the exact error it produces, and the shape to write instead. Some are refused outright; some run and simply cost more than anyone intended, which the note says.
Exporting an entry point
That the platform imports the script and calls a main function.
Never:
export function main(input) {
return { ok: true };
}
You get: PARSE_ERROR — parse error: 'import' and 'export' may appear only with 'sourceType: "module"' (1:0)
Instead:
return { ok: true };
Why: A script is a program, not a module. Nothing imports it and nothing calls it — its statements are the run.
Assigning to module.exports
That the script is a CommonJS module whose export is the handler.
Never:
module.exports = function (input) {
return { ok: true };
};
You get: UNCAUGHT_EXCEPTION — ReferenceError: module is not defined
Instead:
const order = input.event.body;
return { id: order.id };
Why: There is no module system and no wrapper around your code — module is simply a name nothing defines.
Awaiting a platform call
That a call which does I/O must return a promise.
Never:
const response = await fetch({ connection: "crm", method: "GET", path: "/contacts" });
return response.status;
You get: PARSE_ERROR — parse error: 'await' is only allowed within async functions and at the top levels of modules. (1:17)
Instead:
const response = fetch({ connection: "crm", method: "GET", path: "/contacts" });
return response.status;
Why: The pause happens beneath the call, not in the script: the platform saves the whole run and continues it with the result already in place.
Declaring an async function
That work involving I/O has to be wrapped in an async function.
Never:
async function loadContacts() {
return fetch({ connection: "crm", method: "GET", path: "/contacts" });
}
return loadContacts();
You get: UNSUPPORTED_SYNTAX — unsupported syntax: async function at 1:0
Instead:
function loadContacts() {
return fetch({ connection: "crm", method: "GET", path: "/contacts" });
}
return loadContacts().status;
Why: There is nothing asynchronous to express: an ordinary function that calls the platform already does the whole job.
Building a promise
That values arriving later are wrapped in promises.
Never:
return new Promise(function (resolve) {
resolve(fetch({ connection: "crm", method: "GET", path: "/contacts" }));
});
You get: UNCAUGHT_EXCEPTION — ReferenceError: Promise is not defined
Instead:
return fetch({ connection: "crm", method: "GET", path: "/contacts" }).body;
Why: Nothing in a script arrives later — a call returns its value on the next line, so there is no promise to build.
Chaining .then
That a call’s result has to be unwrapped before it can be used.
Never:
const contacts = fetch({ connection: "crm", method: "GET", path: "/contacts" }).then(function (response) {
return response.body.items;
});
return contacts;
You get: UNCAUGHT_EXCEPTION — TypeError: fetch(...).then is not a function
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.
Requiring a library
That an HTTP client — or anything else — is pulled in with require or an import.
Never:
const axios = require("axios");
return axios.get("https://crm.example.com/contacts");
You get: UNCAUGHT_EXCEPTION — ReferenceError: require is not defined
Instead:
const response = fetch({ connection: "crm", method: "GET", path: "/contacts" });
return response.body;
Why: There is no package manager and nothing to import. Platform functions are in scope by name — fetch is the one HTTP client, and it brings the connection’s credentials, retries and logging with it — and shared code comes from a library script the platform puts in front of yours.
Reading configuration from the environment
That secrets and settings live in environment variables.
Never:
return process.env.CRM_TOKEN;
You get: UNCAUGHT_EXCEPTION — ReferenceError: process is not defined
Instead:
return input.settings.crmConnection;
Why: A script never handles a credential: settings carry configuration, and a connection carries its own secret, which the platform applies when the call is made.
Waiting with setTimeout
That waiting means scheduling a callback.
Never:
setTimeout(function () {
log("an hour later");
}, 3600000);
You get: UNCAUGHT_EXCEPTION — ReferenceError: setTimeout is not defined
Instead:
checkpoint(3600000);
log("an hour later");
Why: A wait is not a callback here: checkpoint saves the run, frees the worker, and continues the very next line when the wait is over — even across a restart.
Logging to the console
That output goes to standard output.
Never:
console.log("order received");
You get: UNCAUGHT_EXCEPTION — ReferenceError: console is not defined
Instead:
log("order received", { id: input.event.body.id });
Why: A run’s log is part of the run: lines are stored with it, levelled, searchable, and kept with the evidence of what happened.
Tagging a template with a function
That a function can be called by putting a template after its name.
Never:
return slugify`order ${input.event.body.id}`;
You get: UNSUPPORTED_SYNTAX — unsupported syntax: TaggedTemplateExpression at 1:7
Instead:
return slugify(`order ${input.event.body.id}`);
Why: Backtick strings themselves work, holes included. What is not here is the tag form, where a template becomes an argument list for the function in front of it — write an ordinary call instead.
Declaring a class
That data is modelled with classes.
Never:
class Order {
constructor(id) {
this.id = id;
}
}
return new Order("o-1").id;
You get: UNSUPPORTED_SYNTAX — unsupported syntax: ClassDeclaration at 1:0
Instead:
function order(id) {
return { id: id };
}
return order(input.event.body.id).id;
Why: Classes and this are excluded — the platform saves a run’s whole state between steps, and plain objects and functions are what it can save faithfully.
Putting an object into a string
That an object turns itself into text when concatenated.
Never:
return "order: " + input.event.body;
You get: UNCAUGHT_EXCEPTION — UnsupportedOperation: operator + is not supported on objects — convert explicitly, e.g. with JSON.stringify
Instead:
return "order: " + JSON.stringify(input.event.body);
Why: Silently turning an object into [object Object] is how useless log lines happen. Say what you meant instead.
Changing the trigger
That the incoming payload is a working copy.
Never:
input.event.body.status = "processed";
return input.event.body;
You get: UNCAUGHT_EXCEPTION — TypeError: cannot mutate the read-only input
Instead:
const order = { id: input.event.body.id, status: "processed" };
return order;
Why: The trigger is the run’s evidence of what it was asked to do, and a resumed run must read exactly what it started with.
Calling the platform inside a callback
That map is a way to make one call per item without thinking about the calls.
Avoid:
const contacts = input.event.body.items.map((item) =>
fetch({ connection: "crm", method: "GET", path: "/contacts/" + item.sku }),
);
return contacts.length;
This runs, and it costs: It works, and it is one call per item — in order, one after another, never at the same time. Ten items is fine. A thousand is a thousand calls, and if the far end starts refusing them the run parks and resumes once per call, which can take hours. Nothing warns you: the shape that costs an hour looks exactly like the shape that costs a second. And when the per-item work is only staging rows, there is no loop to pay for at all: dsPutMany stores the whole list in one call, dsGetMany reads many keys back in one, dsFetchInto reads every page of a source into a dataset without a loop at all, and dsMapInto/dsEnrich reshape or fill in a whole staged set the same way.
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.
Summarising a dataset by reading it into the script
That counting staged rows means walking them and keeping a tally.
Avoid:
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;
This runs, and it costs: It works, and on a few hundred rows it is fine. On a real staged set it pulls the whole set into the run a page at a time to produce three numbers — every row read, counted and thrown away. dsStats counts in one query without moving a row, dsCount answers a total the same way, and sendReport turns the whole report dataset into a readable summary without a script seeing a single row.
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.
Reading a file into the script to parse it
That a stored CSV is read into the script and parsed there.
Avoid:
const file = writeBlob(btoa("email\nada@example.com\n"), "text/csv");
return parseCsv(atob(readBlob(file).contentBase64)).length;
This runs, and it costs: It works on a sample and refuses on the real thing: a script may hold a few hundred kilobytes, and readBlob answers BLOB_TOO_LARGE for an export measured in megabytes. dsFromCsv reads the file where it is stored, a batch at a time, into a dataset.
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.
The rest of this pack
The page above is everything that is true only HERE. Ask for these parts by name for the rest:
- model — how a script is executed, the shape of its input, the rules enforced while it runs, and the mistakes that do not work here.
- functions-core — the core functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-encoding — the encoding functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-crypto — the crypto functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-text — the text functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-number — the numbers functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-structured — the structured data functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-document — the documents functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-markdown — the markdown functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-datetime — the date & time functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-email — the email functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-blob — the files functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-mapping — the mapping functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-dataset — the datasets functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-state — the state functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-pagination — the pagination functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-control — the run control functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- functions-functions — the config functions — what each one does, its arguments, whether it pauses the run, and a line of real script.
- standard-library — every built-in the language itself provides —
JSON,Math,Object,String,Array,Number,Map,Set,Date,RegExpand the bare globals — one line each, with an example and its answer.