Script Language Reference
Complete reference for the ArchAstro script language — syntax, operators, namespaces, and patterns.
This reference is also available in the CLI via
archastro script docsorarchastro configs script-reference. This page is auto-generated from the platform source. Do not edit manually.
ArchAstro Script Language Reference
ArchAstro scripts are expression-oriented. Every statement produces a value. The last expression in a script is its return value. Statements are separated by semicolons or newlines (automatic semicolon insertion).
Comments
// line comment
/* block comment (nestable) */
Literals
- Numbers:
42,3.14,1e-5 - Strings:
"hello"or'hello'with escapes\n,\r,\t,\",\',\\ - Booleans:
true,false - Null:
null - Arrays:
[1, 2, 3] - Objects:
{key: "value", "other_key": 42}
Truthy / Falsy
Only these values are falsy: false, null, 0, 0.0, "" (empty string), [] (empty array).
Everything else is truthy, including empty objects {}.
Variables
let declares a binding. Variables are block-scoped. Rebinding a name in the same scope shadows the previous value.
let name = "world"
let count = 42
let items = [1, 2, 3]
let config = {key: "value", enabled: true}
let count = count + 1 // shadows previous count
Reserved names that cannot be used as variables: env, import, viewer, unwrap.
Operators
Precedence (highest to lowest):
- Member access:
.property,[index] - Function call:
fn(args) - Unary:
!,- - Multiplicative:
*,/,% - Additive:
+,- - Relational:
<,<=,>,>= - Equality:
==,!= - Logical AND:
&& - Logical OR:
|| - Ternary:
? : - Try-unwrap: postfix
?
Short-circuit operators
&& and || return actual values (not booleans), like JavaScript:
"hello" && "world" // "world"
0 && "skipped" // 0
null || "default" // "default"
"found" || "fallback" // "found"
String concatenation uses +: "hello " + "world".
Conditional Expressions
if (condition) {
thenValue
} else {
elseValue
}
Conditionals are expressions that return a value:
let label = if (count > 10) { "many" } else { "few" }
Ternary shorthand: condition ? thenValue : elseValue
Important: } else must be on the same line to avoid automatic semicolon insertion.
// CORRECT
if (x) { 1 } else { 2 }
// CORRECT
if (x) {
1
} else {
2
}
// WRONG — ASI inserts semicolon after }
if (x) {
1
}
else {
2
}
Functions
Anonymous functions:
fn(x) { x * 2 }
fn(a, b) { a + b }
Named functions (desugars to let binding):
fn double(x) { x * 2 }
double(5) // 10
Functions are first-class values — they can be passed as arguments, returned from other functions, and stored in variables. Closures capture their lexical scope at definition time. Named functions support recursion.
// Recursion
fn factorial(n) {
if (n <= 1) { 1 } else { n * factorial(n - 1) }
}
factorial(5) // 120
// Higher-order: passing functions as arguments
fn apply(f, x) { f(x) }
apply(fn(n) { n * 2 }, 5) // 10
// Closures capture outer scope
fn makeAdder(x) {
fn(y) { x + y }
}
let add5 = makeAdder(5)
add5(10) // 15
// Most common pattern: inline anonymous functions with namespaces
array.filter(items, fn(item) { item.active })
array.map(items, fn(item) { item.name })
array.reduce(items, 0, fn(sum, item) { sum + item.price })
Functions enforce exact parameter count — calling with wrong arity is an error.
JSONPath Access
$ accesses the root input payload. @ is the current item in projections/filters.
$.user.name
$.items[0]
$.items[*].price
$.items[?(@.active == true)]
Namespace Imports
Import a namespace to access its functions:
let arr = import("array")
arr.map([1, 2, 3], fn(x) { x * 2 })
Or call namespace functions directly without import:
array.map([1, 2, 3], fn(x) { x * 2 })
string.uppercase("hello")
Result Type
Operations that can fail return Result values:
- Ok:
{"ok": true, "value": <data>} - Err:
{"ok": false, "error": {"code": "error_code", "message": "description"}}
unwrap(result) — extracts value from Ok, halts script on Err.
unwrap(result, default) — extracts value from Ok, returns default on Err.
Postfix ? operator — unwraps Ok, early-returns Err from current function.
// Halt on error
let data = unwrap(http.get("https://api.example.com"))
// Provide fallback
let data = unwrap(http.get("https://api.example.com"), null)
// Early return in function
fn fetchUser(id) {
let resp = http.get("https://api.example.com/users/" + id)?
resp.body
}
Debugging
println(...) outputs values to the console panel. Takes any number of arguments.
println("user:", user)
println("count =", array.length(items))
Special Identifiers
$— JSONPath root input. Use $.field to read from workflow input payload.@— JSONPath current item. Available inside JSONPath projections and filters.
Environment Variables
Apps can configure environment variables (secrets, API keys, configuration). These are injected into scripts as the env object. Access them with dot notation:
env.API_KEY
env.WEBHOOK_SECRET
env.BASE_URL
env is a reserved name — you cannot use it as a variable name. Environment variables are read-only. If no environment variables are configured, env is not available and accessing it will produce an error.
// Use env vars for secrets in HTTP requests
let http = import("requests")
let resp = unwrap(http.post(env.WEBHOOK_URL, {
body: $.payload,
headers: {"Authorization": "Bearer " + env.API_TOKEN}
}))
resp.body
Builtin Functions
contains(string, substring), contains(list, value)— Returns true if a string contains a substring or a list contains a value. →booleanicontains(string, substring)— Case-insensitive substring check. →booleanimport(namespaceName)— Loads a namespace (array, map, string, math) and returns its function map. →namespacelowercase(string)— Returns a lowercased string. →stringmap(key1, value1, key2, value2, ...)— Builds a map from alternating key/value pairs. →mapmerge(leftMap, rightMap)— Merges two maps. Keys in rightMap overwrite leftMap. →mapprintln(...)— No documentation available. →anyput(map, key, value)— Returns a map with key set to value. Nil map input is treated as empty map. →mapunwrap(result), unwrap(result, default)— Extracts the value from an Ok result. Halts with an error if the result is Err. With two arguments, returns the default value instead of halting on Err. →any
Namespaces
array
Array/list helpers.
array.concat(listA, listB)— Concatenates two lists. →listarray.every(list, fn(item) -> boolean)— Returns true when all items satisfy the predicate. →booleanarray.filter(list, fn(item) -> boolean)— Returns items where predicate is truthy. →listarray.find(list, fn(item) -> boolean)— Returns first item matching predicate or nil. →any | nilarray.first(list)— Returns first list item or nil. →any | nilarray.flat(list), array.flat(list, depth)— Flattens nested lists (all levels by default). →listarray.indexOf(list, value)— Returns index of value or -1 when not found. →integerarray.join(list), array.join(list, separator)— Joins list values into a string. →stringarray.last(list)— Returns last list item or nil. →any | nilarray.length(list)— Returns list length. →integerarray.map(list, fn(item) -> value)— Transforms each list item with mapper function. →listarray.reduce(list, initial, fn(acc, item) -> nextAcc)— Reduces list into a single value. →anyarray.reverse(list)— Returns a reversed list. →listarray.slice(list, start), array.slice(list, start, stop)— Returns list slice with stop treated as exclusive. →listarray.some(list, fn(item) -> boolean)— Returns true if at least one item satisfies predicate. →boolean
datetime
Date and time operations: parsing, formatting, arithmetic, comparison, and timezone conversion.
datetime.add(datetime, amount, unit)— Adds a duration to a datetime. Amount can be negative to subtract. Units: seconds, minutes, hours, days, weeks, months, years. →Result<string>datetime.compare(a, b)— Compares two datetimes. Returns -1 if a < b, 0 if equal, 1 if a > b. →Result<number>datetime.diff(a, b, unit)— Returns the difference between two datetimes (a - b) in the given unit. Units: seconds, minutes, hours, days, weeks. →Result<number>datetime.format(datetime, pattern)— Formats a datetime using strftime patterns. Common: %Y (year), %m (month), %d (day), %H (hour), %M (minute), %S (second), %B (month name), %A (weekday name). →Result<string>datetime.now(), datetime.now(timezone)— Returns the current time as an ISO 8601 string. Without arguments returns UTC. With a timezone (e.g. "America/Denver") returns local time with offset. →string (ISO 8601)datetime.parse(string)— Parses a date or datetime string into a normalized ISO 8601 string. Accepts ISO 8601 dates ("2026-02-18"), datetimes ("2026-02-18T15:30:00Z"), and datetimes with offsets. →Result<string>datetime.parts(datetime)— Decomposes a datetime into its component parts as a map. →Result<{year, month, day, hour, minute, second, weekday, iso}>datetime.startOf(datetime, unit)— Truncates a datetime to the start of the given unit. Units: second, minute, hour, day, month, year. →Result<string>datetime.toTimezone(datetime, timezone)— Converts a datetime to the specified timezone. Returns an ISO 8601 string with the timezone offset. →Result<string>datetime.unix(), datetime.unix(datetime)— Returns a Unix timestamp (seconds since epoch). Without arguments returns the current UTC time. With a datetime string, converts it to a Unix timestamp. Useful for JWT iat/exp claims. →number (Unix timestamp in seconds)
Email sending and template rendering.
email.loadTemplate(template_id)— Loads an EmailTemplate config by ID, lookup_key, or virtual_path. Returns a Result containing the template fields. →Result<{id, name, html_template, text_template}>email.render(template, variables)— Renders a loaded email template with the given variables using Liquid syntax. Returns a Result with rendered html and text strings. →Result<{html, text}>email.send({to, subject, text_body, html_body?, cc?, bcc?, from_name?, from_email?, reply_to?})— Sends an email. Required fields: to, subject, text_body. Optional: html_body (defaults to text_body), cc, bcc, from_name, from_email, reply_to. →Result<{status, to, subject}>
jwt
Create and sign JSON Web Tokens for service authentication.
jwt.decode(token)— Decodes a JWT and returns the payload claims WITHOUT verifying the signature. Use this to read claims from a token, not to validate it. →Result with claims mapjwt.sign(claims, secret, algorithm)— Signs a JWT with the given claims, secret/key, and algorithm. Supported algorithms: RS256 (RSA private key PEM), HS256 (shared secret string). Returns a Result — use unwrap() to get the token string. →Result with signed JWT string
map
Map/object helpers.
map.delete(object, key)— Returns map without the provided key. →mapmap.entries(object)— Returns [[key, value], ...] pairs for a map. →listmap.filterKeys(object, fn(key) -> boolean)— Keeps entries whose key passes predicate. →mapmap.fromEntries(entries)— Builds a map from [[key, value], ...] entries. →mapmap.get(object, key), map.get(object, key, defaultValue)— Reads a value from a map with optional default fallback. →anymap.has(object, key)— Returns true when key exists in map. →booleanmap.keys(object)— Returns map keys. →listmap.mapValues(object, fn(value) -> newValue)— Transforms each value while preserving keys. →mapmap.merge(left, right)— Merges two maps. Keys in right overwrite left. →mapmap.put(object, key, value)— Returns map with key set to value. →mapmap.size(object)— Returns map size. →integermap.values(object)— Returns map values. →list
math
Math helpers.
math.abs(number)— Returns absolute value. →numbermath.ceil(number)— Rounds number up to nearest integer. →integermath.floor(number)— Rounds number down to nearest integer. →integermath.max(a, b), math.max(list)— Returns maximum of two numbers or max element from list. →numbermath.min(a, b), math.min(list)— Returns minimum of two numbers or min element from list. →numbermath.pow(base, exponent)— Returns base raised to exponent. →numbermath.round(number)— Rounds number to nearest integer. →integermath.sqrt(number)— Returns square root of non-negative numbers. →number
persona_templates
Bound API namespace persona_templates.
persona_templates.install({user: ..., template: ...})— Install a persona template for a user.
The template_id parameter accepts a persona template ID, key, or a config ID
(uuid, public id, lookup key, or virtual path) for a stored PersonaTemplate config.
→ The installed persona
persona_templates.list({app: ...})— List persona templates for an app →Resultpersona_templates.show({app: ..., persona_template: ...})— Show a single persona template →Persona template
personas
Bound API namespace personas.
personas.list({user: ..., filter: ...})— List personas for a user →Result
requests
HTTP client for making requests to external APIs.
http.delete(url), http.delete(url, { headers?, query?, body?, timeout?, auth? })— Makes an HTTP DELETE request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). →Result<{status, body, headers}>http.get(url), http.get(url, { headers?, query?, body?, timeout?, auth? })— Makes an HTTP GET request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). →Result<{status, body, headers}>http.head(url), http.head(url, { headers?, query?, body?, timeout?, auth? })— Makes an HTTP HEAD request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). →Result<{status, body, headers}>http.patch(url), http.patch(url, { headers?, query?, body?, timeout?, auth? })— Makes an HTTP PATCH request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). →Result<{status, body, headers}>http.post(url), http.post(url, { headers?, query?, body?, timeout?, auth? })— Makes an HTTP POST request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). →Result<{status, body, headers}>http.put(url), http.put(url, { headers?, query?, body?, timeout?, auth? })— Makes an HTTP PUT request. Options: headers (map), query (map), body (map or string), timeout (seconds, default 30), auth ({bearer: token} or {basic: {username, password}}). →Result<{status, body, headers}>
result
Result type helpers. All functions handle non-Result inputs defensively (no crashes).
result.err(message), result.err(code, message)— Constructs an Err result with optional code. →Resultresult.isErr(value)— Returns true if value is an Err result. Returns false for non-Result values. →booleanresult.isOk(value)— Returns true if value is an Ok result. Returns false for non-Result values. →booleanresult.map(result, fn(value) -> newValue)— Applies mapper to Ok value, returns Err unchanged. Returns non-Result values unchanged. →Resultresult.ok(value)— Constructs an Ok result wrapping the given value. →Resultresult.unwrapOr(result, default)— Returns the Ok value or the default. Returns default for non-Result values. →any
slack
Send messages to Slack channels via the agent's Slack bot integration.
slack.send({channel, text, thread_ts?})— Posts a message to a Slack channel. Requires channel (e.g. "#alerts") and text. Optional thread_ts for replying in a Slack thread. The agent must have integration/slack_bot installed. →Result with {ok: true} or error
string
String helpers.
string.capitalize(value)— Uppercases the first character, leaves the rest unchanged. →stringstring.charAt(value, index)— Returns the character at the given index, or null if out of bounds. Supports negative indices. →string | nullstring.endsWith(value, suffix)— Checks whether value ends with suffix. →booleanstring.format(template, ...args)— C-style string formatting. Supported specifiers: %s (string), %d (integer), %f (float, 6 decimals), %.Nf (float with N decimal places), %j (compact JSON), %J (pretty-printed JSON), %% (literal %). Example: string.format("Hello %s, you are %d", name, age) →stringstring.includes(value, substring)— Checks whether value contains substring. →booleanstring.indexOf(value, substring)— Returns the byte position of the first occurrence, or -1 if not found. →integerstring.lastIndexOf(value, substring)— Returns the byte position of the last occurrence, or -1 if not found. →integerstring.length(value)— Returns character count. →integerstring.lowercase(value)— Lowercases a string. →stringstring.match(value, pattern)— Runs a regex pattern against the string. Returns the first match with index and capture groups, or null if no match. →{match, index, groups} | nullstring.padEnd(value, targetLength), string.padEnd(value, targetLength, padString)— Pads the end of the string to the target length. Defaults to spaces. →stringstring.padStart(value, targetLength), string.padStart(value, targetLength, padString)— Pads the start of the string to the target length. Defaults to spaces. →stringstring.repeat(value, count)— Repeats the string count times. Max count is 10,000. →stringstring.replace(value, pattern, replacement)— Replaces all occurrences of a literal pattern with replacement. →stringstring.replacePattern(value, regexPattern, replacement)— Replaces all regex matches with replacement. Supports capture group backreferences (\1, \2). →stringstring.reverse(value)— Reverses the string. →stringstring.split(value, separator)— Splits a string into a list by separator. →liststring.startsWith(value, prefix)— Checks whether value starts with prefix. →booleanstring.substring(value, start), string.substring(value, start, length)— Returns a substring from start with optional length. →stringstring.test(value, pattern)— Tests whether a regex pattern matches anywhere in the string. →booleanstring.toNumber(value)— Parses a string to a number (integer or float). Returns null if the string is not a valid number. →number | nullstring.toString(value)— Converts any value to its string representation. Maps and lists are JSON-encoded. →stringstring.trim(value)— Trims surrounding whitespace. →stringstring.trimEnd(value)— Trims trailing whitespace. →stringstring.trimStart(value)— Trims leading whitespace. →stringstring.uppercase(value)— Uppercases a string. →string
threads
Bound API namespace threads.
threads.create({user: ..., thread: ..., skip_welcome_message: ...})— Create a thread for a user →The created threadthreads.list({user: ..., filter: ...})— List threads for a user →Resultthreads.toggle_persona({user: ..., thread: ..., persona: ..., enabled: ...})— Toggle a persona on/off for a user thread →204 No Content
users
Bound API namespace users.
users.create({app: ..., email: ..., full_name: ..., org: ..., org_role: ..., is_system_user: ..., skip_onboarding: ...})— Create a new user for an app →Created userusers.list({app: ..., page: ..., page_size: ..., search: ..., status: ..., is_system_user: ..., email: ..., org: ..., org_role: ...})— List paginated users for an app →Result
Examples
Data transformation
let items = $.order.items
let arr = import("array")
let total = arr.reduce(items, 0, fn(sum, item) {
sum + item.price * item.qty
})
{total: total, count: arr.length(items)}
Filtering and mapping
let users = $.users
let active = array.filter(users, fn(u) { u.status == "active" })
array.map(active, fn(u) {
{name: string.uppercase(u.name), email: u.email}
})
Conditional logic with defaults
let role = $.user.role || "viewer"
let limit = if (role == "admin") { 1000 } else { 100 }
{role: role, limit: limit}
String formatting
let name = $.user.name
let count = array.length($.items)
string.format("Hello %s, you have %d items", name, count)
Error handling
let http = import("requests")
let resp = unwrap(http.get($.api_url), null)
if (resp) {
resp.body
} else {
{error: "request failed"}
}
Working with dates
let dt = import("datetime")
let now = dt.now()
let deadline = unwrap(dt.parse($.due_date))
let days_left = dt.diff(deadline, now, "days")
if (days_left < 0) {
"overdue by " + string.toString(math.abs(days_left)) + " days"
} else {
string.toString(days_left) + " days remaining"
}
Building maps dynamically
let entries = array.map($.fields, fn(f) {
[f.key, string.trim(f.value)]
})
map.fromEntries(entries)
HTTP POST with headers
let http = import("requests")
let resp = unwrap(http.post("https://api.example.com/webhooks", {
body: {event: "order.created", data: $.order},
headers: {"X-Api-Key": $.api_key},
timeout: 30
}))
resp.body
Regex matching
let email = $.user.email
if (string.test(email, "^[^@]+@[^@]+\\.[^@]+$")) {
let parts = unwrap(string.match(email, "^([^@]+)@(.+)$"), null)
if (parts) {
{local: parts.groups[0], domain: parts.groups[1]}
} else {
{error: "parse failed"}
}
} else {
{error: "invalid email"}
}
Chained data pipeline
let orders = $.orders
// Filter → transform → aggregate
let result = array.filter(orders, fn(o) { o.status == "completed" })
let result = array.map(result, fn(o) {
{id: o.id, total: o.price * o.qty, date: o.created_at}
})
let grandTotal = array.reduce(result, 0, fn(sum, o) { sum + o.total })
{orders: result, grand_total: grandTotal, count: array.length(result)}
Function composition pattern
fn pipe(value, fns) {
array.reduce(fns, value, fn(acc, f) { f(acc) })
}
let result = pipe($.input, [
fn(s) { string.trim(s) },
fn(s) { string.lowercase(s) },
fn(s) { string.replace(s, " ", "-") }
])
result
Need something clearer?
Tell us where this page still falls short.
If a step is confusing, a diagram is misleading, or a workflow needs a better example, send feedback directly and we will tighten it.