a9script

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, RegExp and the bare globals — one line each, with an example and its answer.
Rendered from docs/guide/authoring-pack-mistakes.md in the product's own repository, at build time. Found a problem on this page? Write to the address in the footer.