a9script

Docs / Connections & config functions

Connections and functions

Two pieces of configuration carry almost everything a script leans on. A connection says where another system is and who you are to it. A function is one operation against it — or one piece of text, or one mapping — configured once and called by name.

Getting these two right is what makes scripts short. A script that reads as business logic is a script whose calls were configured properly.

Connections

A connection holds:

  • the address — the base URL every call is made relative to;
  • the credential — basic, bearer, an API key (in a header or in a query parameter), OAuth 2, or a signed assertion, whichever the far end wants;
  • the endpoints — the paths other systems call to reach you, each with its own rule for who is allowed to.

Three things follow from putting it here rather than in a script.

A script never holds a credential. It names the connection; the platform applies the secret as the call goes out. A secret is encrypted at rest, is never returned to any screen or command once stored, and does not appear in a run’s recorded request — the header is there, its value is not.

A call cannot wander. Requests go to the connection’s own address, and only there. A path or a continuation link that would leave it is refused rather than followed.

Both directions are one place. The orders endpoint your shop POSTs to and the calls you make out to that shop live on the same connection, which is where you look when the integration with that system is misbehaving.

Functions: six kinds of one idea

Whatever it does underneath, a function is a name your script calls with an object of arguments.

TypeWhat it is for
httpA call to another system. The one you will author most.
textA template filled with values — a message, a subject line, a small document.
emailA whole message: envelope, markdown body rendered to text and HTML, attachments.
csvOne columns table used in both directions: records to CSV, CSV to records.
jsonmapA target JSON structure built from a source one.
llmA prompt to a language model, with the answer parsed for you.
const contact = FindContact({ email: "ada@example.com" });                    // http
const message = OrderConfirmation({ name: "Ada", total: "30.00" });           // text
const target = ContactToCrm({ name: "Ada Lovelace", email: "ada@example.com" }); // jsonmap
const file = ContactsCsv([{ email: "ada@example.com", name: "Ada" }]);        // csv

log("built", { message: message, target: target.displayName, bytes: file.length });

return contact === null ? "no contact" : contact.id;

The two mapping types take one value rather than named arguments — the thing being mapped — because that is the whole of what they do.

An email function sends when it is called:

OrderReceipt({ to: input.event.body.customer, order: input.event.body.id });

return "receipt sent";

What each type asks you for

http — the connection, a method and a path, and the parameters it declares. A placeholder can read into the value it names — {{author.displayName}}, {{lines[0].sku}} — with the same dot path you would write in a script, so a caller hands over the object it already has instead of taking it apart first.

Each {{param}} goes wherever you put it: in the path, a query value, a header value, or the JSON body. Then, optionally: which statuses are an answer, what the answer is, how to page, what to repeat and what to wait for, how long it may go silent, and whether it is safe to repeat after a lost connection. A body can also be a form of parts, or a stored file sent whole — and a big response can go straight into a stored file instead of into your run.

When the body is not JSON

Three settings for the calls a JSON body cannot make, and each of them exists because the bytes are the problem.

A form of parts. Say the body is a form and list its parts. A part has a field name, and its content is either a template value or a stored file the caller names — one or the other, never both and never neither, because a part with no content is a field the far end receives empty and one with two is the platform choosing on your behalf. A part may also say what the far end should call the file and what type it is. You do not write the boundary; that is the platform’s, and a part header carrying a quote or a line break is refused where you type it.

A stored file as the whole body. Where the far end wants the bytes and nothing else, name the parameter that carries the file’s handle. The platform streams it from where it is stored — your script passes the handle it was given and never holds the contents.

A big answer into a file. A call can put a successful response straight into a stored file and hand your script the handle instead of the value. That is the only way a download larger than a run can hold is usable at all. An error page is left readable: a failure you cannot see is worse than a large one.

All three are about the same thing. A script orchestrates; the bytes go from where they are to where they are going without passing through it.

text — a format (plain or markdown) and a template with {{param}} holes. It answers the filled string; it sends nothing. Values are not escaped, because a template that quietly escaped them could not write the markup its author meant — escapeHtml is the answer where a value is untrusted markup.

email — the envelope (to, cc, bcc, from, replyTo, all templated, comma-separated where there are several), a subject, and a markdown body that is rendered to both an HTML and a plain-text part. An attachment is the value passed for a parameter — what createPdf or a stored file handed you — so a script sends a document by passing it, and the function says what it is called. Name the mail account that sends it, or leave it blank if the environment has exactly one. What that account then does with the message is the account’s own setting — see Mail.

csv — one columns table: for each column, its name in the file, where its value lives in a record, its type (string, number, boolean, date) and optionally the longest it may be. The same table works both ways — records in, CSV text out; CSV text in, typed records out — which is what keeps a file you write and a file you read from meaning different things. The file’s own settings sit beside it: delimiter, header row, byte-order mark, date format, and the formula guard — on unless you turn it off — which keeps a cell starting with =, +, - or @ from being run as a formula by whoever opens the file in a spreadsheet.

jsonmap — the target structure, written as JSON with {{path}} holes pointing into the source value. Say what a missing value does: write null (the default) or leave the key out.

llm — a provider and a model, a system and a user prompt with {{param}} holes, a cap on the answer’s length, and optionally JSON mode, which parses the answer for you. See the warning at the end of this page.

Whichever type it is, a Test button is on its form: fill in the parameters, press it, and read exactly what a script would get.

What a mail account does with a message

Building something that sends means running it, and running it means it sends — so every mail account says what it does with a message: deliver it, redirect every recipient to a fixed list, or record it into the run’s log and send nothing. Mail is the page about accounts, mailboxes and that setting, including which of the three to build against.

A parameter is a value, never structure

{{email}} inside a path, a query value, a header value, a JSON body or a template is replaced with the value you passed — encoded correctly for the place it sits in. It cannot become a second query parameter, an extra header, or a new field in a body: a template that tried would be refused when it was authored.

That is what makes it safe to build a call out of whatever arrived in a webhook.

What a call answers is a decision, not a discovery

By default an HTTP function hands your script both halves of the answer — { status, body } — and a status outside 200–299 arrives the same way, as something to branch on.

Most well-configured calls narrow that, and it is worth doing:

  • Which statuses are an answer. Anything else stops the run where the call was made, saying what came back: the status, what the far end sent, and how many times it had already been asked. Your script writes no status check — if the next line runs, the call was answered.
  • What the answer is. The value at a path in the body, or the single record found in a collection. Several records where one was expected stops the run rather than picking one, because which is right is a business question.
  • What nothing means. A search that matched nothing, a “not there” status, or a null where the value should be — which is how a far end says it made no record — can be an ordinary null you branch on, or it can stop the run, naming what was looked for. Stopping is what happens unless you say otherwise, so a create that quietly made nothing never reaches your script as a value.

So the same call site can read as either of these, and the difference is configuration:

// Two functions, the same request, different configuration.
const response = ContactSearch({ email: "ada@example.com" });   // { status, body }
const contact = FindContact({ email: "ada@example.com" });      // the contact itself

return { status: response.status, contact: contact === null ? null : contact.id };

You cannot tell from the call which it is, and that is the point: a call can be made stricter later without touching a single script. Press Test and read what comes back — the panel shows exactly what a script receives.

When a call does not simply succeed

Two different things, worth telling apart before you configure either.

Repeating is the platform making the same call again, moments apart, while your run waits: when nothing answered at all, and when the answer means not now. It never repeats a 400, 401, 404 or 422, because asking again cannot change them.

Which calls are repeated on a 5xx depends on what the call does. A GET, a PUT or a DELETE promises that nothing happens twice, so it is asked again like any transient answer. A POST or a PATCH changes something: the far end may have created the record and then failed to answer, and asking again would create it twice — so a call like that is repeated only where the far end says it did not process (408, 425, 429), never on a 5xx, unless you mark it idempotent or name the status yourself. A dead connection follows the same line: one that never reached the far end — a refused port, a name that did not resolve — is retried for every call; one that died after the request may have gone out is retried only for a call that is safe to repeat.

Waiting is bigger. On a status you name as a long wait — 429 is the usual one — the whole run is put away for as long as the far end asked, holding nothing, and the call is made again when it wakes. Naming a status that way also stops it being repeated on the spot: waiting wins.

A call you mark idempotent is treated like a GET in both places: it is repeated on a 5xx, and it may be repeated after the connection died mid-request, where the platform cannot know whether the far end got it. Mark a create only if the far end deduplicates it.

A guard, not the API. Some APIs sit behind a bot challenge — a guard that answers instead of the API, expecting a browser to run its scripts before the real request goes through. When the platform recognises one (a vendor’s marker header, or an HTML page where your call declared it expects data), it refuses the call and names what happened; it will never try to solve the challenge. A far end behind a bot challenge needs an arrangement with its owner, not a cleverer script — and the API’s own credential usually exempts a caller from the challenge in the first place.

Every number attached to this — how many repeats, how long a call may be silent, how long one wait may last — is on Limits and safety.

Paging is configuration too

If a call answers a collection a page at a time, say so on the function: where the items are, and how the next page is asked for. Then one line reads the whole thing:

const pull = dsFetchInto("ListContacts", {}, "run:contacts");

return { pages: pull.pages, rows: pull.rows };

No cursor loop, no page counter, and it keeps its place if the far end makes it wait.

This works for a GraphQL API too, whose cursor travels inside the posted body rather than in the URL: the call declares a parameter for it, the body writes that parameter where the query wants it, and the function says where the answer puts the next cursor and whether there is more.

A model’s answer is input, not truth

An llm function is configured like any other call — a prompt with {{param}} holes, and a model behind it — and answers text, or a parsed object in JSON mode:

const verdict = ClassifyMessage({ text: "the invoice never arrived" });

log("classified", { model: verdict.model, tokens: verdict.usage.outputTokens });

return verdict.json;

Whatever comes back is untrusted input, and it is your job to check it. A model can answer with a category you never defined, an id that does not exist, a number as words, or an empty object — none of which is an error, and none of which the platform can catch for you. Validate the shape before you act on it, and never pass a model’s answer straight into a call that changes something.

Rendered from docs/guide/connections-and-functions.md in the product's own repository, at build time. Found a problem on this page? Write to the address in the footer.