Docs / CLI, MCP & files
Scripts — mains, libraries and the settings script
Three of the kinds you write hold code, and only one of them runs on its own.
| kind | what it is | runs? |
|---|---|---|
script_main | the integration itself — what a trigger fires | yes |
script_library | shared functions a main uses | only as part of a main |
script_settings | this environment’s configuration, as data | once, before the main |
A main script does not import anything. It links the libraries and the settings script it wants, and everything they declare is simply in scope.
Linking
Links are written by name in the main script’s file, in ONE ordered list:
{
"kind": "script_main",
"name": "on-order",
"displayName": "On order",
"codeFile": "on-order.js",
"links": [
{ "kind": "script_settings", "name": "crm-settings" },
{ "kind": "script_library", "name": "formatting" }
]
}
- The order is the assembly order. Libraries are placed ahead of the main in the order you list them, so a library may use what an earlier one declared — and the last declaration of a name wins.
- A main links AT MOST ONE settings script. Linking is what makes the settings apply to that main; a settings script nothing links runs for nobody. A second link is refused — to switch, list only the new one: the old link goes before the new one comes.
- Config functions are NOT linked. A script simply calls one by its name.
Whether the environment configures that name is answered when the run makes
the call — and by the warnings
apply --dry-rungives you before that. - A library link is a relation, so the same library serves any number of mains.
Two consequences worth remembering: a library’s top-level names share ONE scope with the main, so a library function named exactly like a config function shadows the call — the script reaches the library and the configured function is silently never used (the lint warns about this; do not ignore it). And a name a library declares can be overwritten by the main declaring it again, with no error.
The settings script
A settings script exists so that the values that differ between environments — a connection name, a page size, an account id, a shared secret — sit in a file of their own rather than inside the integration.
It must produce a value. Same rule as a main: return it, or leave it in a
top-level variable named output. A settings script that only assigns to a
local name configures nothing:
return {
crmConnection: "crm",
pageSize: 50,
signingSecret: "shared-secret"
};
Settings are data. The settings script runs before the main and reaches nothing at all — no calls, no logging, no storage. Calling something fails the whole run before a line of the main executes, and the failure names the settings script, not the main. Compute; do not fetch. The value must be JSON: a function, a Map, a bigint or a cycle in it fails the run the same way, naming its path.
The main reads it as input.settings:
const size = input.settings.pageSize;
return { pageSize: size };
With no settings script linked, input.settings is undefined — so a main
that reads input.settings.pageSize without one fails on the second dot, not
with a helpful message. If a main needs settings, link them.
Resources — text a script reads by name
Text a script takes in that is neither an event nor a setting — the brief a
reply follows, a model’s instructions, a code list — is a resource entity:
{ "kind": "resource", "name": "support-brief", "config": { "contentType": "text/markdown" }, "bodyFile": "support-brief.md" }
A script reads it with readResource("support-brief") — the text, whole;
JSON.parse(readResource("country-codes")) for JSON. It is NOT linked: like a
config function it is found by name when the run reads it, read afresh each
time (an activated change reaches the next read; a value read before a pause
stays). A name the environment lacks throws RESOURCE_NOT_FOUND. At most
64 KiB a resource — the limits page has every bound; larger reference data
belongs in a dataset or a file store. No secrets: a resource is ordinary text.
input — the only global holding data
input has exactly two parts:
input.event— what triggered the run. Always carriesid,kind,tenantId,environmentId,bodyandreceivedAt;kindis what to branch on when one script serves more than one door.input.settings— the value the linked settings script returned.
input is frozen at every depth: assigning to any part of it throws a
catchable TypeError. Build a new value instead.
For the exact input.event shape of each kind of trigger, and everything a
script may call, ask the platform — the first command answers the shapes, the
second the calls:
a9script authoring-pack
a9script authoring-pack --part functions-core
The language
Write modern JS. const and let, arrow functions, template strings,
for…of, spread, optional chaining (?.), ??, destructuring, and the
built-in methods — find, filter, map, includes — all work exactly as
they do anywhere else. Do not fall back on var and hand-written index loops:
nothing here needs them.
What is missing is a short list, and every one of these is refused by name when you lint, so there is nothing to guess at:
classthissupernew.targetasync/awaitgeneratorsimport/exporttagged templatesgetters/setterswithevaldebuggerFunctiondirectivesdestructuring defaultssparse arrayscomputed object keys
Two of those are worth a sentence. There is no async/await because a
platform call already waits for its answer — you write it as an ordinary call.
And there is no import: a main script links its libraries by name, and
everything they declare is in scope (above).
A loop beats map, find or filter only when the callback would make a
call. Over plain data use the built-in — items.find((item) => item.field === "status") — and keep a for loop for the case where each turn calls a far end,
because a loop can stop and a callback cannot.
For every built-in the language gives you, one line each with an example and its answer:
a9script authoring-pack --part standard-library
What return means depends on who is listening
A run started by a webhook or a schedule answers nobody: the value is kept
with the run and shown when somebody opens it, and nothing else reads it. So
return "not linked" is a note to your future self, not a message — useful,
and never load-bearing. A script with nothing to do may simply stop:
const linked = getMapping("issues", input.event.body.id);
if (linked === null) { return "not linked"; }
return linked;
Only a run started by a synchronous API call answers its caller, and there the value is what the caller receives.
The platform logs every call it makes for you
Each configured call, mapping read, send and file write already gets its own
line in the run’s log: the name, the argument keys, the outcome and how long it
took. So do not write log("calling X") or log("X answered") — they are
already there, at debug — below the log’s default filter.
Log the business fact the platform cannot know, once, with the key in the MESSAGE rather than only in the data — a search across runs matches messages:
const order = input.event.body;
log(`linked ${order.id} to ${order.customer}`);
One failure, one signal. logError marks the run’s health as your warning and
a throw marks it as the platform’s failure; both for the same problem report
it twice, and the worse one wins. throw where the run cannot go on,
logError where it can.
The edges of a data call
Datasets, mappings and state stay on the platform’s side; a script works
through them a slice at a time. Past an edge the call throws
DATA_CALL_TOO_LARGE, and its message names the call, the bound and the way
on — catch it only to do what it says.
- One platform call answers at most 1 MiB into a script. Pass names and handles; read large data back a page at a time.
- A page —
dsPage,listMappings,dsDiffKeys— is at most 1000 rows, and stops early at 768 KiB of values, never below one row. Continue past the last key you read and stop on an empty page — anoffsetsteps over every row before it:
let after;
for (;;) {
const page = dsPage("run:people", after === undefined ? { limit: 1000 } : { after, limit: 1000 });
if (page.length === 0) { break; }
// … work through page …
after = page[page.length - 1].key;
}
- A batch —
setMappings,getMappings,dsPutMany— is at most 10000 items;dsGetManyat most 1000 keys. Split longer lists. - A dataset holds at most 500000 rows and 512 MiB, one value at most
1 MiB (
DATASET_FULLpast it); a state value at most 64 KiB. dsStatsanswers at most 1000 groups; for a near-unique path usedsDuplicates.
A library is just source
{ "kind": "script_library", "name": "formatting", "codeFile": "formatting.js" }
function formatOrder(order) {
return order.id + " for " + order.customer;
}
No exports, no module wrapper — declare functions and constants at the top level and the main has them.
Testing what you wrote
a9script run <main> runs the main as configured: its libraries and its
settings script are in effect, exactly as they would be for a real trigger.
That is why the name is what you pass, and why there is no way to run a loose
piece of source — a test that ran without your libraries would not be a test of
what production does.
A library or a settings script has no run of its own. To exercise one, run a main that links it.