a9script

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.

kindwhat it isruns?
script_mainthe integration itself — what a trigger firesyes
script_libraryshared functions a main usesonly as part of a main
script_settingsthis environment’s configuration, as dataonce, 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-run gives 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 carries id, kind, tenantId, environmentId, body and receivedAt; kind is 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:

  • 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

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.

Rendered from docs/skills/a9script-authoring/reference/scripts.md in the product's own repository, at build time. Found a problem on this page? Write to the address in the footer.