Docs / Agents: CLI, MCP, skill
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.
- 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 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.
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.
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.