Docs / Agents: CLI, MCP, skill
Authoring a9script integrations
You author an integration as a folder of files and put it into an
environment with one command. Everything below is done through the a9script
CLI; you never need the web UI.
Before you start
You need one thing, and you must be given it — do not guess it:
- a PAT stored once with
a9script login --server <url> --token <pat>.
The token IS your context: every token is bound to one environment at mint, and every command acts there — there is no environment flag. An environment belongs to one tenant and is isolated from every other; a sandbox and a production environment never see each other, and working in another environment means being handed another token.
Check the credential works — and see where it is bound — before writing anything:
a9script whoami --json
The answer’s token block names the tenant and environment every command
below will act on.
The loop
Work in this order, every time. It is short on purpose: each step’s output tells you what the next one should be.
1 · Look at what is already there. You are almost never the first author in an environment. Read it before you add to it:
a9script entity list --json
That is every entity, with its kind and name. Read one whole — a configured
call, a connection, a script — with entity get:
a9script entity get config_function FindContact --json
Read the calls you intend to use, not just their names: a function’s own definition says what it takes, what it answers, and whether it writes.
If the environment has inbound endpoints, read what each one takes before you write a script behind it:
a9script describe orders --json
That is the same document a caller fetching the endpoint’s own URL receives: its URL, the credential it asks for, the networks it answers, which scripts run behind it and under which filters, and the shapes it declares. Where it says a shape is undeclared, nobody has declared one — it is not a shape you may guess.
2 · Probe the reads before you write anything. A configured call can be fired on its own, and the answer carries the request as sent and the far end’s own response:
a9script probe FindContact --input '{"email":"ada@example.com"}' --json
That is how you learn the shape a script must read instead of guessing it. Reads probe freely; a call that WRITES is gated — see config functions.
3 · Write the files into a folder — shapes in the folder model, scripts and their links in scripts.
Give every call an expect, and an answer where it fetches a record. The
script then holds the record and checks no status: that is where most of the
ceremony in a first draft comes from. See
config functions.
4 · Lint the whole folder, and read the warnings. Nothing goes live:
a9script apply ./integration --dry-run --json
Each issue names the file it came from and the path inside it. Fix that file and repeat until it is clean. Warnings are separate, never change the exit code, and are the half people skip: each names a reference that resolves to nothing — a call to a function nobody has configured, a string naming a function or connection that does not exist, or one of the wrong type. A warning about a file you have not written yet is fine; a warning you did not expect is a typo you would otherwise meet mid-run.
5 · Apply for real. Every file that succeeds is live immediately:
a9script apply ./integration --json
6 · Run the script with the trace on. In a sandbox, always add --debug:
it keeps every outbound call’s request and response for you to read.
a9script run on-order --input _input.json --debug --json
A script that pauses itself is followed through the pause — you get the
finished run, not the pause. Add --wait 1h if it waits longer than the
ten-minute default, or --no-wait to start it and look later.
7 · When it fails, read the exchange — not just the error. The run’s error is your script’s view of what happened; the trace is the far end’s:
a9script history <run> --json
a9script inspect <run> 1 --json
history lists the calls that left the platform and points at the one worth
opening; inspect shows that call whole — the request as sent, every attempt
back, headers and bodies. The fix usually comes out of the response body, in
the far end’s own words. See debugging.
8 · Fix ONE file, then go back to step 5. A re-apply only touches what actually changed.
9 · Step the run when its middle writes. A new write path is the one place where “run it and see” is the wrong move. Start it stepped and it pauses after every call that leaves the platform, so you decide whether the next one happens:
a9script run place-order --input _input.json --step --json
a9script position <run> --json
a9script continue <run> --json
a9script abort <run> --json
sessions lists what is paused. See debugging.
Exit codes are the contract
0 it worked · 1 something is wrong that you cannot fix by editing files (no
credential, no network, the environment is locked by someone else) · 2 your
input is invalid — the issues say where.
a9script run follows the run and exits on the RUN’s outcome: 0 when it
succeeded, 1 when it failed. A run that pauses itself — a checkpoint wait,
a paged pull, a far end asking you to slow down — is followed through the
pause to its end, so a pause is never a 1 and what you get back is the
finished run.
You can bound the wait: --wait 30s (or 10m, 1h) sets the budget, 10
minutes by default; --no-wait starts the run and returns immediately. If the
budget runs out while the run is still healthy the exit is still 0, and
under --json the object carries waitExpired: true beside the wake time.
That is an outcome, not an error: the run continues without you, so read it
later — never start a second one to “retry” it. A --step run that PARKS also
exits 0, because starting the session is what you asked for.
Under --json stdout is always the run itself, so parse one shape and branch
on the code. a9script probe follows the same rule: 0 only when the call
answered, and the whole exchange on stdout either way.
Under --json, probe, history, inspect, sessions, position,
continue and abort all add a nextActions array — the concrete commands
worth running next. Read it; it is written for you.
What you build, and where it is described
- The file shapes and the folder rules: folder model.
- Saying what a caller must send, so an application or an assistant gets the shape right: parameter structures.
- Main scripts, libraries and the settings script — what links to what, and
what
inputholds: scripts. - The six config-function types and their fields — HTTP calls, text and email templates, CSV and JSON mappings, LLM prompts — plus how to probe one headlessly: config functions.
- Traces, stepping and what to do when a run is wrong: debugging.
- What an inbound endpoint takes, assembled from what the platform runs rather
than written down beside it:
a9script describe <endpoint>. - The standard task — “here is an API’s documentation, build the functions for it”: from an API description.
- Reaching all of this with nothing installed — a URL and a token, where every command is a tool and your files travel with the call: the same loop, with nothing installed.
What you cannot do — and the words to ask for it
Some entities carry credentials, and a token can only create, change, switch or delete those kinds when it was minted with “may manage credentialed kinds” — most agent tokens are not, on purpose. These are the kinds:
http_connection— a system you call, and the inbound endpoints it exposes.email_in— a mailbox the platform polls.email_out— a mail account the platform sends through.
The refusal names each entity and is all-or-nothing: nothing in that apply landed. Do not work around it — there is no other door, and the point of the gate is that a machine credential cannot repoint a credential at something else. Apply everything else, then hand the person you are working for a request they can act on without reading your files. Say, in this order:
- which entity you need, by kind and name (
http_connectionnamedcrm); - what it must reach — the base URL, and which auth scheme the far end
documents (bearer token, API key header and its name, basic, OAuth2); for a
bidirectional integration also the connection’s
identity(who the integration is in that system) and the endpoint’signorerules, which are what stop your own writes coming back as events; - what you will do with it — the calls you intend to configure, and whether any of them write;
- that you will not see the secret — they enter it once in the UI, your files leave it blank, and every later apply preserves it.
Then re-apply. You can always READ those entities (secrets masked) and
reference them by name — connectionName resolves by name, so your files never
need ids.
The same rule of thumb covers everything else you may be refused: a refusal that names a policy is a question for a person, not a problem to route around.
The rules that will bite you if you skip them
A run answers with return — output is a variable, not a function. The
platform reads a top-level variable named output only when nothing was
returned; calling output as if it were a function fails the run with
“output is not defined”. This is the mistake a live test of this skill
actually made — and the next rule is what caught it.
Never invent a function name. What a script may call is generated from the platform you are talking to, not from memory. Ask it:
a9script authoring-pack --json
a9script authoring-pack --part functions-core
a9script authoring-pack --part standard-library
Start with a9script authoring-pack alone — it states the execution model (no
imports, no entry point, what input holds, how a result is returned) and
names every part that exists; fetch the parts you need by name. This is the
same content the platform publishes as its function reference, and fetching it
is the point: the answer describes the deployment you are talking to.
Files carry names, never ids. No tenant, no environment, no uuids. Scope comes from the token you logged in with, and one entity refers to another by its name. The same folder therefore applies to sandbox and to production unchanged — with each environment’s own token.
A name is a key, not a label. Lowercase letters, digits and hyphens —
nightly-sync. The one exception is a config function, whose name is what a
script writes, so it is an identifier: FindContact. Anything else is refused
by name, at the door. What you want a person to read goes in displayName,
which is free text.
Writing needs a full-scope token. A config:read token reads config and
lints (apply --dry-run); an apply, an entity command, a lock, a transfer or a
restore with one is refused, naming the scope. Mint the token for what the job
actually does.
Treat your token as the environment itself. There is no tier between “reads config” and “everything in this environment”, so a full-scope token can activate config AND start runs — and a run calls every function configured there, using their stored credentials, which you never see. That is the whole blast radius of the token in your profile: not what you can read, but what the environment can reach. So never write it into a file you are authoring, an example, a log line or a commit; ask for the shortest life that covers the job rather than the longest allowed; and if you ever think it has been seen, say so and ask for it to be revoked, which takes effect at once. A token you are not using is one nobody has to trust.
Look before you write. The first thing to do against an unfamiliar API is
not to write a script — it is to probe one configured call and read what comes
back (step 2, above). A field name you assumed, an envelope you did not
expect, an id format nobody documented: all of it is one command away, and none
of it is guessable. Probe the error paths too — a missing id tells you which
status means not found; that status goes into expect beside
"empty": "null", not into a branch in your script.
A write is not yours to decide. The platform classifies every function as
a read or a write, and your token may only fire a write ad hoc when the
environment allows it AND you acknowledge it. That is deliberate: probing
DeleteProject must never be as easy as probing GetIssue. When you are
refused, do not retry and do not look for another door — tell the person you
are working for exactly which call you want to fire, against which system, and
what it would change. Reads need none of this.
Tokens expire. Every token has an expiry chosen at mint (two weeks unless
the deployment says otherwise). A 401 with code TOKEN_EXPIRED means exactly
that — mint a new token on the account page and swap it in; nothing else is
wrong with your request.
Never put a credential in a file — and your token could not post one in any case. A secret value is entered once in the UI, by a person; your files leave a blank where it goes, and a blank preserves what is stored. A file carrying a literal secret is refused, naming the file and the field, and nothing in that apply lands.
apply never deletes. Removing a file leaves its entity alone. Deleting is
always explicit:
a9script entity delete script_library helpers
Prefix your own files with _. Test inputs, notes, recorded outputs —
_input.json, _notes.md — are ignored when you apply a folder. Anything else
ending in .json is read as an entity. Naming a file explicitly overrides
this: a9script apply _input.json will try to apply it — an explicit name is
taken as intent.
Order is not yours to choose. apply writes connections and sources first,
then functions, settings, libraries, main scripts, and automations last (the
full order is in the folder model). That is why a
main script may link a library the same folder creates, and why a half-applied
folder can never fire: the trigger rides on the main, which is written last.
When something fails
--dry-runis clean butapplyfails — the failure is about the environment, not the files: the edit lock, a credential, a name that already belongs to a different kind of entity. Read the message.runsays there is no such script — the applied config reaches the runtime a moment after the apply. Retry briefly before concluding anything.- A run fails inside the script — the answer carries
error.codeanderror.message, andlogsup to the failure. AddlogInfo(...)and re-run; logs survive a failed run. - A call is not doing what you expect — probe the function on its own
(
a9script probe <name> --input '<json>'): the answer holds the request as sent and the response as received, which settles most arguments about whose fault it is in one command. - An unhappy HTTP status is an ANSWER, not a failure — unless the
function says which statuses it accepts (
expect), in which case any other answer stops the run with the far end’s own words. Configure that, and the script checks nothing; without it, branch onanswer.status. Either way the platform may already have repeated the call by the time you see it — a GET on any transient answer, a POST only where the far end said it did not process, unless the function is markedidempotentor itsretrynames the status — and the run’slogsname every attempt, with query values redacted. Only a call that could not be MADE at all throws — a catchablePlatformErrorcarrying the platform’s reason. - The environment is locked by someone else —
a9script lock statussays who. Do not force it; ask.
What to hand back
When you are done, say which entities you created or changed (the apply
report lists them), and how you proved it — the run you executed and what it
returned. A folder that was applied but never run has not been shown to work.
Name what you could NOT verify, too, and what is needed to finish it.