a9script

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 input holds: 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:

  1. which entity you need, by kind and name (http_connection named crm);
  2. 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’s ignore rules, which are what stop your own writes coming back as events;
  3. what you will do with it — the calls you intend to configure, and whether any of them write;
  4. 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 returnoutput 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-run is clean but apply fails — 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.
  • run says 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.code and error.message, and logs up to the failure. Add logInfo(...) 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 on answer.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 marked idempotent or its retry names the status — and the run’s logs name every attempt, with query values redacted. Only a call that could not be MADE at all throws — a catchable PlatformError carrying the platform’s reason.
  • The environment is locked by someone elsea9script lock status says 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.

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